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