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