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