* Copyright 2001-2015 Haiku, Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Stefano Ceccherini, stefano.ceccherini@gmail.com
* Marc Flerackers, mflerackers@androme.be
* Bill Hayden, haydentech@users.sourceforge.net
* Olivier Milla
* John Scipione, jscipione@gmail.com
*/
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <Bitmap.h>
#include <ControlLook.h>
#include <MenuItem.h>
#include <Shape.h>
#include <String.h>
#include <Window.h>
#include <MenuPrivate.h>
#include "utf8_functions.h"
static const float kMarkTint = 0.75f;
const char* kUTF8ControlMap[] = {
NULL,
"\xe2\x86\xb8",
NULL, NULL,
NULL,
NULL,
NULL, NULL,
NULL,
"\xe2\x86\xb9",
"\xe2\x8f\x8e",
NULL,
NULL,
NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
"\xe2\x86\x90",
"\xe2\x86\x92",
"\xe2\x86\x91",
"\xe2\x86\x93",
};
using BPrivate::MenuPrivate;
BMenuItem::BMenuItem(const char* label, BMessage* message, char shortcut,
uint32 modifiers)
{
_InitData();
if (label != NULL)
fLabel = strdup(label);
SetMessage(message);
fShortcutChar = shortcut;
if (shortcut != 0)
fModifiers = modifiers | B_COMMAND_KEY;
else
fModifiers = 0;
}
BMenuItem::BMenuItem(BMenu* menu, BMessage* message)
{
_InitData();
SetMessage(message);
_InitMenuData(menu);
}
BMenuItem::BMenuItem(BMessage* data)
{
_InitData();
if (data->HasString("_label")) {
const char* string;
data->FindString("_label", &string);
SetLabel(string);
}
bool disable;
if (data->FindBool("_disable", &disable) == B_OK)
SetEnabled(!disable);
bool marked;
if (data->FindBool("_marked", &marked) == B_OK)
SetMarked(marked);
int32 userTrigger;
if (data->FindInt32("_user_trig", &userTrigger) == B_OK)
SetTrigger(userTrigger);
if (data->HasInt32("_shortcut")) {
int32 shortcut, mods;
data->FindInt32("_shortcut", &shortcut);
data->FindInt32("_mods", &mods);
SetShortcut(shortcut, mods);
}
if (data->HasMessage("_msg")) {
BMessage* message = new BMessage;
data->FindMessage("_msg", message);
SetMessage(message);
}
BMessage subMessage;
if (data->FindMessage("_submenu", &subMessage) == B_OK) {
BArchivable* object = instantiate_object(&subMessage);
if (object != NULL) {
BMenu* menu = dynamic_cast<BMenu*>(object);
if (menu != NULL)
_InitMenuData(menu);
}
}
}
BArchivable*
BMenuItem::Instantiate(BMessage* data)
{
if (validate_instantiation(data, "BMenuItem"))
return new BMenuItem(data);
return NULL;
}
status_t
BMenuItem::Archive(BMessage* data, bool deep) const
{
status_t status = BArchivable::Archive(data, deep);
if (status == B_OK && fLabel)
status = data->AddString("_label", Label());
if (status == B_OK && !IsEnabled())
status = data->AddBool("_disable", true);
if (status == B_OK && IsMarked())
status = data->AddBool("_marked", true);
if (status == B_OK && fUserTrigger)
status = data->AddInt32("_user_trig", fUserTrigger);
if (status == B_OK && fShortcutChar) {
status = data->AddInt32("_shortcut", fShortcutChar);
if (status == B_OK)
status = data->AddInt32("_mods", fModifiers);
}
if (status == B_OK && Message() != NULL)
status = data->AddMessage("_msg", Message());
if (status == B_OK && deep && fSubmenu) {
BMessage submenu;
if (fSubmenu->Archive(&submenu, true) == B_OK)
status = data->AddMessage("_submenu", &submenu);
}
return status;
}
BMenuItem::~BMenuItem()
{
if (fSuper != NULL)
fSuper->RemoveItem(this);
free(fLabel);
delete fSubmenu;
}
void
BMenuItem::SetLabel(const char* string)
{
if (fLabel != NULL) {
free(fLabel);
fLabel = NULL;
}
if (string != NULL)
fLabel = strdup(string);
if (fSuper != NULL) {
fSuper->InvalidateLayout();
if (fSuper->LockLooper()) {
fSuper->Invalidate();
fSuper->UnlockLooper();
}
}
}
void
BMenuItem::SetEnabled(bool enable)
{
if (fEnabled == enable)
return;
fEnabled = enable;
if (fSubmenu != NULL)
fSubmenu->SetEnabled(enable);
BMenu* menu = fSuper;
if (menu != NULL && menu->LockLooper()) {
menu->Invalidate(fBounds);
menu->UnlockLooper();
}
}
void
BMenuItem::SetMarked(bool mark)
{
fMark = mark;
if (mark && fSuper != NULL) {
MenuPrivate priv(fSuper);
priv.ItemMarked(this);
}
}
void
BMenuItem::SetTrigger(char trigger)
{
fUserTrigger = trigger;
const char* pos = strchr(Label(), toupper(trigger));
trigger = tolower(trigger);
if (pos == NULL) {
pos = strchr(Label(), trigger);
}
if (pos != NULL) {
fTriggerIndex = UTF8CountChars(Label(), pos - Label());
fTrigger = trigger;
} else {
fTrigger = 0;
fTriggerIndex = -1;
}
if (fSuper != NULL)
fSuper->InvalidateLayout();
}
void
BMenuItem::SetShortcut(char shortcut, uint32 modifiers)
{
if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) != 0
&& fWindow != NULL) {
fWindow->RemoveShortcut(fShortcutChar, fModifiers);
}
fShortcutChar = shortcut;
if (shortcut != 0)
fModifiers = modifiers | B_COMMAND_KEY;
else
fModifiers = 0;
if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) && fWindow)
fWindow->AddShortcut(fShortcutChar, fModifiers, this);
if (fSuper != NULL) {
fSuper->InvalidateLayout();
if (fSuper->LockLooper()) {
fSuper->Invalidate();
fSuper->UnlockLooper();
}
}
}
const char*
BMenuItem::Label() const
{
return fLabel;
}
bool
BMenuItem::IsEnabled() const
{
if (fSubmenu)
return fSubmenu->IsEnabled();
if (!fEnabled)
return false;
return fSuper != NULL ? fSuper->IsEnabled() : true;
}
bool
BMenuItem::IsMarked() const
{
return fMark;
}
char
BMenuItem::Trigger() const
{
return fUserTrigger;
}
char
BMenuItem::Shortcut(uint32* modifiers) const
{
if (modifiers)
*modifiers = fModifiers;
return fShortcutChar;
}
BMenu*
BMenuItem::Submenu() const
{
return fSubmenu;
}
BMenu*
BMenuItem::Menu() const
{
return fSuper;
}
BRect
BMenuItem::Frame() const
{
return fBounds;
}
void
BMenuItem::GetContentSize(float* _width, float* _height)
{
MenuPrivate(fSuper).CacheFontInfo();
fCachedWidth = fSuper->StringWidth(fLabel);
if (_width)
*_width = (float)ceil(fCachedWidth);
if (_height)
*_height = MenuPrivate(fSuper).FontHeight();
}
void
BMenuItem::TruncateLabel(float maxWidth, char* newLabel)
{
BFont font;
fSuper->GetFont(&font);
BString string(fLabel);
font.TruncateString(&string, B_TRUNCATE_MIDDLE, maxWidth);
string.CopyInto(newLabel, 0, string.Length());
newLabel[string.Length()] = '\0';
}
void
BMenuItem::DrawContent()
{
MenuPrivate menuPrivate(fSuper);
menuPrivate.CacheFontInfo();
fSuper->MovePenBy(0, menuPrivate.Ascent());
BPoint lineStart = fSuper->PenLocation();
fSuper->SetDrawingMode(B_OP_OVER);
float labelWidth;
float labelHeight;
GetContentSize(&labelWidth, &labelHeight);
const BRect& padding = menuPrivate.Padding();
float maxContentWidth = fSuper->MaxContentWidth();
float frameWidth = maxContentWidth > 0 ? maxContentWidth
: fSuper->Frame().Width() - padding.left - padding.right;
if (roundf(frameWidth) >= roundf(labelWidth))
fSuper->DrawString(fLabel);
else {
char* truncatedLabel = new char[strlen(fLabel) + 4];
TruncateLabel(frameWidth, truncatedLabel);
fSuper->DrawString(truncatedLabel);
delete[] truncatedLabel;
}
if (fSuper->AreTriggersEnabled() && fTriggerIndex != -1) {
float escapements[fTriggerIndex + 1];
BFont font;
fSuper->GetFont(&font);
font.GetEscapements(fLabel, fTriggerIndex + 1, escapements);
for (int32 i = 0; i < fTriggerIndex; i++)
lineStart.x += escapements[i] * font.Size();
lineStart.x--;
lineStart.y++;
BPoint lineEnd(lineStart);
lineEnd.x += escapements[fTriggerIndex] * font.Size();
fSuper->StrokeLine(lineStart, lineEnd);
}
}
void
BMenuItem::Draw()
{
const color_which lowColor = fSuper->LowUIColor();
const color_which highColor = fSuper->HighUIColor();
fSuper->SetLowColor(_LowColor());
fSuper->SetHighColor(_HighColor());
if (_IsActivated()) {
BRect frame(Frame());
be_control_look->DrawMenuItemBackground(fSuper, frame, frame,
fSuper->LowColor(), BControlLook::B_ACTIVATED);
}
fSuper->MovePenTo(ContentLocation());
DrawContent();
MenuPrivate privateAccessor(fSuper);
const menu_layout layout = privateAccessor.Layout();
if (layout == B_ITEMS_IN_COLUMN) {
if (IsMarked())
_DrawMarkSymbol();
if (fShortcutChar)
_DrawShortcutSymbol(privateAccessor.HasSubmenus());
if (Submenu() != NULL)
_DrawSubmenuSymbol();
}
fSuper->SetLowUIColor(lowColor);
fSuper->SetHighUIColor(highColor);
}
void
BMenuItem::Highlight(bool highlight)
{
fSuper->Invalidate(Frame());
}
bool
BMenuItem::IsSelected() const
{
return fSelected;
}
BPoint
BMenuItem::ContentLocation() const
{
const BRect& padding = MenuPrivate(fSuper).Padding();
return BPoint(fBounds.left + padding.left, fBounds.top + padding.top);
}
void BMenuItem::_ReservedMenuItem1() {}
void BMenuItem::_ReservedMenuItem2() {}
void BMenuItem::_ReservedMenuItem3() {}
void BMenuItem::_ReservedMenuItem4() {}
BMenuItem::BMenuItem(const BMenuItem &)
{
}
BMenuItem&
BMenuItem::operator=(const BMenuItem &)
{
return *this;
}
void
BMenuItem::_InitData()
{
fLabel = NULL;
fSubmenu = NULL;
fWindow = NULL;
fSuper = NULL;
fModifiers = 0;
fCachedWidth = 0;
fTriggerIndex = -1;
fUserTrigger = 0;
fTrigger = 0;
fShortcutChar = 0;
fMark = false;
fEnabled = true;
fSelected = false;
}
void
BMenuItem::_InitMenuData(BMenu* menu)
{
fSubmenu = menu;
MenuPrivate(fSubmenu).SetSuperItem(this);
BMenuItem* item = menu->FindMarked();
if (menu->IsRadioMode() && menu->IsLabelFromMarked() && item != NULL)
SetLabel(item->Label());
else
SetLabel(menu->Name());
}
void
BMenuItem::Install(BWindow* window)
{
if (fSubmenu != NULL)
MenuPrivate(fSubmenu).Install(window);
fWindow = window;
if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) && fWindow)
window->AddShortcut(fShortcutChar, fModifiers, this);
if (!Messenger().IsValid())
SetTarget(window);
}
status_t
BMenuItem::Invoke(BMessage* message)
{
if (!IsEnabled())
return B_ERROR;
if (fSuper->IsRadioMode())
SetMarked(true);
bool notify = false;
uint32 kind = InvokeKind(¬ify);
BMessage clone(kind);
status_t err = B_BAD_VALUE;
if (message == NULL && !notify)
message = Message();
if (message == NULL) {
if (!fSuper->IsWatched())
return err;
} else
clone = *message;
clone.AddInt32("index", fSuper->IndexOf(this));
clone.AddInt64("when", (int64)system_time());
clone.AddPointer("source", this);
clone.AddMessenger("be:sender", BMessenger(fSuper));
if (message != NULL)
err = BInvoker::Invoke(&clone);
return err;
}
void
BMenuItem::Uninstall()
{
if (fSubmenu != NULL)
MenuPrivate(fSubmenu).Uninstall();
if (Target() == fWindow)
SetTarget(BMessenger());
if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) != 0
&& fWindow != NULL) {
fWindow->RemoveShortcut(fShortcutChar, fModifiers);
}
fWindow = NULL;
}
void
BMenuItem::SetSuper(BMenu* super)
{
if (fSuper != NULL && super != NULL) {
debugger("Error - can't add menu or menu item to more than 1 container"
" (either menu or menubar).");
}
if (fSubmenu != NULL)
MenuPrivate(fSubmenu).SetSuper(super);
fSuper = super;
}
void
BMenuItem::Select(bool selected)
{
if (fSelected == selected)
return;
if (Submenu() != NULL || IsEnabled()) {
fSelected = selected;
Highlight(selected);
}
}
bool
BMenuItem::_IsActivated()
{
return IsSelected() && (IsEnabled() || fSubmenu != NULL);
}
rgb_color
BMenuItem::_LowColor()
{
return _IsActivated() ? ui_color(B_MENU_SELECTED_BACKGROUND_COLOR)
: ui_color(B_MENU_BACKGROUND_COLOR);
}
rgb_color
BMenuItem::_HighColor()
{
rgb_color highColor;
bool isEnabled = IsEnabled();
bool isSelected = IsSelected();
if (isEnabled && isSelected)
highColor = ui_color(B_MENU_SELECTED_ITEM_TEXT_COLOR);
else if (isEnabled)
highColor = ui_color(B_MENU_ITEM_TEXT_COLOR);
else {
rgb_color bgColor = fSuper->LowColor();
if (bgColor.red + bgColor.green + bgColor.blue > 128 * 3)
highColor = tint_color(bgColor, B_DISABLED_LABEL_TINT);
else
highColor = tint_color(bgColor, B_LIGHTEN_2_TINT);
}
return highColor;
}
void
BMenuItem::_DrawMarkSymbol()
{
fSuper->PushState();
BRect r(fBounds);
float leftMargin;
MenuPrivate(fSuper).GetItemMargins(&leftMargin, NULL, NULL, NULL);
float gap = leftMargin / 4;
r.right = r.left + leftMargin - gap;
r.left += gap / 3;
BPoint center(floorf((r.left + r.right) / 2.0),
floorf((r.top + r.bottom) / 2.0));
float size = std::min(r.Height() - 2, r.Width());
r.top = floorf(center.y - size / 2 + 0.5);
r.bottom = floorf(center.y + size / 2 + 0.5);
r.left = floorf(center.x - size / 2 + 0.5);
r.right = floorf(center.x + size / 2 + 0.5);
BShape arrowShape;
center.x += 0.5;
center.y += 0.5;
size *= 0.3;
arrowShape.MoveTo(BPoint(center.x - size, center.y - size * 0.25));
arrowShape.LineTo(BPoint(center.x - size * 0.25, center.y + size));
arrowShape.LineTo(BPoint(center.x + size, center.y - size));
fSuper->SetHighColor(tint_color(_HighColor(), kMarkTint));
fSuper->SetDrawingMode(B_OP_OVER);
fSuper->SetPenSize(2.0);
fSuper->MovePenTo(B_ORIGIN);
fSuper->StrokeShape(&arrowShape);
fSuper->PopState();
}
void
BMenuItem::_DrawShortcutSymbol(bool submenus)
{
BMenu* menu = fSuper;
BFont font;
menu->GetFont(&font);
BPoint where = ContentLocation();
where.x = fBounds.right - font.Size();
if (submenus)
where.x -= fBounds.Height() / 2;
const float ascent = MenuPrivate(fSuper).Ascent();
if (fShortcutChar < B_SPACE && kUTF8ControlMap[(int)fShortcutChar])
_DrawControlChar(fShortcutChar, where + BPoint(0, ascent));
else
fSuper->DrawChar(fShortcutChar, where + BPoint(0, ascent));
where.y += (fBounds.Height() - 11) / 2 - 1;
where.x -= 4;
if ((fModifiers & B_COMMAND_KEY) != 0) {
const BBitmap* command = MenuPrivate::MenuItemCommand();
const BRect &rect = command->Bounds();
where.x -= rect.Width() + 1;
fSuper->DrawBitmap(command, where);
}
if ((fModifiers & B_CONTROL_KEY) != 0) {
const BBitmap* control = MenuPrivate::MenuItemControl();
const BRect &rect = control->Bounds();
where.x -= rect.Width() + 1;
fSuper->DrawBitmap(control, where);
}
if ((fModifiers & B_OPTION_KEY) != 0) {
const BBitmap* option = MenuPrivate::MenuItemOption();
const BRect &rect = option->Bounds();
where.x -= rect.Width() + 1;
fSuper->DrawBitmap(option, where);
}
if ((fModifiers & B_SHIFT_KEY) != 0) {
const BBitmap* shift = MenuPrivate::MenuItemShift();
const BRect &rect = shift->Bounds();
where.x -= rect.Width() + 1;
fSuper->DrawBitmap(shift, where);
}
}
void
BMenuItem::_DrawSubmenuSymbol()
{
fSuper->PushState();
float symbolSize = roundf(Frame().Height() * 2 / 3);
BRect rect(fBounds);
rect.left = rect.right - symbolSize;
BRect symbolRect(0, 0, symbolSize, symbolSize);
symbolRect.OffsetTo(BPoint(rect.left,
fBounds.top + (fBounds.Height() - symbolSize) / 2));
be_control_look->DrawArrowShape(Menu(), symbolRect, symbolRect,
_HighColor(), BControlLook::B_RIGHT_ARROW, 0, kMarkTint);
fSuper->PopState();
}
void
BMenuItem::_DrawControlChar(char shortcut, BPoint where)
{
const char* symbol = " ";
if (kUTF8ControlMap[(int)fShortcutChar])
symbol = kUTF8ControlMap[(int)fShortcutChar];
fSuper->DrawString(symbol, where);
}
void
BMenuItem::SetAutomaticTrigger(int32 index, uint32 trigger)
{
fTriggerIndex = index;
fTrigger = trigger;
}