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