1/*
2 * Copyright 2001-2015 Haiku, Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Stefano Ceccherini, stefano.ceccherini@gmail.com
7 *		Marc Flerackers, mflerackers@androme.be
8 *		Bill Hayden, haydentech@users.sourceforge.net
9 *		Olivier Milla
10 *		John Scipione, jscipione@gmail.com
11 */
12
13
14#include <ctype.h>
15#include <stdlib.h>
16#include <string.h>
17
18#include <algorithm>
19
20#include <Bitmap.h>
21#include <ControlLook.h>
22#include <MenuItem.h>
23#include <Shape.h>
24#include <String.h>
25#include <Window.h>
26
27#include <MenuPrivate.h>
28
29#include "utf8_functions.h"
30
31
32static const float kMarkTint = 0.75f;
33
34// map control key shortcuts to drawable Unicode characters
35// cf. http://unicode.org/charts/PDF/U2190.pdf
36const char* kUTF8ControlMap[] = {
37	NULL,
38	"\xe2\x86\xb8", /* B_HOME U+21B8 */
39	NULL, NULL,
40	NULL, /* B_END */
41	NULL, /* B_INSERT */
42	NULL, NULL,
43	NULL, /* B_BACKSPACE */
44	"\xe2\x86\xb9", /* B_TAB U+21B9 */
45	"\xe2\x86\xb5", /* B_ENTER, U+21B5 */
46	//"\xe2\x8f\x8e", /* B_ENTER, U+23CE it's the official one */
47	NULL, /* B_PAGE_UP */
48	NULL, /* B_PAGE_DOWN */
49	NULL, NULL, NULL,
50	NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
51	NULL, NULL, NULL, NULL,
52	"\xe2\x86\x90", /* B_LEFT_ARROW */
53	"\xe2\x86\x92", /* B_RIGHT_ARROW */
54	"\xe2\x86\x91", /* B_UP_ARROW */
55	"\xe2\x86\x93", /* B_DOWN_ARROW */
56};
57
58
59using BPrivate::MenuPrivate;
60
61BMenuItem::BMenuItem(const char* label, BMessage* message, char shortcut,
62	uint32 modifiers)
63{
64	_InitData();
65	if (label != NULL)
66		fLabel = strdup(label);
67
68	SetMessage(message);
69
70	fShortcutChar = shortcut;
71
72	if (shortcut != 0)
73		fModifiers = modifiers | B_COMMAND_KEY;
74	else
75		fModifiers = 0;
76}
77
78
79BMenuItem::BMenuItem(BMenu* menu, BMessage* message)
80{
81	_InitData();
82	SetMessage(message);
83	_InitMenuData(menu);
84}
85
86
87BMenuItem::BMenuItem(BMessage* data)
88{
89	_InitData();
90
91	if (data->HasString("_label")) {
92		const char* string;
93
94		data->FindString("_label", &string);
95		SetLabel(string);
96	}
97
98	bool disable;
99	if (data->FindBool("_disable", &disable) == B_OK)
100		SetEnabled(!disable);
101
102	bool marked;
103	if (data->FindBool("_marked", &marked) == B_OK)
104		SetMarked(marked);
105
106	int32 userTrigger;
107	if (data->FindInt32("_user_trig", &userTrigger) == B_OK)
108		SetTrigger(userTrigger);
109
110	if (data->HasInt32("_shortcut")) {
111		int32 shortcut, mods;
112
113		data->FindInt32("_shortcut", &shortcut);
114		data->FindInt32("_mods", &mods);
115
116		SetShortcut(shortcut, mods);
117	}
118
119	if (data->HasMessage("_msg")) {
120		BMessage* message = new BMessage;
121		data->FindMessage("_msg", message);
122		SetMessage(message);
123	}
124
125	BMessage subMessage;
126	if (data->FindMessage("_submenu", &subMessage) == B_OK) {
127		BArchivable* object = instantiate_object(&subMessage);
128		if (object != NULL) {
129			BMenu* menu = dynamic_cast<BMenu*>(object);
130			if (menu != NULL)
131				_InitMenuData(menu);
132		}
133	}
134}
135
136
137BArchivable*
138BMenuItem::Instantiate(BMessage* data)
139{
140	if (validate_instantiation(data, "BMenuItem"))
141		return new BMenuItem(data);
142
143	return NULL;
144}
145
146
147status_t
148BMenuItem::Archive(BMessage* data, bool deep) const
149{
150	status_t status = BArchivable::Archive(data, deep);
151
152	if (status == B_OK && fLabel)
153		status = data->AddString("_label", Label());
154
155	if (status == B_OK && !IsEnabled())
156		status = data->AddBool("_disable", true);
157
158	if (status == B_OK && IsMarked())
159		status = data->AddBool("_marked", true);
160
161	if (status == B_OK && fUserTrigger)
162		status = data->AddInt32("_user_trig", fUserTrigger);
163
164	if (status == B_OK && fShortcutChar) {
165		status = data->AddInt32("_shortcut", fShortcutChar);
166		if (status == B_OK)
167			status = data->AddInt32("_mods", fModifiers);
168	}
169
170	if (status == B_OK && Message() != NULL)
171		status = data->AddMessage("_msg", Message());
172
173	if (status == B_OK && deep && fSubmenu) {
174		BMessage submenu;
175		if (fSubmenu->Archive(&submenu, true) == B_OK)
176			status = data->AddMessage("_submenu", &submenu);
177	}
178
179	return status;
180}
181
182
183BMenuItem::~BMenuItem()
184{
185	if (fSuper != NULL)
186		fSuper->RemoveItem(this);
187
188	free(fLabel);
189	delete fSubmenu;
190}
191
192
193void
194BMenuItem::SetLabel(const char* string)
195{
196	if (fLabel != NULL) {
197		free(fLabel);
198		fLabel = NULL;
199	}
200
201	if (string != NULL)
202		fLabel = strdup(string);
203
204	if (fSuper != NULL) {
205		fSuper->InvalidateLayout();
206
207		if (fSuper->LockLooper()) {
208			fSuper->Invalidate();
209			fSuper->UnlockLooper();
210		}
211	}
212}
213
214
215void
216BMenuItem::SetEnabled(bool enable)
217{
218	if (fEnabled == enable)
219		return;
220
221	fEnabled = enable;
222
223	if (fSubmenu != NULL)
224		fSubmenu->SetEnabled(enable);
225
226	BMenu* menu = fSuper;
227	if (menu != NULL && menu->LockLooper()) {
228		menu->Invalidate(fBounds);
229		menu->UnlockLooper();
230	}
231}
232
233
234void
235BMenuItem::SetMarked(bool mark)
236{
237	fMark = mark;
238
239	if (mark && fSuper != NULL) {
240		MenuPrivate priv(fSuper);
241		priv.ItemMarked(this);
242	}
243}
244
245
246void
247BMenuItem::SetTrigger(char trigger)
248{
249	fUserTrigger = trigger;
250
251	// try uppercase letters first
252
253	const char* pos = strchr(Label(), toupper(trigger));
254	trigger = tolower(trigger);
255
256	if (pos == NULL) {
257		// take lowercase, too
258		pos = strchr(Label(), trigger);
259	}
260
261	if (pos != NULL) {
262		fTriggerIndex = UTF8CountChars(Label(), pos - Label());
263		fTrigger = trigger;
264	} else {
265		fTrigger = 0;
266		fTriggerIndex = -1;
267	}
268
269	if (fSuper != NULL)
270		fSuper->InvalidateLayout();
271}
272
273
274void
275BMenuItem::SetShortcut(char shortcut, uint32 modifiers)
276{
277	if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) != 0
278		&& fWindow != NULL) {
279		fWindow->RemoveShortcut(fShortcutChar, fModifiers);
280	}
281
282	fShortcutChar = shortcut;
283
284	if (shortcut != 0)
285		fModifiers = modifiers | B_COMMAND_KEY;
286	else
287		fModifiers = 0;
288
289	if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) && fWindow)
290		fWindow->AddShortcut(fShortcutChar, fModifiers, this);
291
292	if (fSuper != NULL) {
293		fSuper->InvalidateLayout();
294
295		if (fSuper->LockLooper()) {
296			fSuper->Invalidate();
297			fSuper->UnlockLooper();
298		}
299	}
300}
301
302
303const char*
304BMenuItem::Label() const
305{
306	return fLabel;
307}
308
309
310bool
311BMenuItem::IsEnabled() const
312{
313	if (fSubmenu)
314		return fSubmenu->IsEnabled();
315
316	if (!fEnabled)
317		return false;
318
319	return fSuper != NULL ? fSuper->IsEnabled() : true;
320}
321
322
323bool
324BMenuItem::IsMarked() const
325{
326	return fMark;
327}
328
329
330char
331BMenuItem::Trigger() const
332{
333	return fUserTrigger;
334}
335
336
337char
338BMenuItem::Shortcut(uint32* modifiers) const
339{
340	if (modifiers)
341		*modifiers = fModifiers;
342
343	return fShortcutChar;
344}
345
346
347BMenu*
348BMenuItem::Submenu() const
349{
350	return fSubmenu;
351}
352
353
354BMenu*
355BMenuItem::Menu() const
356{
357	return fSuper;
358}
359
360
361BRect
362BMenuItem::Frame() const
363{
364	return fBounds;
365}
366
367
368void
369BMenuItem::GetContentSize(float* _width, float* _height)
370{
371	// TODO: Get rid of this. BMenu should handle this
372	// automatically. Maybe it's not even needed, since our
373	// BFont::Height() caches the value locally
374	MenuPrivate(fSuper).CacheFontInfo();
375
376	fCachedWidth = fSuper->StringWidth(fLabel);
377
378	if (_width)
379		*_width = (float)ceil(fCachedWidth);
380	if (_height)
381		*_height = MenuPrivate(fSuper).FontHeight();
382}
383
384
385void
386BMenuItem::TruncateLabel(float maxWidth, char* newLabel)
387{
388	BFont font;
389	fSuper->GetFont(&font);
390
391	BString string(fLabel);
392
393	font.TruncateString(&string, B_TRUNCATE_MIDDLE, maxWidth);
394
395	string.CopyInto(newLabel, 0, string.Length());
396	newLabel[string.Length()] = '\0';
397}
398
399
400void
401BMenuItem::DrawContent()
402{
403	MenuPrivate menuPrivate(fSuper);
404	menuPrivate.CacheFontInfo();
405
406	fSuper->MovePenBy(0, menuPrivate.Ascent());
407	BPoint lineStart = fSuper->PenLocation();
408
409	fSuper->SetDrawingMode(B_OP_OVER);
410
411	float labelWidth;
412	float labelHeight;
413	GetContentSize(&labelWidth, &labelHeight);
414
415	const BRect& padding = menuPrivate.Padding();
416	float maxContentWidth = fSuper->MaxContentWidth();
417	float frameWidth = maxContentWidth > 0 ? maxContentWidth
418		: fSuper->Frame().Width() - padding.left - padding.right;
419
420	if (roundf(frameWidth) >= roundf(labelWidth))
421		fSuper->DrawString(fLabel);
422	else {
423		// truncate label to fit
424		char* truncatedLabel = new char[strlen(fLabel) + 4];
425		TruncateLabel(frameWidth, truncatedLabel);
426		fSuper->DrawString(truncatedLabel);
427		delete[] truncatedLabel;
428	}
429
430	if (fSuper->AreTriggersEnabled() && fTriggerIndex != -1) {
431		float escapements[fTriggerIndex + 1];
432		BFont font;
433		fSuper->GetFont(&font);
434
435		font.GetEscapements(fLabel, fTriggerIndex + 1, escapements);
436
437		for (int32 i = 0; i < fTriggerIndex; i++)
438			lineStart.x += escapements[i] * font.Size();
439
440		lineStart.x--;
441		lineStart.y++;
442
443		BPoint lineEnd(lineStart);
444		lineEnd.x += escapements[fTriggerIndex] * font.Size();
445
446		fSuper->StrokeLine(lineStart, lineEnd);
447	}
448}
449
450
451void
452BMenuItem::Draw()
453{
454	const color_which lowColor = fSuper->LowUIColor();
455	const color_which highColor = fSuper->HighUIColor();
456
457	fSuper->SetLowColor(_LowColor());
458	fSuper->SetHighColor(_HighColor());
459
460	if (_IsActivated()) {
461		// fill in the background
462		BRect frame(Frame());
463		be_control_look->DrawMenuItemBackground(fSuper, frame, frame,
464			fSuper->LowColor(), BControlLook::B_ACTIVATED);
465	}
466
467	// draw content
468	fSuper->MovePenTo(ContentLocation());
469	DrawContent();
470
471	// draw extra symbols
472	MenuPrivate privateAccessor(fSuper);
473	const menu_layout layout = privateAccessor.Layout();
474	if (layout == B_ITEMS_IN_COLUMN) {
475		if (IsMarked())
476			_DrawMarkSymbol();
477
478		if (fShortcutChar)
479			_DrawShortcutSymbol(privateAccessor.HasSubmenus());
480
481		if (Submenu() != NULL)
482			_DrawSubmenuSymbol();
483	}
484
485	// restore the parent menu's low color and high color
486	fSuper->SetLowUIColor(lowColor);
487	fSuper->SetHighUIColor(highColor);
488}
489
490
491void
492BMenuItem::Highlight(bool highlight)
493{
494	fSuper->Invalidate(Frame());
495}
496
497
498bool
499BMenuItem::IsSelected() const
500{
501	return fSelected;
502}
503
504
505BPoint
506BMenuItem::ContentLocation() const
507{
508	const BRect& padding = MenuPrivate(fSuper).Padding();
509
510	return BPoint(fBounds.left + padding.left, fBounds.top + padding.top);
511}
512
513
514void BMenuItem::_ReservedMenuItem1() {}
515void BMenuItem::_ReservedMenuItem2() {}
516void BMenuItem::_ReservedMenuItem3() {}
517void BMenuItem::_ReservedMenuItem4() {}
518
519
520BMenuItem::BMenuItem(const BMenuItem &)
521{
522}
523
524
525BMenuItem&
526BMenuItem::operator=(const BMenuItem &)
527{
528	return *this;
529}
530
531
532void
533BMenuItem::_InitData()
534{
535	fLabel = NULL;
536	fSubmenu = NULL;
537	fWindow = NULL;
538	fSuper = NULL;
539	fModifiers = 0;
540	fCachedWidth = 0;
541	fTriggerIndex = -1;
542	fUserTrigger = 0;
543	fTrigger = 0;
544	fShortcutChar = 0;
545	fMark = false;
546	fEnabled = true;
547	fSelected = false;
548}
549
550
551void
552BMenuItem::_InitMenuData(BMenu* menu)
553{
554	fSubmenu = menu;
555
556	MenuPrivate(fSubmenu).SetSuperItem(this);
557
558	BMenuItem* item = menu->FindMarked();
559
560	if (menu->IsRadioMode() && menu->IsLabelFromMarked() && item != NULL)
561		SetLabel(item->Label());
562	else
563		SetLabel(menu->Name());
564}
565
566
567void
568BMenuItem::Install(BWindow* window)
569{
570	if (fSubmenu != NULL)
571		MenuPrivate(fSubmenu).Install(window);
572
573	fWindow = window;
574
575	if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) && fWindow)
576		window->AddShortcut(fShortcutChar, fModifiers, this);
577
578	if (!Messenger().IsValid())
579		SetTarget(window);
580}
581
582
583status_t
584BMenuItem::Invoke(BMessage* message)
585{
586	if (!IsEnabled())
587		return B_ERROR;
588
589	if (fSuper->IsRadioMode())
590		SetMarked(true);
591
592	bool notify = false;
593	uint32 kind = InvokeKind(&notify);
594
595	BMessage clone(kind);
596	status_t err = B_BAD_VALUE;
597
598	if (message == NULL && !notify)
599		message = Message();
600
601	if (message == NULL) {
602		if (!fSuper->IsWatched())
603			return err;
604	} else
605		clone = *message;
606
607	clone.AddInt32("index", fSuper->IndexOf(this));
608	clone.AddInt64("when", (int64)system_time());
609	clone.AddPointer("source", this);
610	clone.AddMessenger("be:sender", BMessenger(fSuper));
611
612	if (message != NULL)
613		err = BInvoker::Invoke(&clone);
614
615//	TODO: assynchronous messaging
616//	SendNotices(kind, &clone);
617
618	return err;
619}
620
621
622void
623BMenuItem::Uninstall()
624{
625	if (fSubmenu != NULL)
626		MenuPrivate(fSubmenu).Uninstall();
627
628	if (Target() == fWindow)
629		SetTarget(BMessenger());
630
631	if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) != 0
632		&& fWindow != NULL) {
633		fWindow->RemoveShortcut(fShortcutChar, fModifiers);
634	}
635
636	fWindow = NULL;
637}
638
639
640void
641BMenuItem::SetSuper(BMenu* super)
642{
643	if (fSuper != NULL && super != NULL) {
644		debugger("Error - can't add menu or menu item to more than 1 container"
645			" (either menu or menubar).");
646	}
647
648	if (fSubmenu != NULL)
649		MenuPrivate(fSubmenu).SetSuper(super);
650
651	fSuper = super;
652}
653
654
655void
656BMenuItem::Select(bool selected)
657{
658	if (fSelected == selected)
659		return;
660
661	if (Submenu() != NULL || IsEnabled()) {
662		fSelected = selected;
663		Highlight(selected);
664	}
665}
666
667
668bool
669BMenuItem::_IsActivated()
670{
671	return IsSelected() && (IsEnabled() || fSubmenu != NULL);
672}
673
674
675rgb_color
676BMenuItem::_LowColor()
677{
678	return _IsActivated() ? ui_color(B_MENU_SELECTED_BACKGROUND_COLOR)
679		: ui_color(B_MENU_BACKGROUND_COLOR);
680}
681
682
683rgb_color
684BMenuItem::_HighColor()
685{
686	rgb_color highColor;
687
688	bool isEnabled = IsEnabled();
689	bool isSelected = IsSelected();
690
691	if (isEnabled && isSelected)
692		highColor = ui_color(B_MENU_SELECTED_ITEM_TEXT_COLOR);
693	else if (isEnabled)
694		highColor = ui_color(B_MENU_ITEM_TEXT_COLOR);
695	else {
696		rgb_color bgColor = fSuper->LowColor();
697		if (bgColor.red + bgColor.green + bgColor.blue > 128 * 3)
698			highColor = tint_color(bgColor, B_DISABLED_LABEL_TINT);
699		else
700			highColor = tint_color(bgColor, B_LIGHTEN_2_TINT);
701	}
702
703	return highColor;
704}
705
706
707void
708BMenuItem::_DrawMarkSymbol()
709{
710	fSuper->PushState();
711
712	BRect r(fBounds);
713	float leftMargin;
714	MenuPrivate(fSuper).GetItemMargins(&leftMargin, NULL, NULL, NULL);
715	float gap = leftMargin / 4;
716	r.right = r.left + leftMargin - gap;
717	r.left += gap / 3;
718
719	BPoint center(floorf((r.left + r.right) / 2.0),
720		floorf((r.top + r.bottom) / 2.0));
721
722	float size = std::min(r.Height() - 2, r.Width());
723	r.top = floorf(center.y - size / 2 + 0.5);
724	r.bottom = floorf(center.y + size / 2 + 0.5);
725	r.left = floorf(center.x - size / 2 + 0.5);
726	r.right = floorf(center.x + size / 2 + 0.5);
727
728	BShape arrowShape;
729	center.x += 0.5;
730	center.y += 0.5;
731	size *= 0.3;
732	arrowShape.MoveTo(BPoint(center.x - size, center.y - size * 0.25));
733	arrowShape.LineTo(BPoint(center.x - size * 0.25, center.y + size));
734	arrowShape.LineTo(BPoint(center.x + size, center.y - size));
735
736	fSuper->SetHighColor(tint_color(_HighColor(), kMarkTint));
737	fSuper->SetDrawingMode(B_OP_OVER);
738	fSuper->SetPenSize(2.0);
739	// NOTE: StrokeShape() offsets the shape by the current pen position,
740	// it is not documented in the BeBook, but it is true!
741	fSuper->MovePenTo(B_ORIGIN);
742	fSuper->StrokeShape(&arrowShape);
743
744	fSuper->PopState();
745}
746
747
748void
749BMenuItem::_DrawShortcutSymbol(bool submenus)
750{
751	BMenu* menu = fSuper;
752	BFont font;
753	menu->GetFont(&font);
754	BPoint where = ContentLocation();
755	// Start from the right and walk our way back
756	where.x = fBounds.right - font.Size();
757
758	// Leave space for the submenu arrow if any item in the menu has a submenu
759	if (submenus)
760		where.x -= fBounds.Height() / 2;
761
762	const float ascent = MenuPrivate(fSuper).Ascent();
763	if (fShortcutChar < B_SPACE && kUTF8ControlMap[(int)fShortcutChar])
764		_DrawControlChar(fShortcutChar, where + BPoint(0, ascent));
765	else
766		fSuper->DrawChar(fShortcutChar, where + BPoint(0, ascent));
767
768	where.y += (fBounds.Height() - 11) / 2 - 1;
769	where.x -= 4;
770
771	// TODO: It would be nice to draw these taking into account the text (low)
772	// color.
773	if ((fModifiers & B_COMMAND_KEY) != 0) {
774		const BBitmap* command = MenuPrivate::MenuItemCommand();
775		const BRect &rect = command->Bounds();
776		where.x -= rect.Width() + 1;
777		fSuper->DrawBitmap(command, where);
778	}
779
780	if ((fModifiers & B_CONTROL_KEY) != 0) {
781		const BBitmap* control = MenuPrivate::MenuItemControl();
782		const BRect &rect = control->Bounds();
783		where.x -= rect.Width() + 1;
784		fSuper->DrawBitmap(control, where);
785	}
786
787	if ((fModifiers & B_OPTION_KEY) != 0) {
788		const BBitmap* option = MenuPrivate::MenuItemOption();
789		const BRect &rect = option->Bounds();
790		where.x -= rect.Width() + 1;
791		fSuper->DrawBitmap(option, where);
792	}
793
794	if ((fModifiers & B_SHIFT_KEY) != 0) {
795		const BBitmap* shift = MenuPrivate::MenuItemShift();
796		const BRect &rect = shift->Bounds();
797		where.x -= rect.Width() + 1;
798		fSuper->DrawBitmap(shift, where);
799	}
800}
801
802
803void
804BMenuItem::_DrawSubmenuSymbol()
805{
806	fSuper->PushState();
807
808	float symbolSize = roundf(Frame().Height() * 2 / 3);
809
810	BRect rect(fBounds);
811	rect.left = rect.right - symbolSize;
812
813	// 14px by default, scaled with font size up to right margin - padding
814	BRect symbolRect(0, 0, symbolSize, symbolSize);
815	symbolRect.OffsetTo(BPoint(rect.left,
816		fBounds.top + (fBounds.Height() - symbolSize) / 2));
817
818	be_control_look->DrawArrowShape(Menu(), symbolRect, symbolRect,
819		_HighColor(), BControlLook::B_RIGHT_ARROW, 0, kMarkTint);
820
821	fSuper->PopState();
822}
823
824
825void
826BMenuItem::_DrawControlChar(char shortcut, BPoint where)
827{
828	// TODO: If needed, take another font for the control characters
829	//	(or have font overlays in the app_server!)
830	const char* symbol = " ";
831	if (kUTF8ControlMap[(int)fShortcutChar])
832		symbol = kUTF8ControlMap[(int)fShortcutChar];
833
834	fSuper->DrawString(symbol, where);
835}
836
837
838void
839BMenuItem::SetAutomaticTrigger(int32 index, uint32 trigger)
840{
841	fTriggerIndex = index;
842	fTrigger = trigger;
843}
844