xref: /haiku/src/apps/webpositive/tabview/TabContainerView.cpp (revision 5e96d7d537fbec23bad4ae9b4c8e7b02e769f0c6)
1 /*
2  * Copyright (C) 2010 Rene Gollent <rene@gollent.com>
3  * Copyright (C) 2010 Stephan Aßmus <superstippi@gmx.de>
4  *
5  * All rights reserved. Distributed under the terms of the MIT License.
6  */
7 
8 #include "TabContainerView.h"
9 
10 #include <stdio.h>
11 
12 #include <Application.h>
13 #include <AbstractLayoutItem.h>
14 #include <Bitmap.h>
15 #include <Button.h>
16 #include <CardLayout.h>
17 #include <Catalog.h>
18 #include <ControlLook.h>
19 #include <GroupView.h>
20 #include <MenuBar.h>
21 #include <SpaceLayoutItem.h>
22 #include <Window.h>
23 
24 #include "TabView.h"
25 
26 
27 #undef B_TRANSLATION_CONTEXT
28 #define B_TRANSLATION_CONTEXT "Tab Manager"
29 
30 
31 static const float kLeftTabInset = 4;
32 
33 
34 TabContainerView::TabContainerView(Controller* controller)
35 	:
36 	BGroupView(B_HORIZONTAL, 0.0),
37 	fLastMouseEventTab(NULL),
38 	fMouseDown(false),
39 	fClickCount(0),
40 	fSelectedTab(NULL),
41 	fController(controller),
42 	fFirstVisibleTabIndex(0)
43 {
44 	SetFlags(Flags() | B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE);
45 	SetViewColor(B_TRANSPARENT_COLOR);
46 	GroupLayout()->SetInsets(kLeftTabInset, 0, 0, 1);
47 	GroupLayout()->AddItem(BSpaceLayoutItem::CreateGlue(), 0.0f);
48 }
49 
50 
51 TabContainerView::~TabContainerView()
52 {
53 }
54 
55 
56 BSize
57 TabContainerView::MinSize()
58 {
59 	// Eventually, we want to be scrolling if the tabs don't fit.
60 	BSize size(BGroupView::MinSize());
61 	size.width = 300;
62 	return size;
63 }
64 
65 
66 void
67 TabContainerView::MessageReceived(BMessage* message)
68 {
69 	switch (message->what) {
70 		default:
71 			BGroupView::MessageReceived(message);
72 	}
73 }
74 
75 
76 void
77 TabContainerView::Draw(BRect updateRect)
78 {
79 	// Stroke separator line at bottom.
80 	rgb_color base = ui_color(B_PANEL_BACKGROUND_COLOR);
81 	BRect frame(Bounds());
82 	SetHighColor(tint_color(base, B_DARKEN_2_TINT));
83 	StrokeLine(frame.LeftBottom(), frame.RightBottom());
84 	frame.bottom--;
85 
86 	// Draw empty area before first tab.
87 	uint32 borders = BControlLook::B_TOP_BORDER | BControlLook::B_BOTTOM_BORDER;
88 	BRect leftFrame(frame.left, frame.top, kLeftTabInset, frame.bottom);
89 	be_control_look->DrawInactiveTab(this, leftFrame, updateRect, base, 0,
90 		borders);
91 
92 	// Draw all tabs, keeping track of where they end.
93 	BGroupLayout* layout = GroupLayout();
94 	int32 count = layout->CountItems() - 1;
95 	for (int32 i = 0; i < count; i++) {
96 		TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
97 			layout->ItemAt(i));
98 		if (!item || !item->IsVisible())
99 			continue;
100 		item->Parent()->Draw(updateRect);
101 		frame.left = item->Frame().right + 1;
102 	}
103 
104 	// Draw empty area after last tab.
105 	be_control_look->DrawInactiveTab(this, frame, updateRect, base, 0, borders);
106 }
107 
108 
109 void
110 TabContainerView::MouseDown(BPoint where)
111 {
112 	uint32 buttons;
113 	if (Window()->CurrentMessage()->FindInt32("buttons", (int32*)&buttons) != B_OK)
114 		buttons = B_PRIMARY_MOUSE_BUTTON;
115 	uint32 clicks;
116 	if (Window()->CurrentMessage()->FindInt32("clicks", (int32*)&clicks) != B_OK)
117 		clicks = 1;
118 	fMouseDown = true;
119 	SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS);
120 	if (fLastMouseEventTab)
121 		fLastMouseEventTab->MouseDown(where, buttons);
122 	else {
123 		if ((buttons & B_TERTIARY_MOUSE_BUTTON) != 0) {
124 			// Middle click outside tabs should always open a new tab.
125 			fController->DoubleClickOutsideTabs();
126 		} else if (clicks > 1)
127 			fClickCount++;
128 		else
129 			fClickCount = 1;
130 	}
131 }
132 
133 
134 void
135 TabContainerView::MouseUp(BPoint where)
136 {
137 	fMouseDown = false;
138 	if (fLastMouseEventTab) {
139 		fLastMouseEventTab->MouseUp(where);
140 		fClickCount = 0;
141 	} else if (fClickCount > 1) {
142 		// NOTE: fClickCount is >= 1 only if the first click was outside
143 		// any tab. So even if fLastMouseEventTab has been reset to NULL
144 		// because this tab was removed during mouse down, we wouldn't
145 		// run the "outside tabs" code below.
146 		fController->DoubleClickOutsideTabs();
147 		fClickCount = 0;
148 	}
149 	// Always check the tab under the mouse again, since we don't update
150 	// it with fMouseDown == true.
151 	_SendFakeMouseMoved();
152 }
153 
154 
155 void
156 TabContainerView::MouseMoved(BPoint where, uint32 transit,
157 	const BMessage* dragMessage)
158 {
159 	_MouseMoved(where, transit, dragMessage);
160 }
161 
162 
163 void
164 TabContainerView::DoLayout()
165 {
166 	BGroupView::DoLayout();
167 
168 	_ValidateTabVisibility();
169 	_SendFakeMouseMoved();
170 }
171 
172 void
173 TabContainerView::AddTab(const char* label, int32 index)
174 {
175 	TabView* tab;
176 	if (fController)
177 		tab = fController->CreateTabView();
178 	else
179 		tab = new TabView();
180 	tab->SetLabel(label);
181 	AddTab(tab, index);
182 }
183 
184 
185 void
186 TabContainerView::AddTab(TabView* tab, int32 index)
187 {
188 	tab->SetContainerView(this);
189 
190 	if (index == -1)
191 		index = GroupLayout()->CountItems() - 1;
192 
193 	bool hasFrames = fController != NULL && fController->HasFrames();
194 	bool isFirst = index == 0 && hasFrames;
195 	bool isLast = index == GroupLayout()->CountItems() - 1 && hasFrames;
196 	bool isFront = fSelectedTab == NULL;
197 	tab->Update(isFirst, isLast, isFront);
198 
199 	GroupLayout()->AddItem(index, tab->LayoutItem());
200 
201 	if (isFront)
202 		SelectTab(tab);
203 	if (isLast) {
204 		TabLayoutItem* item
205 			= dynamic_cast<TabLayoutItem*>(GroupLayout()->ItemAt(index - 1));
206 		if (item)
207 			item->Parent()->SetIsLast(false);
208 	}
209 
210 	SetFirstVisibleTabIndex(MaxFirstVisibleTabIndex());
211 	_ValidateTabVisibility();
212 }
213 
214 
215 TabView*
216 TabContainerView::RemoveTab(int32 index)
217 {
218 	TabLayoutItem* item
219 		= dynamic_cast<TabLayoutItem*>(GroupLayout()->RemoveItem(index));
220 
221 	if (!item)
222 		return NULL;
223 
224 	BRect dirty(Bounds());
225 	dirty.left = item->Frame().left;
226 	TabView* removedTab = item->Parent();
227 	removedTab->SetContainerView(NULL);
228 
229 	if (removedTab == fLastMouseEventTab)
230 		fLastMouseEventTab = NULL;
231 
232 	// Update tabs after or before the removed tab.
233 	bool hasFrames = fController != NULL && fController->HasFrames();
234 	item = dynamic_cast<TabLayoutItem*>(GroupLayout()->ItemAt(index));
235 	if (item) {
236 		// This tab is behind the removed tab.
237 		TabView* tab = item->Parent();
238 		tab->Update(index == 0 && hasFrames,
239 			index == GroupLayout()->CountItems() - 2 && hasFrames,
240 			tab == fSelectedTab);
241 		if (removedTab == fSelectedTab) {
242 			fSelectedTab = NULL;
243 			SelectTab(tab);
244 		} else if (fController && tab == fSelectedTab)
245 			fController->TabSelected(index);
246 	} else {
247 		// The removed tab was the last tab.
248 		item = dynamic_cast<TabLayoutItem*>(GroupLayout()->ItemAt(index - 1));
249 		if (item) {
250 			TabView* tab = item->Parent();
251 			tab->Update(index == 0 && hasFrames,
252 				index == GroupLayout()->CountItems() - 2 && hasFrames,
253 				tab == fSelectedTab);
254 			if (removedTab == fSelectedTab) {
255 				fSelectedTab = NULL;
256 				SelectTab(tab);
257 			}
258 		}
259 	}
260 
261 	Invalidate(dirty);
262 	_ValidateTabVisibility();
263 
264 	return removedTab;
265 }
266 
267 
268 TabView*
269 TabContainerView::TabAt(int32 index) const
270 {
271 	TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
272 		GroupLayout()->ItemAt(index));
273 	if (item)
274 		return item->Parent();
275 	return NULL;
276 }
277 
278 
279 int32
280 TabContainerView::IndexOf(TabView* tab) const
281 {
282 	return GroupLayout()->IndexOfItem(tab->LayoutItem());
283 }
284 
285 
286 void
287 TabContainerView::SelectTab(int32 index)
288 {
289 	TabView* tab = NULL;
290 	TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
291 		GroupLayout()->ItemAt(index));
292 	if (item)
293 		tab = item->Parent();
294 
295 	SelectTab(tab);
296 }
297 
298 
299 void
300 TabContainerView::SelectTab(TabView* tab)
301 {
302 	if (tab == fSelectedTab)
303 		return;
304 
305 	if (fSelectedTab)
306 		fSelectedTab->SetIsFront(false);
307 
308 	fSelectedTab = tab;
309 
310 	if (fSelectedTab)
311 		fSelectedTab->SetIsFront(true);
312 
313 	if (fController != NULL) {
314 		int32 index = -1;
315 		if (fSelectedTab != NULL)
316 			index = GroupLayout()->IndexOfItem(tab->LayoutItem());
317 
318 		if (!tab->LayoutItem()->IsVisible())
319 			SetFirstVisibleTabIndex(index);
320 
321 		fController->TabSelected(index);
322 	}
323 }
324 
325 
326 void
327 TabContainerView::SetTabLabel(int32 tabIndex, const char* label)
328 {
329 	TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
330 		GroupLayout()->ItemAt(tabIndex));
331 	if (item == NULL)
332 		return;
333 
334 	item->Parent()->SetLabel(label);
335 }
336 
337 
338 void
339 TabContainerView::SetFirstVisibleTabIndex(int32 index)
340 {
341 	if (index < 0)
342 		index = 0;
343 	if (index > MaxFirstVisibleTabIndex())
344 		index = MaxFirstVisibleTabIndex();
345 	if (fFirstVisibleTabIndex == index)
346 		return;
347 
348 	fFirstVisibleTabIndex = index;
349 
350 	_UpdateTabVisibility();
351 }
352 
353 
354 int32
355 TabContainerView::FirstVisibleTabIndex() const
356 {
357 	return fFirstVisibleTabIndex;
358 }
359 
360 
361 int32
362 TabContainerView::MaxFirstVisibleTabIndex() const
363 {
364 	float availableWidth = _AvailableWidthForTabs();
365 	if (availableWidth < 0)
366 		return 0;
367 	float visibleTabsWidth = 0;
368 
369 	BGroupLayout* layout = GroupLayout();
370 	int32 i = layout->CountItems() - 2;
371 	for (; i >= 0; i--) {
372 		TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
373 			layout->ItemAt(i));
374 		if (item == NULL)
375 			continue;
376 
377 		float itemWidth = item->MinSize().width;
378 		if (availableWidth >= visibleTabsWidth + itemWidth)
379 			visibleTabsWidth += itemWidth;
380 		else {
381 			// The tab before this tab is the last one that can be visible.
382 			return i + 1;
383 		}
384 	}
385 
386 	return 0;
387 }
388 
389 
390 bool
391 TabContainerView::CanScrollLeft() const
392 {
393 	return fFirstVisibleTabIndex < MaxFirstVisibleTabIndex();
394 }
395 
396 
397 bool
398 TabContainerView::CanScrollRight() const
399 {
400 	BGroupLayout* layout = GroupLayout();
401 	int32 count = layout->CountItems() - 1;
402 	if (count > 0) {
403 		TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
404 			layout->ItemAt(count - 1));
405 		return !item->IsVisible();
406 	}
407 	return false;
408 }
409 
410 
411 // #pragma mark -
412 
413 
414 TabView*
415 TabContainerView::_TabAt(const BPoint& where) const
416 {
417 	BGroupLayout* layout = GroupLayout();
418 	int32 count = layout->CountItems() - 1;
419 	for (int32 i = 0; i < count; i++) {
420 		TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(layout->ItemAt(i));
421 		if (item == NULL || !item->IsVisible())
422 			continue;
423 		// Account for the fact that the tab frame does not contain the
424 		// visible bottom border.
425 		BRect frame = item->Frame();
426 		frame.bottom++;
427 		if (frame.Contains(where))
428 			return item->Parent();
429 	}
430 	return NULL;
431 }
432 
433 
434 void
435 TabContainerView::_MouseMoved(BPoint where, uint32 _transit,
436 	const BMessage* dragMessage)
437 {
438 	TabView* tab = _TabAt(where);
439 	if (fMouseDown) {
440 		uint32 transit = tab == fLastMouseEventTab
441 			? B_INSIDE_VIEW : B_OUTSIDE_VIEW;
442 		if (fLastMouseEventTab)
443 			fLastMouseEventTab->MouseMoved(where, transit, dragMessage);
444 		return;
445 	}
446 
447 	if (fLastMouseEventTab != NULL && fLastMouseEventTab == tab)
448 		fLastMouseEventTab->MouseMoved(where, B_INSIDE_VIEW, dragMessage);
449 	else {
450 		if (fLastMouseEventTab)
451 			fLastMouseEventTab->MouseMoved(where, B_EXITED_VIEW, dragMessage);
452 		fLastMouseEventTab = tab;
453 		if (fLastMouseEventTab)
454 			fLastMouseEventTab->MouseMoved(where, B_ENTERED_VIEW, dragMessage);
455 		else {
456 			fController->SetToolTip(
457 				B_TRANSLATE("Double-click or middle-click to open new tab."));
458 		}
459 	}
460 }
461 
462 
463 void
464 TabContainerView::_ValidateTabVisibility()
465 {
466 	if (fFirstVisibleTabIndex > MaxFirstVisibleTabIndex())
467 		SetFirstVisibleTabIndex(MaxFirstVisibleTabIndex());
468 	else
469 		_UpdateTabVisibility();
470 }
471 
472 
473 void
474 TabContainerView::_UpdateTabVisibility()
475 {
476 	float availableWidth = _AvailableWidthForTabs();
477 	if (availableWidth < 0)
478 		return;
479 	float visibleTabsWidth = 0;
480 
481 	bool canScrollTabsLeft = fFirstVisibleTabIndex > 0;
482 	bool canScrollTabsRight = false;
483 
484 	BGroupLayout* layout = GroupLayout();
485 	int32 count = layout->CountItems() - 1;
486 	for (int32 i = 0; i < count; i++) {
487 		TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
488 			layout->ItemAt(i));
489 		if (i < fFirstVisibleTabIndex)
490 			item->SetVisible(false);
491 		else {
492 			float itemWidth = item->MinSize().width;
493 			bool visible = availableWidth >= visibleTabsWidth + itemWidth;
494 			item->SetVisible(visible && !canScrollTabsRight);
495 			visibleTabsWidth += itemWidth;
496 			if (!visible)
497 				canScrollTabsRight = true;
498 		}
499 	}
500 	fController->UpdateTabScrollability(canScrollTabsLeft, canScrollTabsRight);
501 }
502 
503 
504 float
505 TabContainerView::_AvailableWidthForTabs() const
506 {
507 	float width = Bounds().Width() - 10;
508 		// TODO: Don't really know why -10 is needed above.
509 
510 	float left;
511 	float right;
512 	GroupLayout()->GetInsets(&left, NULL, &right, NULL);
513 	width -= left + right;
514 
515 	return width;
516 }
517 
518 
519 void
520 TabContainerView::_SendFakeMouseMoved()
521 {
522 	BPoint where;
523 	uint32 buttons;
524 	GetMouse(&where, &buttons, false);
525 	if (Bounds().Contains(where))
526 		_MouseMoved(where, B_INSIDE_VIEW, NULL);
527 }
528