xref: /haiku/src/apps/webpositive/tabview/TabContainerView.cpp (revision 7c095f47094e418e0c071dd84d861699660d35b9)
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 
TabContainerView(Controller * controller)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 
~TabContainerView()50 TabContainerView::~TabContainerView()
51 {
52 }
53 
54 
55 BSize
MinSize()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
MessageReceived(BMessage * message)66 TabContainerView::MessageReceived(BMessage* message)
67 {
68 	switch (message->what) {
69 		default:
70 			BGroupView::MessageReceived(message);
71 	}
72 }
73 
74 
75 void
Draw(BRect updateRect)76 TabContainerView::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 
98 void
MouseDown(BPoint where)99 TabContainerView::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 
133 void
MouseUp(BPoint where)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
MouseMoved(BPoint where,uint32 transit,const BMessage * dragMessage)155 TabContainerView::MouseMoved(BPoint where, uint32 transit,
156 	const BMessage* dragMessage)
157 {
158 	_MouseMoved(where, transit, dragMessage);
159 }
160 
161 
162 void
DoLayout()163 TabContainerView::DoLayout()
164 {
165 	BGroupView::DoLayout();
166 
167 	_ValidateTabVisibility();
168 	_SendFakeMouseMoved();
169 }
170 
171 void
AddTab(const char * label,int32 index)172 TabContainerView::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 
185 void
AddTab(TabView * tab,int32 index)186 TabContainerView::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 
214 TabView*
RemoveTab(int32 index)215 TabContainerView::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 
261 TabView*
TabAt(int32 index) const262 TabContainerView::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 
273 int32
IndexOf(TabView * tab) const274 TabContainerView::IndexOf(TabView* tab) const
275 {
276 	if (tab == NULL || GroupLayout() == NULL)
277 		return -1;
278 
279 	return GroupLayout()->IndexOfItem(tab->LayoutItem());
280 }
281 
282 
283 void
SelectTab(int32 index)284 TabContainerView::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 
296 void
SelectTab(TabView * tab)297 TabContainerView::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 
324 void
SetTabLabel(int32 index,const char * label)325 TabContainerView::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 
336 void
SetFirstVisibleTabIndex(int32 index)337 TabContainerView::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 
352 int32
FirstVisibleTabIndex() const353 TabContainerView::FirstVisibleTabIndex() const
354 {
355 	return fFirstVisibleTabIndex;
356 }
357 
358 
359 int32
MaxFirstVisibleTabIndex() const360 TabContainerView::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 
388 bool
CanScrollLeft() const389 TabContainerView::CanScrollLeft() const
390 {
391 	return fFirstVisibleTabIndex < MaxFirstVisibleTabIndex();
392 }
393 
394 
395 bool
CanScrollRight() const396 TabContainerView::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 
412 TabView*
_TabAt(const BPoint & where) const413 TabContainerView::_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 
432 void
_MouseMoved(BPoint where,uint32 _transit,const BMessage * dragMessage)433 TabContainerView::_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 
461 void
_ValidateTabVisibility()462 TabContainerView::_ValidateTabVisibility()
463 {
464 	if (fFirstVisibleTabIndex > MaxFirstVisibleTabIndex())
465 		SetFirstVisibleTabIndex(MaxFirstVisibleTabIndex());
466 	else
467 		_UpdateTabVisibility();
468 }
469 
470 
471 void
_UpdateTabVisibility()472 TabContainerView::_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 
502 float
_AvailableWidthForTabs() const503 TabContainerView::_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 
517 void
_SendFakeMouseMoved()518 TabContainerView::_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