1/*
2 * Copyright 2001-2015, Haiku, Inc.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Marc Flerackers (mflerackers@androme.be)
7 *		Stefano Ceccherini (stefano.ceccherini@gmail.com)
8 */
9
10//!	BMenuWindow is a custom BWindow for BMenus.
11
12#include <MenuWindow.h>
13
14#include <ControlLook.h>
15#include <Debug.h>
16#include <Menu.h>
17#include <MenuItem.h>
18
19#include <MenuPrivate.h>
20#include <WindowPrivate.h>
21
22
23namespace BPrivate {
24
25class BMenuScroller : public BView {
26public:
27							BMenuScroller(BRect frame);
28
29			bool			IsEnabled() const;
30			void			SetEnabled(bool enabled);
31
32private:
33			bool			fEnabled;
34};
35
36
37class BMenuFrame : public BView {
38public:
39							BMenuFrame(BMenu* menu);
40
41	virtual	void			AttachedToWindow();
42	virtual	void			DetachedFromWindow();
43	virtual	void			Draw(BRect updateRect);
44
45private:
46	friend class BMenuWindow;
47
48			BMenu*			fMenu;
49};
50
51
52class UpperScroller : public BMenuScroller {
53public:
54							UpperScroller(BRect frame);
55
56	virtual	void			Draw(BRect updateRect);
57};
58
59
60class LowerScroller : public BMenuScroller {
61public:
62							LowerScroller(BRect frame);
63
64	virtual	void			Draw(BRect updateRect);
65};
66
67
68}	// namespace BPrivate
69
70
71using namespace BPrivate;
72
73
74const int kScrollerHeight = 12;
75
76
77BMenuScroller::BMenuScroller(BRect frame)
78	:
79	BView(frame, "menu scroller", 0, B_WILL_DRAW | B_FRAME_EVENTS
80		| B_FULL_UPDATE_ON_RESIZE),
81	fEnabled(false)
82{
83	SetViewUIColor(B_MENU_BACKGROUND_COLOR);
84}
85
86
87bool
88BMenuScroller::IsEnabled() const
89{
90	return fEnabled;
91}
92
93
94void
95BMenuScroller::SetEnabled(bool enabled)
96{
97	fEnabled = enabled;
98}
99
100
101//	#pragma mark -
102
103
104UpperScroller::UpperScroller(BRect frame)
105	:
106	BMenuScroller(frame)
107{
108}
109
110
111void
112UpperScroller::Draw(BRect updateRect)
113{
114	SetLowColor(tint_color(ui_color(B_MENU_BACKGROUND_COLOR), B_DARKEN_1_TINT));
115	float middle = Bounds().right / 2;
116
117	// Draw the upper arrow.
118	if (IsEnabled())
119		SetHighColor(0, 0, 0);
120	else {
121		SetHighColor(tint_color(ui_color(B_MENU_BACKGROUND_COLOR),
122			B_DARKEN_2_TINT));
123	}
124
125	FillRect(Bounds(), B_SOLID_LOW);
126
127	FillTriangle(BPoint(middle, (kScrollerHeight / 2) - 3),
128		BPoint(middle + 5, (kScrollerHeight / 2) + 2),
129		BPoint(middle - 5, (kScrollerHeight / 2) + 2));
130}
131
132
133//	#pragma mark -
134
135
136LowerScroller::LowerScroller(BRect frame)
137	:
138	BMenuScroller(frame)
139{
140}
141
142
143void
144LowerScroller::Draw(BRect updateRect)
145{
146	SetLowColor(tint_color(ui_color(B_MENU_BACKGROUND_COLOR), B_DARKEN_1_TINT));
147
148	BRect frame = Bounds();
149	// Draw the lower arrow.
150	if (IsEnabled())
151		SetHighColor(0, 0, 0);
152	else {
153		SetHighColor(tint_color(ui_color(B_MENU_BACKGROUND_COLOR),
154			B_DARKEN_2_TINT));
155	}
156
157	FillRect(frame, B_SOLID_LOW);
158
159	float middle = Bounds().right / 2;
160
161	FillTriangle(BPoint(middle, frame.bottom - (kScrollerHeight / 2) + 3),
162		BPoint(middle + 5, frame.bottom - (kScrollerHeight / 2) - 2),
163		BPoint(middle - 5, frame.bottom - (kScrollerHeight / 2) - 2));
164}
165
166
167//	#pragma mark -
168
169
170BMenuFrame::BMenuFrame(BMenu *menu)
171	:
172	BView(BRect(0, 0, 1, 1), "menu frame", B_FOLLOW_ALL_SIDES, B_WILL_DRAW),
173	fMenu(menu)
174{
175}
176
177
178void
179BMenuFrame::AttachedToWindow()
180{
181	BView::AttachedToWindow();
182
183	if (fMenu != NULL)
184		AddChild(fMenu);
185
186	ResizeTo(Window()->Bounds().Width(), Window()->Bounds().Height());
187	if (fMenu != NULL) {
188		BFont font;
189		fMenu->GetFont(&font);
190		SetFont(&font);
191	}
192}
193
194
195void
196BMenuFrame::DetachedFromWindow()
197{
198	if (fMenu != NULL)
199		RemoveChild(fMenu);
200}
201
202
203void
204BMenuFrame::Draw(BRect updateRect)
205{
206	if (fMenu != NULL && fMenu->CountItems() == 0) {
207		BRect rect(Bounds());
208		be_control_look->DrawMenuBackground(this, rect, updateRect,
209			ui_color(B_MENU_BACKGROUND_COLOR));
210		SetDrawingMode(B_OP_OVER);
211
212		// TODO: Review this as it's a bit hacky.
213		// Since there are no items in this menu, its size is 0x0.
214		// To show an empty BMenu, we use BMenuFrame to draw an empty item.
215		// It would be nice to simply add a real "empty" item, but in that case
216		// we couldn't tell if the item was added by us or not, and applications
217		// could break (because CountItems() would return 1 for an empty BMenu).
218		// See also BMenu::UpdateWindowViewSize()
219		font_height height;
220		GetFontHeight(&height);
221		SetHighColor(tint_color(ui_color(B_MENU_BACKGROUND_COLOR),
222			B_DISABLED_LABEL_TINT));
223		BPoint where(
224			(Bounds().Width() - fMenu->StringWidth(kEmptyMenuLabel)) / 2,
225			ceilf(height.ascent + 1));
226		DrawString(kEmptyMenuLabel, where);
227	}
228}
229
230
231
232//	#pragma mark -
233
234
235BMenuWindow::BMenuWindow(const char *name)
236	// The window will be resized by BMenu, so just pass a dummy rect
237	:
238	BWindow(BRect(0, 0, 0, 0), name, B_BORDERED_WINDOW_LOOK, kMenuWindowFeel,
239		B_NOT_MOVABLE | B_NOT_ZOOMABLE | B_NOT_RESIZABLE | B_AVOID_FOCUS
240			| kAcceptKeyboardFocusFlag),
241	fMenu(NULL),
242	fMenuFrame(NULL),
243	fUpperScroller(NULL),
244	fLowerScroller(NULL),
245	fScrollStep(19)
246{
247	SetSizeLimits(2, 10000, 2, 10000);
248}
249
250
251BMenuWindow::~BMenuWindow()
252{
253	DetachMenu();
254}
255
256
257void
258BMenuWindow::DispatchMessage(BMessage *message, BHandler *handler)
259{
260	BWindow::DispatchMessage(message, handler);
261}
262
263
264void
265BMenuWindow::AttachMenu(BMenu *menu)
266{
267	if (fMenuFrame)
268		debugger("BMenuWindow: a menu is already attached!");
269	if (menu != NULL) {
270		fMenuFrame = new BMenuFrame(menu);
271		AddChild(fMenuFrame);
272		menu->MakeFocus(true);
273		fMenu = menu;
274	}
275}
276
277
278void
279BMenuWindow::DetachMenu()
280{
281	DetachScrollers();
282	if (fMenuFrame) {
283		RemoveChild(fMenuFrame);
284		delete fMenuFrame;
285		fMenuFrame = NULL;
286		fMenu = NULL;
287	}
288}
289
290
291void
292BMenuWindow::AttachScrollers()
293{
294	// We want to attach a scroller only if there's a
295	// menu frame already existing.
296	if (!fMenu || !fMenuFrame)
297		return;
298
299	fMenu->MakeFocus(true);
300
301	BRect frame = Bounds();
302	float newLimit = fMenu->Bounds().Height()
303		- (frame.Height() - 2 * kScrollerHeight);
304
305	if (!HasScrollers())
306		fValue = 0;
307	else if (fValue > newLimit)
308		_ScrollBy(newLimit - fValue);
309
310	fLimit = newLimit;
311
312	if (fUpperScroller == NULL) {
313		fUpperScroller = new UpperScroller(
314			BRect(0, 0, frame.right, kScrollerHeight - 1));
315		AddChild(fUpperScroller);
316	}
317
318	if (fLowerScroller == NULL) {
319		fLowerScroller = new LowerScroller(
320			BRect(0, frame.bottom - kScrollerHeight + 1, frame.right,
321				frame.bottom));
322		AddChild(fLowerScroller);
323	}
324
325	fUpperScroller->ResizeTo(frame.right, kScrollerHeight - 1);
326	fLowerScroller->ResizeTo(frame.right, kScrollerHeight - 1);
327
328	fUpperScroller->SetEnabled(fValue > 0);
329	fLowerScroller->SetEnabled(fValue < fLimit);
330
331	fMenuFrame->ResizeTo(frame.Width(), frame.Height() - 2 * kScrollerHeight);
332	fMenuFrame->MoveTo(0, kScrollerHeight);
333}
334
335
336void
337BMenuWindow::DetachScrollers()
338{
339	// BeOS doesn't remember the position where the last scrolling ended,
340	// so we just scroll back to the beginning.
341	if (fMenu)
342		fMenu->ScrollTo(0, 0);
343
344	if (fLowerScroller) {
345		RemoveChild(fLowerScroller);
346		delete fLowerScroller;
347		fLowerScroller = NULL;
348	}
349
350	if (fUpperScroller) {
351		RemoveChild(fUpperScroller);
352		delete fUpperScroller;
353		fUpperScroller = NULL;
354	}
355
356	BRect frame = Bounds();
357
358	if (fMenuFrame != NULL) {
359		fMenuFrame->ResizeTo(frame.Width(), frame.Height());
360		fMenuFrame->MoveTo(0, 0);
361	}
362}
363
364
365void
366BMenuWindow::SetSmallStep(float step)
367{
368	fScrollStep = step;
369}
370
371
372void
373BMenuWindow::GetSteps(float* _smallStep, float* _largeStep) const
374{
375	if (_smallStep != NULL)
376		*_smallStep = fScrollStep;
377	if (_largeStep != NULL) {
378		if (fMenuFrame != NULL)
379			*_largeStep = fMenuFrame->Bounds().Height() - fScrollStep;
380		else
381			*_largeStep = fScrollStep * 2;
382	}
383}
384
385
386bool
387BMenuWindow::HasScrollers() const
388{
389	return fMenuFrame != NULL && fUpperScroller != NULL
390		&& fLowerScroller != NULL;
391}
392
393
394bool
395BMenuWindow::CheckForScrolling(const BPoint &cursor)
396{
397	if (!fMenuFrame || !fUpperScroller || !fLowerScroller)
398		return false;
399
400	return _Scroll(cursor);
401}
402
403
404bool
405BMenuWindow::TryScrollBy(const float& step)
406{
407	if (!fMenuFrame || !fUpperScroller || !fLowerScroller)
408		return false;
409
410	_ScrollBy(step);
411	return true;
412}
413
414
415bool
416BMenuWindow::TryScrollTo(const float& where)
417{
418	if (!fMenuFrame || !fUpperScroller || !fLowerScroller)
419		return false;
420
421	_ScrollBy(where - fValue);
422	return true;
423}
424
425
426bool
427BMenuWindow::_Scroll(const BPoint& where)
428{
429	ASSERT((fLowerScroller != NULL));
430	ASSERT((fUpperScroller != NULL));
431
432	const BPoint cursor = ConvertFromScreen(where);
433	const BRect &lowerFrame = fLowerScroller->Frame();
434	const BRect &upperFrame = fUpperScroller->Frame();
435
436	int32 delta = 0;
437	if (fLowerScroller->IsEnabled() && lowerFrame.Contains(cursor))
438		delta = 1;
439	else if (fUpperScroller->IsEnabled() && upperFrame.Contains(cursor))
440		delta = -1;
441
442	if (delta == 0)
443		return false;
444
445	float smallStep;
446	GetSteps(&smallStep, NULL);
447	_ScrollBy(smallStep * delta);
448
449	snooze(5000);
450
451	return true;
452}
453
454
455void
456BMenuWindow::_ScrollBy(const float& step)
457{
458	if (step > 0) {
459		if (fValue == 0) {
460			fUpperScroller->SetEnabled(true);
461			fUpperScroller->Invalidate();
462		}
463
464		if (fValue + step >= fLimit) {
465			// If we reached the limit, only scroll to the end
466			fMenu->ScrollBy(0, fLimit - fValue);
467			fValue = fLimit;
468			fLowerScroller->SetEnabled(false);
469			fLowerScroller->Invalidate();
470		} else {
471			fMenu->ScrollBy(0, step);
472			fValue += step;
473		}
474	} else if (step < 0) {
475		if (fValue == fLimit) {
476			fLowerScroller->SetEnabled(true);
477			fLowerScroller->Invalidate();
478		}
479
480		if (fValue + step <= 0) {
481			fMenu->ScrollBy(0, -fValue);
482			fValue = 0;
483			fUpperScroller->SetEnabled(false);
484			fUpperScroller->Invalidate();
485		} else {
486			fMenu->ScrollBy(0, step);
487			fValue += step;
488		}
489	}
490}
491
492