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