xref: /haiku/src/apps/webpositive/tabview/TabContainerView.cpp (revision 7a74a5df454197933bc6e80a542102362ee98703)
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 
47 static const float kLeftTabInset = 4;
48 
49 
50 TabContainerView::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 
67 TabContainerView::~TabContainerView()
68 {
69 }
70 
71 
72 BSize
73 TabContainerView::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 
82 void
83 TabContainerView::MessageReceived(BMessage* message)
84 {
85 	switch (message->what) {
86 		default:
87 			BGroupView::MessageReceived(message);
88 	}
89 }
90 
91 
92 void
93 TabContainerView::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 
125 void
126 TabContainerView::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 
150 void
151 TabContainerView::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 
171 void
172 TabContainerView::MouseMoved(BPoint where, uint32 transit,
173 	const BMessage* dragMessage)
174 {
175 	_MouseMoved(where, transit, dragMessage);
176 }
177 
178 
179 void
180 TabContainerView::DoLayout()
181 {
182 	BGroupView::DoLayout();
183 
184 	_ValidateTabVisibility();
185 	_SendFakeMouseMoved();
186 }
187 
188 void
189 TabContainerView::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 
201 void
202 TabContainerView::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 
230 TabView*
231 TabContainerView::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 
283 TabView*
284 TabContainerView::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 
294 int32
295 TabContainerView::IndexOf(TabView* tab) const
296 {
297 	return GroupLayout()->IndexOfItem(tab->LayoutItem());
298 }
299 
300 
301 void
302 TabContainerView::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 
314 void
315 TabContainerView::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 
342 void
343 TabContainerView::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 
354 void
355 TabContainerView::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 
370 int32
371 TabContainerView::FirstVisibleTabIndex() const
372 {
373 	return fFirstVisibleTabIndex;
374 }
375 
376 
377 int32
378 TabContainerView::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 
406 bool
407 TabContainerView::CanScrollLeft() const
408 {
409 	return fFirstVisibleTabIndex < MaxFirstVisibleTabIndex();
410 }
411 
412 
413 bool
414 TabContainerView::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 
430 TabView*
431 TabContainerView::_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 
450 void
451 TabContainerView::_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 
477 void
478 TabContainerView::_ValidateTabVisibility()
479 {
480 	if (fFirstVisibleTabIndex > MaxFirstVisibleTabIndex())
481 		SetFirstVisibleTabIndex(MaxFirstVisibleTabIndex());
482 	else
483 		_UpdateTabVisibility();
484 }
485 
486 
487 void
488 TabContainerView::_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 
518 float
519 TabContainerView::_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 
533 void
534 TabContainerView::_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