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