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	const menu_layout layout = MenuPrivate(fSuper).Layout();
473	if (layout == B_ITEMS_IN_COLUMN) {
474		if (IsMarked())
475			_DrawMarkSymbol();
476
477		if (fShortcutChar)
478			_DrawShortcutSymbol();
479
480		if (Submenu() != NULL)
481			_DrawSubmenuSymbol();
482	}
483
484	// restore the parent menu's low color and high color
485	fSuper->SetLowUIColor(lowColor);
486	fSuper->SetHighUIColor(highColor);
487}
488
489
490void
491BMenuItem::Highlight(bool highlight)
492{
493	fSuper->Invalidate(Frame());
494}
495
496
497bool
498BMenuItem::IsSelected() const
499{
500	return fSelected;
501}
502
503
504BPoint
505BMenuItem::ContentLocation() const
506{
507	const BRect& padding = MenuPrivate(fSuper).Padding();
508
509	return BPoint(fBounds.left + padding.left, fBounds.top + padding.top);
510}
511
512
513void BMenuItem::_ReservedMenuItem1() {}
514void BMenuItem::_ReservedMenuItem2() {}
515void BMenuItem::_ReservedMenuItem3() {}
516void BMenuItem::_ReservedMenuItem4() {}
517
518
519BMenuItem::BMenuItem(const BMenuItem &)
520{
521}
522
523
524BMenuItem&
525BMenuItem::operator=(const BMenuItem &)
526{
527	return *this;
528}
529
530
531void
532BMenuItem::_InitData()
533{
534	fLabel = NULL;
535	fSubmenu = NULL;
536	fWindow = NULL;
537	fSuper = NULL;
538	fModifiers = 0;
539	fCachedWidth = 0;
540	fTriggerIndex = -1;
541	fUserTrigger = 0;
542	fTrigger = 0;
543	fShortcutChar = 0;
544	fMark = false;
545	fEnabled = true;
546	fSelected = false;
547}
548
549
550void
551BMenuItem::_InitMenuData(BMenu* menu)
552{
553	fSubmenu = menu;
554
555	MenuPrivate(fSubmenu).SetSuperItem(this);
556
557	BMenuItem* item = menu->FindMarked();
558
559	if (menu->IsRadioMode() && menu->IsLabelFromMarked() && item != NULL)
560		SetLabel(item->Label());
561	else
562		SetLabel(menu->Name());
563}
564
565
566void
567BMenuItem::Install(BWindow* window)
568{
569	if (fSubmenu != NULL)
570		MenuPrivate(fSubmenu).Install(window);
571
572	fWindow = window;
573
574	if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) && fWindow)
575		window->AddShortcut(fShortcutChar, fModifiers, this);
576
577	if (!Messenger().IsValid())
578		SetTarget(window);
579}
580
581
582status_t
583BMenuItem::Invoke(BMessage* message)
584{
585	if (!IsEnabled())
586		return B_ERROR;
587
588	if (fSuper->IsRadioMode())
589		SetMarked(true);
590
591	bool notify = false;
592	uint32 kind = InvokeKind(&notify);
593
594	BMessage clone(kind);
595	status_t err = B_BAD_VALUE;
596
597	if (message == NULL && !notify)
598		message = Message();
599
600	if (message == NULL) {
601		if (!fSuper->IsWatched())
602			return err;
603	} else
604		clone = *message;
605
606	clone.AddInt32("index", fSuper->IndexOf(this));
607	clone.AddInt64("when", (int64)system_time());
608	clone.AddPointer("source", this);
609	clone.AddMessenger("be:sender", BMessenger(fSuper));
610
611	if (message != NULL)
612		err = BInvoker::Invoke(&clone);
613
614//	TODO: assynchronous messaging
615//	SendNotices(kind, &clone);
616
617	return err;
618}
619
620
621void
622BMenuItem::Uninstall()
623{
624	if (fSubmenu != NULL)
625		MenuPrivate(fSubmenu).Uninstall();
626
627	if (Target() == fWindow)
628		SetTarget(BMessenger());
629
630	if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) != 0
631		&& fWindow != NULL) {
632		fWindow->RemoveShortcut(fShortcutChar, fModifiers);
633	}
634
635	fWindow = NULL;
636}
637
638
639void
640BMenuItem::SetSuper(BMenu* super)
641{
642	if (fSuper != NULL && super != NULL) {
643		debugger("Error - can't add menu or menu item to more than 1 container"
644			" (either menu or menubar).");
645	}
646
647	if (fSubmenu != NULL)
648		MenuPrivate(fSubmenu).SetSuper(super);
649
650	fSuper = super;
651}
652
653
654void
655BMenuItem::Select(bool selected)
656{
657	if (fSelected == selected)
658		return;
659
660	if (Submenu() != NULL || IsEnabled()) {
661		fSelected = selected;
662		Highlight(selected);
663	}
664}
665
666
667bool
668BMenuItem::_IsActivated()
669{
670	return IsSelected() && (IsEnabled() || fSubmenu != NULL);
671}
672
673
674rgb_color
675BMenuItem::_LowColor()
676{
677	return _IsActivated() ? ui_color(B_MENU_SELECTED_BACKGROUND_COLOR)
678		: ui_color(B_MENU_BACKGROUND_COLOR);
679}
680
681
682rgb_color
683BMenuItem::_HighColor()
684{
685	rgb_color highColor;
686
687	bool isEnabled = IsEnabled();
688	bool isSelected = IsSelected();
689
690	if (isEnabled && isSelected)
691		highColor = ui_color(B_MENU_SELECTED_ITEM_TEXT_COLOR);
692	else if (isEnabled)
693		highColor = ui_color(B_MENU_ITEM_TEXT_COLOR);
694	else {
695		rgb_color bgColor = fSuper->LowColor();
696		if (bgColor.red + bgColor.green + bgColor.blue > 128 * 3)
697			highColor = tint_color(bgColor, B_DISABLED_LABEL_TINT);
698		else
699			highColor = tint_color(bgColor, B_LIGHTEN_2_TINT);
700	}
701
702	return highColor;
703}
704
705
706void
707BMenuItem::_DrawMarkSymbol()
708{
709	fSuper->PushState();
710
711	BRect r(fBounds);
712	float leftMargin;
713	MenuPrivate(fSuper).GetItemMargins(&leftMargin, NULL, NULL, NULL);
714	float gap = leftMargin / 4;
715	r.right = r.left + leftMargin - gap;
716	r.left += gap / 3;
717
718	BPoint center(floorf((r.left + r.right) / 2.0),
719		floorf((r.top + r.bottom) / 2.0));
720
721	float size = std::min(r.Height() - 2, r.Width());
722	r.top = floorf(center.y - size / 2 + 0.5);
723	r.bottom = floorf(center.y + size / 2 + 0.5);
724	r.left = floorf(center.x - size / 2 + 0.5);
725	r.right = floorf(center.x + size / 2 + 0.5);
726
727	BShape arrowShape;
728	center.x += 0.5;
729	center.y += 0.5;
730	size *= 0.3;
731	arrowShape.MoveTo(BPoint(center.x - size, center.y - size * 0.25));
732	arrowShape.LineTo(BPoint(center.x - size * 0.25, center.y + size));
733	arrowShape.LineTo(BPoint(center.x + size, center.y - size));
734
735	fSuper->SetHighColor(tint_color(_HighColor(), kMarkTint));
736	fSuper->SetDrawingMode(B_OP_OVER);
737	fSuper->SetPenSize(2.0);
738	// NOTE: StrokeShape() offsets the shape by the current pen position,
739	// it is not documented in the BeBook, but it is true!
740	fSuper->MovePenTo(B_ORIGIN);
741	fSuper->StrokeShape(&arrowShape);
742
743	fSuper->PopState();
744}
745
746
747void
748BMenuItem::_DrawShortcutSymbol()
749{
750	BMenu* menu = fSuper;
751	BFont font;
752	menu->GetFont(&font);
753	BPoint where = ContentLocation();
754	where.x = fBounds.right - font.Size();
755
756	if (fSubmenu != NULL)
757		where.x -= fBounds.Height() / 2;
758
759	const float ascent = MenuPrivate(fSuper).Ascent();
760	if (fShortcutChar < B_SPACE && kUTF8ControlMap[(int)fShortcutChar])
761		_DrawControlChar(fShortcutChar, where + BPoint(0, ascent));
762	else
763		fSuper->DrawChar(fShortcutChar, where + BPoint(0, ascent));
764
765	where.y += (fBounds.Height() - 11) / 2 - 1;
766	where.x -= 4;
767
768	// TODO: It would be nice to draw these taking into account the text (low)
769	// color.
770	if ((fModifiers & B_COMMAND_KEY) != 0) {
771		const BBitmap* command = MenuPrivate::MenuItemCommand();
772		const BRect &rect = command->Bounds();
773		where.x -= rect.Width() + 1;
774		fSuper->DrawBitmap(command, where);
775	}
776
777	if ((fModifiers & B_CONTROL_KEY) != 0) {
778		const BBitmap* control = MenuPrivate::MenuItemControl();
779		const BRect &rect = control->Bounds();
780		where.x -= rect.Width() + 1;
781		fSuper->DrawBitmap(control, where);
782	}
783
784	if ((fModifiers & B_OPTION_KEY) != 0) {
785		const BBitmap* option = MenuPrivate::MenuItemOption();
786		const BRect &rect = option->Bounds();
787		where.x -= rect.Width() + 1;
788		fSuper->DrawBitmap(option, where);
789	}
790
791	if ((fModifiers & B_SHIFT_KEY) != 0) {
792		const BBitmap* shift = MenuPrivate::MenuItemShift();
793		const BRect &rect = shift->Bounds();
794		where.x -= rect.Width() + 1;
795		fSuper->DrawBitmap(shift, where);
796	}
797}
798
799
800void
801BMenuItem::_DrawSubmenuSymbol()
802{
803	fSuper->PushState();
804
805	float symbolSize = roundf(Frame().Height() * 2 / 3);
806
807	BRect rect(fBounds);
808	rect.left = rect.right - symbolSize;
809
810	// 14px by default, scaled with font size up to right margin - padding
811	BRect symbolRect(0, 0, symbolSize, symbolSize);
812	symbolRect.OffsetTo(BPoint(rect.left,
813		fBounds.top + (fBounds.Height() - symbolSize) / 2));
814
815	be_control_look->DrawArrowShape(Menu(), symbolRect, symbolRect,
816		_HighColor(), BControlLook::B_RIGHT_ARROW, 0, kMarkTint);
817
818	fSuper->PopState();
819}
820
821
822void
823BMenuItem::_DrawControlChar(char shortcut, BPoint where)
824{
825	// TODO: If needed, take another font for the control characters
826	//	(or have font overlays in the app_server!)
827	const char* symbol = " ";
828	if (kUTF8ControlMap[(int)fShortcutChar])
829		symbol = kUTF8ControlMap[(int)fShortcutChar];
830
831	fSuper->DrawString(symbol, where);
832}
833
834
835void
836BMenuItem::SetAutomaticTrigger(int32 index, uint32 trigger)
837{
838	fTriggerIndex = index;
839	fTrigger = trigger;
840}
841