/*
* Copyright 2001-2018 Haiku, Inc. All rights reserved.
* Distributed under the terms of the MIT license.
*
* Authors:
* Stephan Aßmus, superstippi@gmx.de
* Stefano Ceccherini, stefano.ceccherini@gmail.com
* Adrien Destugues, pulkomandy@pulkomandy.tk
* Marc Flerackers, mflerackers@androme.be
* Rene Gollent, anevilyak@gmail.com
* John Scipione, jscipione@gmail.com
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "utf8_functions.h"
#define USE_CACHED_MENUWINDOW 1
using BPrivate::gSystemCatalog;
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Menu"
#undef B_TRANSLATE
#define B_TRANSLATE(str) \
gSystemCatalog.GetString(B_TRANSLATE_MARK(str), "Menu")
using std::nothrow;
using BPrivate::BMenuWindow;
namespace BPrivate {
class TriggerList {
public:
TriggerList() {}
~TriggerList() {}
bool HasTrigger(uint32 c)
{ return fList.find(BUnicodeChar::ToLower(c)) != fList.end(); }
bool AddTrigger(uint32 c)
{
fList.insert(BUnicodeChar::ToLower(c));
return true;
}
private:
std::set fList;
};
class ExtraMenuData {
public:
menu_tracking_hook trackingHook;
void* trackingState;
// Used to track when the menu would be drawn offscreen and instead gets
// shifted back on the screen towards the left. This information
// allows us to draw submenus in the same direction as their parents.
bool frameShiftedLeft;
ExtraMenuData()
{
trackingHook = NULL;
trackingState = NULL;
frameShiftedLeft = false;
}
};
} // namespace BPrivate
menu_info BMenu::sMenuInfo;
uint32 BMenu::sShiftKey;
uint32 BMenu::sControlKey;
uint32 BMenu::sOptionKey;
uint32 BMenu::sCommandKey;
uint32 BMenu::sMenuKey;
static property_info sPropList[] = {
{ "Enabled", { B_GET_PROPERTY, 0 },
{ B_DIRECT_SPECIFIER, 0 }, "Returns true if menu or menu item is "
"enabled; false otherwise.",
0, { B_BOOL_TYPE }
},
{ "Enabled", { B_SET_PROPERTY, 0 },
{ B_DIRECT_SPECIFIER, 0 }, "Enables or disables menu or menu item.",
0, { B_BOOL_TYPE }
},
{ "Label", { B_GET_PROPERTY, 0 },
{ B_DIRECT_SPECIFIER, 0 }, "Returns the string label of the menu or "
"menu item.",
0, { B_STRING_TYPE }
},
{ "Label", { B_SET_PROPERTY, 0 },
{ B_DIRECT_SPECIFIER, 0 }, "Sets the string label of the menu or menu "
"item.",
0, { B_STRING_TYPE }
},
{ "Mark", { B_GET_PROPERTY, 0 },
{ B_DIRECT_SPECIFIER, 0 }, "Returns true if the menu item or the "
"menu's superitem is marked; false otherwise.",
0, { B_BOOL_TYPE }
},
{ "Mark", { B_SET_PROPERTY, 0 },
{ B_DIRECT_SPECIFIER, 0 }, "Marks or unmarks the menu item or the "
"menu's superitem.",
0, { B_BOOL_TYPE }
},
{ "Menu", { B_CREATE_PROPERTY, 0 },
{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
"Adds a new menu item at the specified index with the text label "
"found in \"data\" and the int32 command found in \"what\" (used as "
"the what field in the BMessage sent by the item)." , 0, {},
{ {{{"data", B_STRING_TYPE}}}
}
},
{ "Menu", { B_DELETE_PROPERTY, 0 },
{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
"Removes the selected menu or menus.", 0, {}
},
{ "Menu", { },
{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
"Directs scripting message to the specified menu, first popping the "
"current specifier off the stack.", 0, {}
},
{ "MenuItem", { B_COUNT_PROPERTIES, 0 },
{ B_DIRECT_SPECIFIER, 0 }, "Counts the number of menu items in the "
"specified menu.",
0, { B_INT32_TYPE }
},
{ "MenuItem", { B_CREATE_PROPERTY, 0 },
{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
"Adds a new menu item at the specified index with the text label "
"found in \"data\" and the int32 command found in \"what\" (used as "
"the what field in the BMessage sent by the item).", 0, {},
{ { {{"data", B_STRING_TYPE },
{"be:invoke_message", B_MESSAGE_TYPE},
{"what", B_INT32_TYPE},
{"be:target", B_MESSENGER_TYPE}} }
}
},
{ "MenuItem", { B_DELETE_PROPERTY, 0 },
{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
"Removes the specified menu item from its parent menu."
},
{ "MenuItem", { B_EXECUTE_PROPERTY, 0 },
{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
"Invokes the specified menu item."
},
{ "MenuItem", { },
{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
"Directs scripting message to the specified menu, first popping the "
"current specifier off the stack."
},
{ 0 }
};
// note: this is redefined to localized one in BMenu::_InitData
const char* BPrivate::kEmptyMenuLabel = "";
struct BMenu::LayoutData {
BSize preferred;
uint32 lastResizingMode;
};
// #pragma mark - BMenu
BMenu::BMenu(const char* name, menu_layout layout)
:
BView(BRect(0, 0, 0, 0), name, 0, B_WILL_DRAW),
fChosenItem(NULL),
fSelected(NULL),
fCachedMenuWindow(NULL),
fSuper(NULL),
fSuperitem(NULL),
fAscent(-1.0f),
fDescent(-1.0f),
fFontHeight(-1.0f),
fState(MENU_STATE_CLOSED),
fLayout(layout),
fExtraRect(NULL),
fMaxContentWidth(0.0f),
fInitMatrixSize(NULL),
fExtraMenuData(NULL),
fTrigger(0),
fResizeToFit(true),
fUseCachedMenuLayout(false),
fEnabled(true),
fDynamicName(false),
fRadioMode(false),
fTrackNewBounds(false),
fStickyMode(false),
fIgnoreHidden(true),
fTriggerEnabled(true),
fHasSubmenus(false),
fAttachAborted(false)
{
_InitData(NULL);
}
BMenu::BMenu(const char* name, float width, float height)
:
BView(BRect(0.0f, 0.0f, 0.0f, 0.0f), name, 0, B_WILL_DRAW),
fChosenItem(NULL),
fSelected(NULL),
fCachedMenuWindow(NULL),
fSuper(NULL),
fSuperitem(NULL),
fAscent(-1.0f),
fDescent(-1.0f),
fFontHeight(-1.0f),
fState(0),
fLayout(B_ITEMS_IN_MATRIX),
fExtraRect(NULL),
fMaxContentWidth(0.0f),
fInitMatrixSize(NULL),
fExtraMenuData(NULL),
fTrigger(0),
fResizeToFit(true),
fUseCachedMenuLayout(false),
fEnabled(true),
fDynamicName(false),
fRadioMode(false),
fTrackNewBounds(false),
fStickyMode(false),
fIgnoreHidden(true),
fTriggerEnabled(true),
fHasSubmenus(false),
fAttachAborted(false)
{
_InitData(NULL);
}
BMenu::BMenu(BMessage* archive)
:
BView(archive),
fChosenItem(NULL),
fSelected(NULL),
fCachedMenuWindow(NULL),
fSuper(NULL),
fSuperitem(NULL),
fAscent(-1.0f),
fDescent(-1.0f),
fFontHeight(-1.0f),
fState(MENU_STATE_CLOSED),
fLayout(B_ITEMS_IN_ROW),
fExtraRect(NULL),
fMaxContentWidth(0.0f),
fInitMatrixSize(NULL),
fExtraMenuData(NULL),
fTrigger(0),
fResizeToFit(true),
fUseCachedMenuLayout(false),
fEnabled(true),
fDynamicName(false),
fRadioMode(false),
fTrackNewBounds(false),
fStickyMode(false),
fIgnoreHidden(true),
fTriggerEnabled(true),
fHasSubmenus(false),
fAttachAborted(false)
{
_InitData(archive);
}
BMenu::~BMenu()
{
_DeleteMenuWindow();
RemoveItems(0, CountItems(), true);
delete fInitMatrixSize;
delete fExtraMenuData;
delete fLayoutData;
}
BArchivable*
BMenu::Instantiate(BMessage* archive)
{
if (validate_instantiation(archive, "BMenu"))
return new (nothrow) BMenu(archive);
return NULL;
}
status_t
BMenu::Archive(BMessage* data, bool deep) const
{
status_t err = BView::Archive(data, deep);
if (err == B_OK && Layout() != B_ITEMS_IN_ROW)
err = data->AddInt32("_layout", Layout());
if (err == B_OK)
err = data->AddBool("_rsize_to_fit", fResizeToFit);
if (err == B_OK)
err = data->AddBool("_disable", !IsEnabled());
if (err == B_OK)
err = data->AddBool("_radio", IsRadioMode());
if (err == B_OK)
err = data->AddBool("_trig_disabled", AreTriggersEnabled());
if (err == B_OK)
err = data->AddBool("_dyn_label", fDynamicName);
if (err == B_OK)
err = data->AddFloat("_maxwidth", fMaxContentWidth);
if (err == B_OK && deep) {
BMenuItem* item = NULL;
int32 index = 0;
while ((item = ItemAt(index++)) != NULL) {
BMessage itemData;
item->Archive(&itemData, deep);
err = data->AddMessage("_items", &itemData);
if (err != B_OK)
break;
if (fLayout == B_ITEMS_IN_MATRIX) {
err = data->AddRect("_i_frames", item->fBounds);
}
}
}
return err;
}
void
BMenu::AttachedToWindow()
{
BView::AttachedToWindow();
_GetShiftKey(sShiftKey);
_GetControlKey(sControlKey);
_GetCommandKey(sCommandKey);
_GetOptionKey(sOptionKey);
_GetMenuKey(sMenuKey);
// The menu should be added to the menu hierarchy and made visible if:
// * the mouse is over the menu,
// * the user has requested the menu via the keyboard.
// So if we don't pass keydown in here, keyboard navigation breaks since
// fAttachAborted will return false if the mouse isn't over the menu
bool keyDown = Supermenu() != NULL
? Supermenu()->fState == MENU_STATE_KEY_TO_SUBMENU : false;
fAttachAborted = _AddDynamicItems(keyDown);
if (!fAttachAborted) {
_CacheFontInfo();
_LayoutItems(0);
_UpdateWindowViewSize(false);
}
}
void
BMenu::DetachedFromWindow()
{
BView::DetachedFromWindow();
}
void
BMenu::AllAttached()
{
BView::AllAttached();
}
void
BMenu::AllDetached()
{
BView::AllDetached();
}
void
BMenu::Draw(BRect updateRect)
{
if (_RelayoutIfNeeded()) {
Invalidate();
return;
}
DrawBackground(updateRect);
DrawItems(updateRect);
}
void
BMenu::MessageReceived(BMessage* message)
{
if (message->HasSpecifiers())
return _ScriptReceived(message);
switch (message->what) {
case B_MOUSE_WHEEL_CHANGED:
{
float deltaY = 0;
message->FindFloat("be:wheel_delta_y", &deltaY);
if (deltaY == 0)
return;
BMenuWindow* window = dynamic_cast(Window());
if (window == NULL)
return;
float largeStep;
float smallStep;
window->GetSteps(&smallStep, &largeStep);
// pressing the shift key scrolls faster
if ((modifiers() & B_SHIFT_KEY) != 0)
deltaY *= largeStep;
else
deltaY *= smallStep;
window->TryScrollBy(deltaY);
break;
}
default:
BView::MessageReceived(message);
break;
}
}
void
BMenu::KeyDown(const char* bytes, int32 numBytes)
{
// TODO: Test how it works on BeOS R5 and implement this correctly
switch (bytes[0]) {
case B_UP_ARROW:
case B_DOWN_ARROW:
{
BMenuBar* bar = dynamic_cast(Supermenu());
if (bar != NULL && fState == MENU_STATE_CLOSED) {
// tell MenuBar's _Track:
bar->fState = MENU_STATE_KEY_TO_SUBMENU;
}
if (fLayout == B_ITEMS_IN_COLUMN)
_SelectNextItem(fSelected, bytes[0] == B_DOWN_ARROW);
break;
}
case B_LEFT_ARROW:
if (fLayout == B_ITEMS_IN_ROW)
_SelectNextItem(fSelected, false);
else {
// this case has to be handled a bit specially.
BMenuItem* item = Superitem();
if (item) {
if (dynamic_cast(Supermenu())) {
// If we're at the top menu below the menu bar, pass
// the keypress to the menu bar so we can move to
// another top level menu.
BMessenger messenger(Supermenu());
messenger.SendMessage(Window()->CurrentMessage());
} else {
// tell _Track
fState = MENU_STATE_KEY_LEAVE_SUBMENU;
}
}
}
break;
case B_RIGHT_ARROW:
if (fLayout == B_ITEMS_IN_ROW)
_SelectNextItem(fSelected, true);
else {
if (fSelected != NULL && fSelected->Submenu() != NULL) {
fSelected->Submenu()->_SetStickyMode(true);
// fix me: this shouldn't be needed but dynamic menus
// aren't getting it set correctly when keyboard
// navigating, which aborts the attach
fState = MENU_STATE_KEY_TO_SUBMENU;
_SelectItem(fSelected, true, true, true);
} else if (dynamic_cast(Supermenu())) {
// if we have no submenu and we're an
// item in the top menu below the menubar,
// pass the keypress to the menubar
// so you can use the keypress to switch menus.
BMessenger messenger(Supermenu());
messenger.SendMessage(Window()->CurrentMessage());
}
}
break;
case B_PAGE_UP:
case B_PAGE_DOWN:
{
BMenuWindow* window = dynamic_cast(Window());
if (window == NULL || !window->HasScrollers())
break;
int32 deltaY = bytes[0] == B_PAGE_UP ? -1 : 1;
float largeStep;
window->GetSteps(NULL, &largeStep);
window->TryScrollBy(deltaY * largeStep);
break;
}
case B_ENTER:
case B_SPACE:
if (fSelected != NULL) {
fChosenItem = fSelected;
// preserve for exit handling
_QuitTracking(false);
}
break;
case B_ESCAPE:
_SelectItem(NULL);
if (fState == MENU_STATE_CLOSED
&& dynamic_cast(Supermenu())) {
// Keyboard may show menu without tracking it
BMessenger messenger(Supermenu());
messenger.SendMessage(Window()->CurrentMessage());
} else
_QuitTracking(false);
break;
default:
{
if (AreTriggersEnabled()) {
uint32 trigger = BUnicodeChar::FromUTF8(&bytes);
for (uint32 i = CountItems(); i-- > 0;) {
BMenuItem* item = ItemAt(i);
if (item->fTriggerIndex < 0 || item->fTrigger != trigger)
continue;
_InvokeItem(item);
_QuitTracking(false);
break;
}
}
break;
}
}
}
BSize
BMenu::MinSize()
{
_ValidatePreferredSize();
BSize size = (GetLayout() != NULL ? GetLayout()->MinSize()
: fLayoutData->preferred);
return BLayoutUtils::ComposeSize(ExplicitMinSize(), size);
}
BSize
BMenu::MaxSize()
{
_ValidatePreferredSize();
BSize size = (GetLayout() != NULL ? GetLayout()->MaxSize()
: fLayoutData->preferred);
return BLayoutUtils::ComposeSize(ExplicitMaxSize(), size);
}
BSize
BMenu::PreferredSize()
{
_ValidatePreferredSize();
BSize size = (GetLayout() != NULL ? GetLayout()->PreferredSize()
: fLayoutData->preferred);
return BLayoutUtils::ComposeSize(ExplicitPreferredSize(), size);
}
void
BMenu::GetPreferredSize(float* _width, float* _height)
{
_ValidatePreferredSize();
if (_width)
*_width = fLayoutData->preferred.width;
if (_height)
*_height = fLayoutData->preferred.height;
}
void
BMenu::ResizeToPreferred()
{
BView::ResizeToPreferred();
}
void
BMenu::DoLayout()
{
// If the user set a layout, we let the base class version call its
// hook.
if (GetLayout() != NULL) {
BView::DoLayout();
return;
}
if (_RelayoutIfNeeded())
Invalidate();
}
void
BMenu::FrameMoved(BPoint where)
{
BView::FrameMoved(where);
}
void
BMenu::FrameResized(float width, float height)
{
BView::FrameResized(width, height);
}
void
BMenu::InvalidateLayout()
{
fUseCachedMenuLayout = false;
// This method exits for backwards compatibility reasons, it is used to
// invalidate the menu layout, but we also use call
// BView::InvalidateLayout() for good measure. Don't delete this method!
BView::InvalidateLayout(false);
}
void
BMenu::MakeFocus(bool focused)
{
BView::MakeFocus(focused);
}
bool
BMenu::AddItem(BMenuItem* item)
{
return AddItem(item, CountItems());
}
bool
BMenu::AddItem(BMenuItem* item, int32 index)
{
if (fLayout == B_ITEMS_IN_MATRIX) {
debugger("BMenu::AddItem(BMenuItem*, int32) this method can only "
"be called if the menu layout is not B_ITEMS_IN_MATRIX");
}
if (item == NULL)
return false;
const bool locked = LockLooper();
if (!_AddItem(item, index)) {
if (locked)
UnlockLooper();
return false;
}
InvalidateLayout();
if (locked) {
if (!Window()->IsHidden()) {
_LayoutItems(index);
_UpdateWindowViewSize(false);
Invalidate();
}
UnlockLooper();
}
return true;
}
bool
BMenu::AddItem(BMenuItem* item, BRect frame)
{
if (fLayout != B_ITEMS_IN_MATRIX) {
debugger("BMenu::AddItem(BMenuItem*, BRect) this method can only "
"be called if the menu layout is B_ITEMS_IN_MATRIX");
}
if (item == NULL)
return false;
const bool locked = LockLooper();
item->fBounds = frame;
int32 index = CountItems();
if (!_AddItem(item, index)) {
if (locked)
UnlockLooper();
return false;
}
if (locked) {
if (!Window()->IsHidden()) {
_LayoutItems(index);
Invalidate();
}
UnlockLooper();
}
return true;
}
bool
BMenu::AddItem(BMenu* submenu)
{
BMenuItem* item = new (nothrow) BMenuItem(submenu);
if (item == NULL)
return false;
if (!AddItem(item, CountItems())) {
item->fSubmenu = NULL;
delete item;
return false;
}
return true;
}
bool
BMenu::AddItem(BMenu* submenu, int32 index)
{
if (fLayout == B_ITEMS_IN_MATRIX) {
debugger("BMenu::AddItem(BMenuItem*, int32) this method can only "
"be called if the menu layout is not B_ITEMS_IN_MATRIX");
}
BMenuItem* item = new (nothrow) BMenuItem(submenu);
if (item == NULL)
return false;
if (!AddItem(item, index)) {
item->fSubmenu = NULL;
delete item;
return false;
}
return true;
}
bool
BMenu::AddItem(BMenu* submenu, BRect frame)
{
if (fLayout != B_ITEMS_IN_MATRIX) {
debugger("BMenu::AddItem(BMenu*, BRect) this method can only "
"be called if the menu layout is B_ITEMS_IN_MATRIX");
}
BMenuItem* item = new (nothrow) BMenuItem(submenu);
if (item == NULL)
return false;
if (!AddItem(item, frame)) {
item->fSubmenu = NULL;
delete item;
return false;
}
return true;
}
bool
BMenu::AddList(BList* list, int32 index)
{
// TODO: test this function, it's not documented in the bebook.
if (list == NULL)
return false;
bool locked = LockLooper();
int32 numItems = list->CountItems();
for (int32 i = 0; i < numItems; i++) {
BMenuItem* item = static_cast(list->ItemAt(i));
if (item != NULL) {
if (!_AddItem(item, index + i))
break;
}
}
InvalidateLayout();
if (locked && Window() != NULL && !Window()->IsHidden()) {
// Make sure we update the layout if needed.
_LayoutItems(index);
_UpdateWindowViewSize(false);
Invalidate();
}
if (locked)
UnlockLooper();
return true;
}
bool
BMenu::AddSeparatorItem()
{
BMenuItem* item = new (nothrow) BSeparatorItem();
if (!item || !AddItem(item, CountItems())) {
delete item;
return false;
}
return true;
}
bool
BMenu::RemoveItem(BMenuItem* item)
{
return _RemoveItems(0, 0, item, false);
}
BMenuItem*
BMenu::RemoveItem(int32 index)
{
BMenuItem* item = ItemAt(index);
if (item != NULL)
_RemoveItems(index, 1, NULL, false);
return item;
}
bool
BMenu::RemoveItems(int32 index, int32 count, bool deleteItems)
{
return _RemoveItems(index, count, NULL, deleteItems);
}
bool
BMenu::RemoveItem(BMenu* submenu)
{
for (int32 i = 0; i < fItems.CountItems(); i++) {
if (static_cast(fItems.ItemAtFast(i))->Submenu()
== submenu) {
return _RemoveItems(i, 1, NULL, false);
}
}
return false;
}
int32
BMenu::CountItems() const
{
return fItems.CountItems();
}
BMenuItem*
BMenu::ItemAt(int32 index) const
{
return static_cast(fItems.ItemAt(index));
}
BMenu*
BMenu::SubmenuAt(int32 index) const
{
BMenuItem* item = static_cast(fItems.ItemAt(index));
return item != NULL ? item->Submenu() : NULL;
}
int32
BMenu::IndexOf(BMenuItem* item) const
{
return fItems.IndexOf(item);
}
int32
BMenu::IndexOf(BMenu* submenu) const
{
for (int32 i = 0; i < fItems.CountItems(); i++) {
if (ItemAt(i)->Submenu() == submenu)
return i;
}
return -1;
}
BMenuItem*
BMenu::FindItem(const char* label) const
{
BMenuItem* item = NULL;
for (int32 i = 0; i < CountItems(); i++) {
item = ItemAt(i);
if (item->Label() && strcmp(item->Label(), label) == 0)
return item;
if (item->Submenu() != NULL) {
item = item->Submenu()->FindItem(label);
if (item != NULL)
return item;
}
}
return NULL;
}
BMenuItem*
BMenu::FindItem(uint32 command) const
{
BMenuItem* item = NULL;
for (int32 i = 0; i < CountItems(); i++) {
item = ItemAt(i);
if (item->Command() == command)
return item;
if (item->Submenu() != NULL) {
item = item->Submenu()->FindItem(command);
if (item != NULL)
return item;
}
}
return NULL;
}
status_t
BMenu::SetTargetForItems(BHandler* handler)
{
status_t status = B_OK;
for (int32 i = 0; i < fItems.CountItems(); i++) {
status = ItemAt(i)->SetTarget(handler);
if (status < B_OK)
break;
}
return status;
}
status_t
BMenu::SetTargetForItems(BMessenger messenger)
{
status_t status = B_OK;
for (int32 i = 0; i < fItems.CountItems(); i++) {
status = ItemAt(i)->SetTarget(messenger);
if (status < B_OK)
break;
}
return status;
}
void
BMenu::SetEnabled(bool enable)
{
if (fEnabled == enable)
return;
fEnabled = enable;
if (dynamic_cast<_BMCMenuBar_*>(Supermenu()) != NULL)
Supermenu()->SetEnabled(enable);
if (fSuperitem)
fSuperitem->SetEnabled(enable);
}
void
BMenu::SetRadioMode(bool on)
{
fRadioMode = on;
if (!on)
SetLabelFromMarked(false);
}
void
BMenu::SetTriggersEnabled(bool enable)
{
fTriggerEnabled = enable;
}
void
BMenu::SetMaxContentWidth(float width)
{
fMaxContentWidth = width;
}
void
BMenu::SetLabelFromMarked(bool on)
{
fDynamicName = on;
if (on)
SetRadioMode(true);
}
bool
BMenu::IsLabelFromMarked()
{
return fDynamicName;
}
bool
BMenu::IsEnabled() const
{
if (!fEnabled)
return false;
return fSuper ? fSuper->IsEnabled() : true ;
}
bool
BMenu::IsRadioMode() const
{
return fRadioMode;
}
bool
BMenu::AreTriggersEnabled() const
{
return fTriggerEnabled;
}
bool
BMenu::IsRedrawAfterSticky() const
{
return false;
}
float
BMenu::MaxContentWidth() const
{
return fMaxContentWidth;
}
BMenuItem*
BMenu::FindMarked()
{
for (int32 i = 0; i < fItems.CountItems(); i++) {
BMenuItem* item = ItemAt(i);
if (item->IsMarked())
return item;
}
return NULL;
}
int32
BMenu::FindMarkedIndex()
{
for (int32 i = 0; i < fItems.CountItems(); i++) {
BMenuItem* item = ItemAt(i);
if (item->IsMarked())
return i;
}
return -1;
}
BMenu*
BMenu::Supermenu() const
{
return fSuper;
}
BMenuItem*
BMenu::Superitem() const
{
return fSuperitem;
}
BHandler*
BMenu::ResolveSpecifier(BMessage* msg, int32 index, BMessage* specifier,
int32 form, const char* property)
{
BPropertyInfo propInfo(sPropList);
BHandler* target = NULL;
if (propInfo.FindMatch(msg, index, specifier, form, property) >= B_OK) {
target = this;
}
if (!target)
target = BView::ResolveSpecifier(msg, index, specifier, form,
property);
return target;
}
status_t
BMenu::GetSupportedSuites(BMessage* data)
{
if (data == NULL)
return B_BAD_VALUE;
status_t err = data->AddString("suites", "suite/vnd.Be-menu");
if (err < B_OK)
return err;
BPropertyInfo propertyInfo(sPropList);
err = data->AddFlat("messages", &propertyInfo);
if (err < B_OK)
return err;
return BView::GetSupportedSuites(data);
}
status_t
BMenu::Perform(perform_code code, void* _data)
{
switch (code) {
case PERFORM_CODE_MIN_SIZE:
((perform_data_min_size*)_data)->return_value
= BMenu::MinSize();
return B_OK;
case PERFORM_CODE_MAX_SIZE:
((perform_data_max_size*)_data)->return_value
= BMenu::MaxSize();
return B_OK;
case PERFORM_CODE_PREFERRED_SIZE:
((perform_data_preferred_size*)_data)->return_value
= BMenu::PreferredSize();
return B_OK;
case PERFORM_CODE_LAYOUT_ALIGNMENT:
((perform_data_layout_alignment*)_data)->return_value
= BMenu::LayoutAlignment();
return B_OK;
case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
((perform_data_has_height_for_width*)_data)->return_value
= BMenu::HasHeightForWidth();
return B_OK;
case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
{
perform_data_get_height_for_width* data
= (perform_data_get_height_for_width*)_data;
BMenu::GetHeightForWidth(data->width, &data->min, &data->max,
&data->preferred);
return B_OK;
}
case PERFORM_CODE_SET_LAYOUT:
{
perform_data_set_layout* data = (perform_data_set_layout*)_data;
BMenu::SetLayout(data->layout);
return B_OK;
}
case PERFORM_CODE_LAYOUT_INVALIDATED:
{
perform_data_layout_invalidated* data
= (perform_data_layout_invalidated*)_data;
BMenu::LayoutInvalidated(data->descendants);
return B_OK;
}
case PERFORM_CODE_DO_LAYOUT:
{
BMenu::DoLayout();
return B_OK;
}
}
return BView::Perform(code, _data);
}
// #pragma mark - BMenu protected methods
BMenu::BMenu(BRect frame, const char* name, uint32 resizingMode, uint32 flags,
menu_layout layout, bool resizeToFit)
:
BView(frame, name, resizingMode, flags),
fChosenItem(NULL),
fSelected(NULL),
fCachedMenuWindow(NULL),
fSuper(NULL),
fSuperitem(NULL),
fAscent(-1.0f),
fDescent(-1.0f),
fFontHeight(-1.0f),
fState(MENU_STATE_CLOSED),
fLayout(layout),
fExtraRect(NULL),
fMaxContentWidth(0.0f),
fInitMatrixSize(NULL),
fExtraMenuData(NULL),
fTrigger(0),
fResizeToFit(resizeToFit),
fUseCachedMenuLayout(false),
fEnabled(true),
fDynamicName(false),
fRadioMode(false),
fTrackNewBounds(false),
fStickyMode(false),
fIgnoreHidden(true),
fTriggerEnabled(true),
fHasSubmenus(false),
fAttachAborted(false)
{
_InitData(NULL);
}
void
BMenu::SetItemMargins(float left, float top, float right, float bottom)
{
fPad.Set(left, top, right, bottom);
}
void
BMenu::GetItemMargins(float* _left, float* _top, float* _right,
float* _bottom) const
{
if (_left != NULL)
*_left = fPad.left;
if (_top != NULL)
*_top = fPad.top;
if (_right != NULL)
*_right = fPad.right;
if (_bottom != NULL)
*_bottom = fPad.bottom;
}
menu_layout
BMenu::Layout() const
{
return fLayout;
}
void
BMenu::Show()
{
Show(false);
}
void
BMenu::Show(bool selectFirst)
{
_Install(NULL);
_Show(selectFirst);
}
void
BMenu::Hide()
{
_Hide();
_Uninstall();
}
BMenuItem*
BMenu::Track(bool sticky, BRect* clickToOpenRect)
{
if (sticky && LockLooper()) {
//RedrawAfterSticky(Bounds());
// the call above didn't do anything, so I've removed it for now
UnlockLooper();
}
if (clickToOpenRect != NULL && LockLooper()) {
fExtraRect = clickToOpenRect;
ConvertFromScreen(fExtraRect);
UnlockLooper();
}
_SetStickyMode(sticky);
int action;
BMenuItem* menuItem = _Track(&action);
fExtraRect = NULL;
return menuItem;
}
// #pragma mark - BMenu private methods
bool
BMenu::AddDynamicItem(add_state state)
{
// Implemented in subclasses
return false;
}
void
BMenu::DrawBackground(BRect updateRect)
{
rgb_color base = ui_color(B_MENU_BACKGROUND_COLOR);
uint32 flags = 0;
if (!IsEnabled())
flags |= BControlLook::B_DISABLED;
if (IsFocus())
flags |= BControlLook::B_FOCUSED;
BRect rect = Bounds();
uint32 borders = BControlLook::B_LEFT_BORDER
| BControlLook::B_RIGHT_BORDER;
if (Window() != NULL && Parent() != NULL) {
if (Parent()->Frame().top == Window()->Bounds().top)
borders |= BControlLook::B_TOP_BORDER;
if (Parent()->Frame().bottom == Window()->Bounds().bottom)
borders |= BControlLook::B_BOTTOM_BORDER;
} else {
borders |= BControlLook::B_TOP_BORDER
| BControlLook::B_BOTTOM_BORDER;
}
be_control_look->DrawMenuBackground(this, rect, updateRect, base, flags,
borders);
}
void
BMenu::SetTrackingHook(menu_tracking_hook func, void* state)
{
fExtraMenuData->trackingHook = func;
fExtraMenuData->trackingState = state;
}
// #pragma mark - Reorder item methods
void
BMenu::SortItems(int (*compare)(const BMenuItem*, const BMenuItem*))
{
fItems.SortItems((int (*)(const void*, const void*))compare);
InvalidateLayout();
if (Window() != NULL && !Window()->IsHidden() && LockLooper()) {
_LayoutItems(0);
Invalidate();
UnlockLooper();
}
}
bool
BMenu::SwapItems(int32 indexA, int32 indexB)
{
bool swapped = fItems.SwapItems(indexA, indexB);
if (swapped) {
InvalidateLayout();
if (Window() != NULL && !Window()->IsHidden() && LockLooper()) {
_LayoutItems(std::min(indexA, indexB));
Invalidate();
UnlockLooper();
}
}
return swapped;
}
bool
BMenu::MoveItem(int32 indexFrom, int32 indexTo)
{
bool moved = fItems.MoveItem(indexFrom, indexTo);
if (moved) {
InvalidateLayout();
if (Window() != NULL && !Window()->IsHidden() && LockLooper()) {
_LayoutItems(std::min(indexFrom, indexTo));
Invalidate();
UnlockLooper();
}
}
return moved;
}
void BMenu::_ReservedMenu3() {}
void BMenu::_ReservedMenu4() {}
void BMenu::_ReservedMenu5() {}
void BMenu::_ReservedMenu6() {}
void
BMenu::_InitData(BMessage* archive)
{
BPrivate::kEmptyMenuLabel = B_TRANSLATE("");
// TODO: Get _color, _fname, _fflt from the message, if present
BFont font;
font.SetFamilyAndStyle(sMenuInfo.f_family, sMenuInfo.f_style);
font.SetSize(sMenuInfo.font_size);
SetFont(&font, B_FONT_FAMILY_AND_STYLE | B_FONT_SIZE);
fExtraMenuData = new (nothrow) BPrivate::ExtraMenuData();
const float labelSpacing = be_control_look->DefaultLabelSpacing();
fPad = BRect(ceilf(labelSpacing * 2.3f), ceilf(labelSpacing / 3.0f),
ceilf((labelSpacing / 3.0f) * 10.0f), 0.0f);
fLayoutData = new LayoutData;
fLayoutData->lastResizingMode = ResizingMode();
SetLowUIColor(B_MENU_BACKGROUND_COLOR);
SetViewColor(B_TRANSPARENT_COLOR);
fTriggerEnabled = sMenuInfo.triggers_always_shown;
if (archive != NULL) {
archive->FindInt32("_layout", (int32*)&fLayout);
archive->FindBool("_rsize_to_fit", &fResizeToFit);
bool disabled;
if (archive->FindBool("_disable", &disabled) == B_OK)
fEnabled = !disabled;
archive->FindBool("_radio", &fRadioMode);
bool disableTrigger = false;
archive->FindBool("_trig_disabled", &disableTrigger);
fTriggerEnabled = !disableTrigger;
archive->FindBool("_dyn_label", &fDynamicName);
archive->FindFloat("_maxwidth", &fMaxContentWidth);
BMessage msg;
for (int32 i = 0; archive->FindMessage("_items", i, &msg) == B_OK; i++) {
BArchivable* object = instantiate_object(&msg);
if (BMenuItem* item = dynamic_cast(object)) {
BRect bounds;
if (fLayout == B_ITEMS_IN_MATRIX
&& archive->FindRect("_i_frames", i, &bounds) == B_OK)
AddItem(item, bounds);
else
AddItem(item);
}
}
}
}
bool
BMenu::_Show(bool selectFirstItem, bool keyDown)
{
if (Window() != NULL)
return false;
// See if the supermenu has a cached menuwindow,
// and use that one if possible.
BMenuWindow* window = NULL;
bool ourWindow = false;
if (fSuper != NULL) {
fSuperbounds = fSuper->ConvertToScreen(fSuper->Bounds());
window = fSuper->_MenuWindow();
}
// Otherwise, create a new one
// This happens for "stand alone" BPopUpMenus
// (i.e. not within a BMenuField)
if (window == NULL) {
// Menu windows get the BMenu's handler name
window = new (nothrow) BMenuWindow(Name());
ourWindow = true;
}
if (window == NULL)
return false;
if (window->Lock()) {
bool addAborted = false;
if (keyDown)
addAborted = _AddDynamicItems(keyDown);
if (addAborted) {
if (ourWindow)
window->Quit();
else
window->Unlock();
return false;
}
fAttachAborted = false;
window->AttachMenu(this);
if (ItemAt(0) != NULL) {
float width, height;
ItemAt(0)->GetContentSize(&width, &height);
window->SetSmallStep(ceilf(height));
}
// Menu didn't have the time to add its items: aborting...
if (fAttachAborted) {
window->DetachMenu();
// TODO: Probably not needed, we can just let _hide() quit the
// window.
if (ourWindow)
window->Quit();
else
window->Unlock();
return false;
}
_UpdateWindowViewSize(true);
window->Show();
if (selectFirstItem)
_SelectItem(ItemAt(0), false);
window->Unlock();
}
return true;
}
void
BMenu::_Hide()
{
BMenuWindow* window = dynamic_cast(Window());
if (window == NULL || !window->Lock())
return;
if (fSelected != NULL)
_SelectItem(NULL);
window->Hide();
window->DetachMenu();
// we don't want to be deleted when the window is removed
#if USE_CACHED_MENUWINDOW
if (fSuper != NULL)
window->Unlock();
else
#endif
window->Quit();
// it's our window, quit it
_DeleteMenuWindow();
// Delete the menu window used by our submenus
}
void BMenu::_ScriptReceived(BMessage* message)
{
BMessage replyMsg(B_REPLY);
status_t err = B_BAD_SCRIPT_SYNTAX;
int32 index;
BMessage specifier;
int32 what;
const char* property;
if (message->GetCurrentSpecifier(&index, &specifier, &what, &property)
!= B_OK) {
return BView::MessageReceived(message);
}
BPropertyInfo propertyInfo(sPropList);
switch (propertyInfo.FindMatch(message, index, &specifier, what,
property)) {
case 0: // Enabled: GET
if (message->what == B_GET_PROPERTY)
err = replyMsg.AddBool("result", IsEnabled());
break;
case 1: // Enabled: SET
if (message->what == B_SET_PROPERTY) {
bool isEnabled;
err = message->FindBool("data", &isEnabled);
if (err >= B_OK)
SetEnabled(isEnabled);
}
break;
case 2: // Label: GET
case 3: // Label: SET
case 4: // Mark: GET
case 5: { // Mark: SET
BMenuItem *item = Superitem();
if (item != NULL)
return Supermenu()->_ItemScriptReceived(message, item);
break;
}
case 6: // Menu: CREATE
if (message->what == B_CREATE_PROPERTY) {
const char *label;
ObjectDeleter invokeMessage(new BMessage());
BMessenger target;
ObjectDeleter item;
err = message->FindString("data", &label);
if (err >= B_OK) {
invokeMessage.SetTo(new BMessage());
err = message->FindInt32("what",
(int32*)&invokeMessage->what);
if (err == B_NAME_NOT_FOUND) {
invokeMessage.Unset();
err = B_OK;
}
}
if (err >= B_OK) {
item.SetTo(new BMenuItem(new BMenu(label),
invokeMessage.Detach()));
}
if (err >= B_OK) {
err = _InsertItemAtSpecifier(specifier, what, item.Get());
}
if (err >= B_OK)
item.Detach();
}
break;
case 7: { // Menu: DELETE
if (message->what == B_DELETE_PROPERTY) {
BMenuItem *item = NULL;
int32 index;
err = _ResolveItemSpecifier(specifier, what, item, &index);
if (err >= B_OK) {
if (item->Submenu() == NULL)
err = B_BAD_VALUE;
else {
if (index >= 0)
RemoveItem(index);
else
RemoveItem(item);
}
}
}
break;
}
case 8: { // Menu: *
// TODO: check that submenu looper is running and handle it
// correctly
BMenu *submenu = NULL;
BMenuItem *item;
err = _ResolveItemSpecifier(specifier, what, item);
if (err >= B_OK)
submenu = item->Submenu();
if (submenu != NULL) {
message->PopSpecifier();
return submenu->_ScriptReceived(message);
}
break;
}
case 9: // MenuItem: COUNT
if (message->what == B_COUNT_PROPERTIES)
err = replyMsg.AddInt32("result", CountItems());
break;
case 10: // MenuItem: CREATE
if (message->what == B_CREATE_PROPERTY) {
const char *label;
ObjectDeleter invokeMessage(new BMessage());
bool targetPresent = true;
BMessenger target;
ObjectDeleter item;
err = message->FindString("data", &label);
if (err >= B_OK) {
err = message->FindMessage("be:invoke_message",
invokeMessage.Get());
if (err == B_NAME_NOT_FOUND) {
err = message->FindInt32("what",
(int32*)&invokeMessage->what);
if (err == B_NAME_NOT_FOUND) {
invokeMessage.Unset();
err = B_OK;
}
}
}
if (err >= B_OK) {
err = message->FindMessenger("be:target", &target);
if (err == B_NAME_NOT_FOUND) {
targetPresent = false;
err = B_OK;
}
}
if (err >= B_OK) {
item.SetTo(new BMenuItem(label, invokeMessage.Detach()));
if (targetPresent)
err = item->SetTarget(target);
}
if (err >= B_OK) {
err = _InsertItemAtSpecifier(specifier, what, item.Get());
}
if (err >= B_OK)
item.Detach();
}
break;
case 11: // MenuItem: DELETE
if (message->what == B_DELETE_PROPERTY) {
BMenuItem *item = NULL;
int32 index;
err = _ResolveItemSpecifier(specifier, what, item, &index);
if (err >= B_OK) {
if (index >= 0)
RemoveItem(index);
else
RemoveItem(item);
}
}
break;
case 12: { // MenuItem: EXECUTE
if (message->what == B_EXECUTE_PROPERTY) {
BMenuItem *item = NULL;
err = _ResolveItemSpecifier(specifier, what, item);
if (err >= B_OK) {
if (!item->IsEnabled())
err = B_NOT_ALLOWED;
else
err = item->Invoke();
}
}
break;
}
case 13: { // MenuItem: *
BMenuItem *item = NULL;
err = _ResolveItemSpecifier(specifier, what, item);
if (err >= B_OK) {
message->PopSpecifier();
return _ItemScriptReceived(message, item);
}
break;
}
default:
return BView::MessageReceived(message);
}
if (err != B_OK) {
replyMsg.what = B_MESSAGE_NOT_UNDERSTOOD;
if (err == B_BAD_SCRIPT_SYNTAX)
replyMsg.AddString("message", "Didn't understand the specifier(s)");
else
replyMsg.AddString("message", strerror(err));
}
replyMsg.AddInt32("error", err);
message->SendReply(&replyMsg);
}
void BMenu::_ItemScriptReceived(BMessage* message, BMenuItem* item)
{
BMessage replyMsg(B_REPLY);
status_t err = B_BAD_SCRIPT_SYNTAX;
int32 index;
BMessage specifier;
int32 what;
const char* property;
if (message->GetCurrentSpecifier(&index, &specifier, &what, &property)
!= B_OK) {
return BView::MessageReceived(message);
}
BPropertyInfo propertyInfo(sPropList);
switch (propertyInfo.FindMatch(message, index, &specifier, what,
property)) {
case 0: // Enabled: GET
if (message->what == B_GET_PROPERTY)
err = replyMsg.AddBool("result", item->IsEnabled());
break;
case 1: // Enabled: SET
if (message->what == B_SET_PROPERTY) {
bool isEnabled;
err = message->FindBool("data", &isEnabled);
if (err >= B_OK)
item->SetEnabled(isEnabled);
}
break;
case 2: // Label: GET
if (message->what == B_GET_PROPERTY)
err = replyMsg.AddString("result", item->Label());
break;
case 3: // Label: SET
if (message->what == B_SET_PROPERTY) {
const char *label;
err = message->FindString("data", &label);
if (err >= B_OK)
item->SetLabel(label);
}
case 4: // Mark: GET
if (message->what == B_GET_PROPERTY)
err = replyMsg.AddBool("result", item->IsMarked());
break;
case 5: // Mark: SET
if (message->what == B_SET_PROPERTY) {
bool isMarked;
err = message->FindBool("data", &isMarked);
if (err >= B_OK)
item->SetMarked(isMarked);
}
break;
case 6: // Menu: CREATE
case 7: // Menu: DELETE
case 8: // Menu: *
case 9: // MenuItem: COUNT
case 10: // MenuItem: CREATE
case 11: // MenuItem: DELETE
case 12: // MenuItem: EXECUTE
case 13: // MenuItem: *
break;
default:
return BView::MessageReceived(message);
}
if (err != B_OK) {
replyMsg.what = B_MESSAGE_NOT_UNDERSTOOD;
replyMsg.AddString("message", strerror(err));
}
replyMsg.AddInt32("error", err);
message->SendReply(&replyMsg);
}
status_t BMenu::_ResolveItemSpecifier(const BMessage& specifier, int32 what,
BMenuItem*& item, int32 *_index)
{
status_t err;
item = NULL;
int32 index = -1;
switch (what) {
case B_INDEX_SPECIFIER:
case B_REVERSE_INDEX_SPECIFIER: {
err = specifier.FindInt32("index", &index);
if (err < B_OK)
return err;
if (what == B_REVERSE_INDEX_SPECIFIER)
index = CountItems() - index;
item = ItemAt(index);
break;
}
case B_NAME_SPECIFIER: {
const char* name;
err = specifier.FindString("name", &name);
if (err < B_OK)
return err;
item = FindItem(name);
break;
}
}
if (item == NULL)
return B_BAD_INDEX;
if (_index != NULL)
*_index = index;
return B_OK;
}
status_t BMenu::_InsertItemAtSpecifier(const BMessage& specifier, int32 what,
BMenuItem* item)
{
status_t err;
switch (what) {
case B_INDEX_SPECIFIER:
case B_REVERSE_INDEX_SPECIFIER: {
int32 index;
err = specifier.FindInt32("index", &index);
if (err < B_OK) return err;
if (what == B_REVERSE_INDEX_SPECIFIER)
index = CountItems() - index;
if (!AddItem(item, index))
return B_BAD_INDEX;
break;
}
case B_NAME_SPECIFIER:
return B_NOT_SUPPORTED;
break;
}
return B_OK;
}
// #pragma mark - mouse tracking
const static bigtime_t kOpenSubmenuDelay = 0;
const static bigtime_t kNavigationAreaTimeout = 1000000;
BMenuItem*
BMenu::_Track(int* action, long start)
{
// TODO: cleanup
BMenuItem* item = NULL;
BRect navAreaRectAbove;
BRect navAreaRectBelow;
bigtime_t selectedTime = system_time();
bigtime_t navigationAreaTime = 0;
fState = MENU_STATE_TRACKING;
fChosenItem = NULL;
// we will use this for keyboard selection
BPoint location;
uint32 buttons = 0;
if (LockLooper()) {
GetMouse(&location, &buttons);
UnlockLooper();
}
bool releasedOnce = buttons == 0;
while (fState != MENU_STATE_CLOSED) {
if (_CustomTrackingWantsToQuit())
break;
if (!LockLooper())
break;
BMenuWindow* window = static_cast(Window());
BPoint screenLocation = ConvertToScreen(location);
if (window->CheckForScrolling(screenLocation)) {
UnlockLooper();
continue;
}
// The order of the checks is important
// to be able to handle overlapping menus:
// first we check if mouse is inside a submenu,
// then if the mouse is inside this menu,
// then if it's over a super menu.
if (_OverSubmenu(fSelected, screenLocation)
|| fState == MENU_STATE_KEY_TO_SUBMENU) {
if (fState == MENU_STATE_TRACKING) {
// not if from R.Arrow
fState = MENU_STATE_TRACKING_SUBMENU;
}
navAreaRectAbove = BRect();
navAreaRectBelow = BRect();
// Since the submenu has its own looper,
// we can unlock ours. Doing so also make sure
// that our window gets any update message to
// redraw itself
UnlockLooper();
// To prevent NULL access violation, ensure a menu has actually
// been selected and that it has a submenu. Because keyboard and
// mouse interactions set selected items differently, the menu
// tracking thread needs to be careful in triggering the navigation
// to the submenu.
if (fSelected != NULL) {
BMenu* submenu = fSelected->Submenu();
int submenuAction = MENU_STATE_TRACKING;
if (submenu != NULL) {
submenu->_SetStickyMode(_IsStickyMode());
// The following call blocks until the submenu
// gives control back to us, either because the mouse
// pointer goes out of the submenu's bounds, or because
// the user closes the menu
BMenuItem* submenuItem = submenu->_Track(&submenuAction);
if (submenuAction == MENU_STATE_CLOSED) {
item = submenuItem;
fState = MENU_STATE_CLOSED;
} else if (submenuAction == MENU_STATE_KEY_LEAVE_SUBMENU) {
if (LockLooper()) {
BMenuItem* temp = fSelected;
// close the submenu:
_SelectItem(NULL);
// but reselect the item itself for user:
_SelectItem(temp, false);
UnlockLooper();
}
// cancel key-nav state
fState = MENU_STATE_TRACKING;
} else
fState = MENU_STATE_TRACKING;
}
}
if (!LockLooper())
break;
} else if ((item = _HitTestItems(location, B_ORIGIN)) != NULL) {
_UpdateStateOpenSelect(item, location, navAreaRectAbove,
navAreaRectBelow, selectedTime, navigationAreaTime);
releasedOnce = true;
} else if (_OverSuper(screenLocation)
&& fSuper->fState != MENU_STATE_KEY_TO_SUBMENU) {
fState = MENU_STATE_TRACKING;
UnlockLooper();
break;
} else if (fState == MENU_STATE_KEY_LEAVE_SUBMENU) {
UnlockLooper();
break;
} else if (fSuper == NULL
|| fSuper->fState != MENU_STATE_KEY_TO_SUBMENU) {
// Mouse pointer outside menu:
// If there's no other submenu opened,
// deselect the current selected item
if (fSelected != NULL
&& (fSelected->Submenu() == NULL
|| fSelected->Submenu()->Window() == NULL)) {
_SelectItem(NULL);
fState = MENU_STATE_TRACKING;
}
if (fSuper != NULL) {
// Give supermenu the chance to continue tracking
*action = fState;
UnlockLooper();
return NULL;
}
}
UnlockLooper();
if (releasedOnce)
_UpdateStateClose(item, location, buttons);
if (fState != MENU_STATE_CLOSED) {
bigtime_t snoozeAmount = 50000;
BPoint newLocation = location;
uint32 newButtons = buttons;
// If user doesn't move the mouse, loop here,
// so we don't interfere with keyboard menu navigation
do {
snooze(snoozeAmount);
if (!LockLooper())
break;
GetMouse(&newLocation, &newButtons, true);
UnlockLooper();
} while (newLocation == location && newButtons == buttons
&& !(item != NULL && item->Submenu() != NULL
&& item->Submenu()->Window() == NULL)
&& fState == MENU_STATE_TRACKING);
if (newLocation != location || newButtons != buttons) {
if (!releasedOnce && newButtons == 0 && buttons != 0)
releasedOnce = true;
location = newLocation;
buttons = newButtons;
}
if (releasedOnce)
_UpdateStateClose(item, location, buttons);
}
}
if (action != NULL)
*action = fState;
// keyboard Enter will set this
if (fChosenItem != NULL)
item = fChosenItem;
else if (fSelected == NULL) {
// needed to cover (rare) mouse/ESC combination
item = NULL;
}
if (fSelected != NULL && LockLooper()) {
_SelectItem(NULL);
UnlockLooper();
}
// delete the menu window recycled for all the child menus
_DeleteMenuWindow();
return item;
}
void
BMenu::_UpdateNavigationArea(BPoint position, BRect& navAreaRectAbove,
BRect& navAreaRectBelow)
{
#define NAV_AREA_THRESHOLD 8
// The navigation area is a region in which mouse-overs won't select
// the item under the cursor. This makes it easier to navigate to
// submenus, as the cursor can be moved to submenu items directly instead
// of having to move it horizontally into the submenu first. The concept
// is illustrated below:
//
// +-------+----+---------+
// | | /| |
// | | /*| |
// |[2]--> | /**| |
// | |/[4]| |
// |------------| |
// | [1] | [6] |
// |------------| |
// | |\[5]| |
// |[3]--> | \**| |
// | | \*| |
// | | \| |
// | +----|---------+
// | |
// +------------+
//
// [1] Selected item, cursor position ('position')
// [2] Upper navigation area rectangle ('navAreaRectAbove')
// [3] Lower navigation area rectangle ('navAreaRectBelow')
// [4] Upper navigation area
// [5] Lower navigation area
// [6] Submenu
//
// The rectangles are used to calculate if the cursor is in the actual
// navigation area (see _UpdateStateOpenSelect()).
if (fSelected == NULL)
return;
BMenu* submenu = fSelected->Submenu();
if (submenu != NULL) {
BRect menuBounds = ConvertToScreen(Bounds());
BRect submenuBounds;
if (fSelected->Submenu()->LockLooper()) {
submenuBounds = fSelected->Submenu()->ConvertToScreen(
fSelected->Submenu()->Bounds());
fSelected->Submenu()->UnlockLooper();
}
if (menuBounds.left < submenuBounds.left) {
navAreaRectAbove.Set(position.x + NAV_AREA_THRESHOLD,
submenuBounds.top, menuBounds.right,
position.y);
navAreaRectBelow.Set(position.x + NAV_AREA_THRESHOLD,
position.y, menuBounds.right,
submenuBounds.bottom);
} else {
navAreaRectAbove.Set(menuBounds.left,
submenuBounds.top, position.x - NAV_AREA_THRESHOLD,
position.y);
navAreaRectBelow.Set(menuBounds.left,
position.y, position.x - NAV_AREA_THRESHOLD,
submenuBounds.bottom);
}
} else {
navAreaRectAbove = BRect();
navAreaRectBelow = BRect();
}
}
void
BMenu::_UpdateStateOpenSelect(BMenuItem* item, BPoint position,
BRect& navAreaRectAbove, BRect& navAreaRectBelow, bigtime_t& selectedTime,
bigtime_t& navigationAreaTime)
{
if (fState == MENU_STATE_CLOSED)
return;
if (item != fSelected) {
if (navigationAreaTime == 0)
navigationAreaTime = system_time();
position = ConvertToScreen(position);
bool inNavAreaRectAbove = navAreaRectAbove.Contains(position);
bool inNavAreaRectBelow = navAreaRectBelow.Contains(position);
if (fSelected == NULL
|| (!inNavAreaRectAbove && !inNavAreaRectBelow)) {
_SelectItem(item, false);
navAreaRectAbove = BRect();
navAreaRectBelow = BRect();
selectedTime = system_time();
navigationAreaTime = 0;
return;
}
bool isLeft = ConvertFromScreen(navAreaRectAbove).left == 0;
BPoint p1, p2;
if (inNavAreaRectAbove) {
if (!isLeft) {
p1 = navAreaRectAbove.LeftBottom();
p2 = navAreaRectAbove.RightTop();
} else {
p2 = navAreaRectAbove.RightBottom();
p1 = navAreaRectAbove.LeftTop();
}
} else {
if (!isLeft) {
p2 = navAreaRectBelow.LeftTop();
p1 = navAreaRectBelow.RightBottom();
} else {
p1 = navAreaRectBelow.RightTop();
p2 = navAreaRectBelow.LeftBottom();
}
}
bool inNavArea =
(p1.y - p2.y) * position.x + (p2.x - p1.x) * position.y
+ (p1.x - p2.x) * p1.y + (p2.y - p1.y) * p1.x >= 0;
bigtime_t systime = system_time();
if (!inNavArea || (navigationAreaTime > 0 && systime -
navigationAreaTime > kNavigationAreaTimeout)) {
// Don't delay opening of submenu if the user had
// to wait for the navigation area timeout anyway
_SelectItem(item, inNavArea);
if (inNavArea) {
_UpdateNavigationArea(position, navAreaRectAbove,
navAreaRectBelow);
} else {
navAreaRectAbove = BRect();
navAreaRectBelow = BRect();
}
selectedTime = system_time();
navigationAreaTime = 0;
}
} else if (fSelected->Submenu() != NULL &&
system_time() - selectedTime > kOpenSubmenuDelay) {
_SelectItem(fSelected, true);
if (!navAreaRectAbove.IsValid() && !navAreaRectBelow.IsValid()) {
position = ConvertToScreen(position);
_UpdateNavigationArea(position, navAreaRectAbove,
navAreaRectBelow);
}
}
if (fState != MENU_STATE_TRACKING)
fState = MENU_STATE_TRACKING;
}
void
BMenu::_UpdateStateClose(BMenuItem* item, const BPoint& where,
const uint32& buttons)
{
if (fState == MENU_STATE_CLOSED)
return;
if (buttons != 0 && _IsStickyMode()) {
if (item == NULL) {
if (item != fSelected && LockLooper()) {
_SelectItem(item, false);
UnlockLooper();
}
fState = MENU_STATE_CLOSED;
} else
_SetStickyMode(false);
} else if (buttons == 0 && !_IsStickyMode()) {
if (fExtraRect != NULL && fExtraRect->Contains(where)) {
_SetStickyMode(true);
fExtraRect = NULL;
// Setting this to NULL will prevent this code
// to be executed next time
} else {
if (item != fSelected && LockLooper()) {
_SelectItem(item, false);
UnlockLooper();
}
fState = MENU_STATE_CLOSED;
}
}
}
bool
BMenu::_AddItem(BMenuItem* item, int32 index)
{
ASSERT(item != NULL);
if (index < 0 || index > fItems.CountItems())
return false;
if (item->IsMarked())
_ItemMarked(item);
if (!fItems.AddItem(item, index))
return false;
// install the item on the supermenu's window
// or onto our window, if we are a root menu
BWindow* window = NULL;
if (Superitem() != NULL)
window = Superitem()->fWindow;
else
window = Window();
if (window != NULL)
item->Install(window);
item->SetSuper(this);
return true;
}
bool
BMenu::_RemoveItems(int32 index, int32 count, BMenuItem* item,
bool deleteItems)
{
bool success = false;
bool invalidateLayout = false;
bool locked = LockLooper();
BWindow* window = Window();
// The plan is simple: If we're given a BMenuItem directly, we use it
// and ignore index and count. Otherwise, we use them instead.
if (item != NULL) {
if (fItems.RemoveItem(item)) {
if (item == fSelected && window != NULL)
_SelectItem(NULL);
item->Uninstall();
item->SetSuper(NULL);
if (deleteItems)
delete item;
success = invalidateLayout = true;
}
} else {
// We iterate backwards because it's simpler
int32 i = std::min(index + count - 1, fItems.CountItems() - 1);
// NOTE: the range check for "index" is done after
// calculating the last index to be removed, so
// that the range is not "shifted" unintentionally
index = std::max((int32)0, index);
for (; i >= index; i--) {
item = static_cast(fItems.ItemAt(i));
if (item != NULL) {
if (fItems.RemoveItem(i)) {
if (item == fSelected && window != NULL)
_SelectItem(NULL);
item->Uninstall();
item->SetSuper(NULL);
if (deleteItems)
delete item;
success = true;
invalidateLayout = true;
} else {
// operation not entirely successful
success = false;
break;
}
}
}
}
if (invalidateLayout) {
InvalidateLayout();
if (locked && window != NULL) {
_LayoutItems(0);
_UpdateWindowViewSize(false);
Invalidate();
}
}
if (locked)
UnlockLooper();
return success;
}
bool
BMenu::_RelayoutIfNeeded()
{
if (!fUseCachedMenuLayout) {
fUseCachedMenuLayout = true;
_CacheFontInfo();
_LayoutItems(0);
_UpdateWindowViewSize(false);
return true;
}
return false;
}
void
BMenu::_LayoutItems(int32 index)
{
_CalcTriggers();
float width;
float height;
_ComputeLayout(index, fResizeToFit, true, &width, &height);
if (fResizeToFit)
ResizeTo(width, height);
}
BSize
BMenu::_ValidatePreferredSize()
{
if (!fLayoutData->preferred.IsWidthSet() || ResizingMode()
!= fLayoutData->lastResizingMode) {
_ComputeLayout(0, true, false, NULL, NULL);
ResetLayoutInvalidation();
}
return fLayoutData->preferred;
}
void
BMenu::_ComputeLayout(int32 index, bool bestFit, bool moveItems,
float* _width, float* _height)
{
// TODO: Take "bestFit", "moveItems", "index" into account,
// Recalculate only the needed items,
// not the whole layout every time
fLayoutData->lastResizingMode = ResizingMode();
BRect frame;
switch (fLayout) {
case B_ITEMS_IN_COLUMN:
{
BRect parentFrame;
BRect* overrideFrame = NULL;
if (dynamic_cast<_BMCMenuBar_*>(Supermenu()) != NULL) {
// When the menu is modified while it's open, we get here in a
// situation where trying to lock the looper would deadlock
// (the window is locked waiting for the menu to terminate).
// In that case, just give up on getting the supermenu bounds
// and keep the menu at the current width and position.
if (Supermenu()->LockLooperWithTimeout(0) == B_OK) {
parentFrame = Supermenu()->Bounds();
Supermenu()->UnlockLooper();
overrideFrame = &parentFrame;
}
}
_ComputeColumnLayout(index, bestFit, moveItems, overrideFrame,
frame);
break;
}
case B_ITEMS_IN_ROW:
_ComputeRowLayout(index, bestFit, moveItems, frame);
break;
case B_ITEMS_IN_MATRIX:
_ComputeMatrixLayout(frame);
break;
}
// change width depending on resize mode
BSize size;
if ((ResizingMode() & B_FOLLOW_LEFT_RIGHT) == B_FOLLOW_LEFT_RIGHT) {
if (dynamic_cast<_BMCMenuBar_*>(this) != NULL)
size.width = Bounds().Width() - fPad.right;
else if (Parent() != NULL)
size.width = Parent()->Frame().Width();
else if (Window() != NULL)
size.width = Window()->Frame().Width();
else
size.width = Bounds().Width();
} else
size.width = frame.Width();
size.height = frame.Height();
if (_width)
*_width = size.width;
if (_height)
*_height = size.height;
if (bestFit)
fLayoutData->preferred = size;
if (moveItems)
fUseCachedMenuLayout = true;
}
void
BMenu::_ComputeColumnLayout(int32 index, bool bestFit, bool moveItems,
BRect* overrideFrame, BRect& frame)
{
bool command = false;
bool control = false;
bool shift = false;
bool option = false;
bool submenu = false;
if (index > 0)
frame = ItemAt(index - 1)->Frame();
else if (overrideFrame != NULL)
frame.Set(0, 0, overrideFrame->right, -1);
else
frame.Set(0, 0, 0, -1);
BFont font;
GetFont(&font);
// Loop over all items to set their top, bottom and left coordinates,
// all while computing the width of the menu
for (; index < fItems.CountItems(); index++) {
BMenuItem* item = ItemAt(index);
float width;
float height;
item->GetContentSize(&width, &height);
if (item->fModifiers && item->fShortcutChar) {
width += font.Size();
if ((item->fModifiers & B_COMMAND_KEY) != 0)
command = true;
if ((item->fModifiers & B_CONTROL_KEY) != 0)
control = true;
if ((item->fModifiers & B_SHIFT_KEY) != 0)
shift = true;
if ((item->fModifiers & B_OPTION_KEY) != 0)
option = true;
}
item->fBounds.left = 0.0f;
item->fBounds.top = frame.bottom + 1.0f;
item->fBounds.bottom = item->fBounds.top + height + fPad.top
+ fPad.bottom;
if (item->fSubmenu != NULL)
submenu = true;
frame.right = std::max(frame.right, width + fPad.left + fPad.right);
frame.bottom = item->fBounds.bottom;
}
// Compute the extra space needed for shortcuts and submenus
if (command) {
frame.right
+= BPrivate::MenuPrivate::MenuItemCommand()->Bounds().Width() + 1;
}
if (control) {
frame.right
+= BPrivate::MenuPrivate::MenuItemControl()->Bounds().Width() + 1;
}
if (option) {
frame.right
+= BPrivate::MenuPrivate::MenuItemOption()->Bounds().Width() + 1;
}
if (shift) {
frame.right
+= BPrivate::MenuPrivate::MenuItemShift()->Bounds().Width() + 1;
}
if (submenu) {
frame.right += ItemAt(0)->Frame().Height() / 2;
fHasSubmenus = true;
} else {
fHasSubmenus = false;
}
if (fMaxContentWidth > 0)
frame.right = std::min(frame.right, fMaxContentWidth);
frame.top = 0;
frame.right = ceilf(frame.right);
// Finally update the "right" coordinate of all items
if (moveItems) {
for (int32 i = 0; i < fItems.CountItems(); i++)
ItemAt(i)->fBounds.right = frame.right;
}
}
void
BMenu::_ComputeRowLayout(int32 index, bool bestFit, bool moveItems,
BRect& frame)
{
font_height fh;
GetFontHeight(&fh);
frame.Set(0.0f, 0.0f, 0.0f, ceilf(fh.ascent + fh.descent + fPad.top
+ fPad.bottom));
for (int32 i = 0; i < fItems.CountItems(); i++) {
BMenuItem* item = ItemAt(i);
float width, height;
item->GetContentSize(&width, &height);
item->fBounds.left = frame.right;
item->fBounds.top = 0.0f;
item->fBounds.right = item->fBounds.left + width + fPad.left
+ fPad.right;
frame.right = item->Frame().right + 1.0f;
frame.bottom = std::max(frame.bottom, height + fPad.top + fPad.bottom);
}
if (moveItems) {
for (int32 i = 0; i < fItems.CountItems(); i++)
ItemAt(i)->fBounds.bottom = frame.bottom;
}
if (bestFit)
frame.right = ceilf(frame.right);
else
frame.right = Bounds().right;
}
void
BMenu::_ComputeMatrixLayout(BRect &frame)
{
frame.Set(0, 0, 0, 0);
for (int32 i = 0; i < CountItems(); i++) {
BMenuItem* item = ItemAt(i);
if (item != NULL) {
frame.left = std::min(frame.left, item->Frame().left);
frame.right = std::max(frame.right, item->Frame().right);
frame.top = std::min(frame.top, item->Frame().top);
frame.bottom = std::max(frame.bottom, item->Frame().bottom);
}
}
}
void
BMenu::LayoutInvalidated(bool descendants)
{
fUseCachedMenuLayout = false;
fLayoutData->preferred.Set(B_SIZE_UNSET, B_SIZE_UNSET);
}
// Assumes the SuperMenu to be locked (due to calling ConvertToScreen())
BPoint
BMenu::ScreenLocation()
{
BMenu* superMenu = Supermenu();
BMenuItem* superItem = Superitem();
if (superMenu == NULL || superItem == NULL) {
debugger("BMenu can't determine where to draw."
"Override BMenu::ScreenLocation() to determine location.");
}
BPoint point;
if (superMenu->Layout() == B_ITEMS_IN_COLUMN)
point = superItem->Frame().RightTop() + BPoint(1.0f, 1.0f);
else
point = superItem->Frame().LeftBottom() + BPoint(1.0f, 1.0f);
superMenu->ConvertToScreen(&point);
return point;
}
BRect
BMenu::_CalcFrame(BPoint where, bool* scrollOn)
{
// TODO: Improve me
BRect bounds = Bounds();
BRect frame = bounds.OffsetToCopy(where);
BScreen screen(Window());
BRect screenFrame = screen.Frame();
BMenu* superMenu = Supermenu();
BMenuItem* superItem = Superitem();
// Reset frame shifted state since this menu is being redrawn
fExtraMenuData->frameShiftedLeft = false;
// TODO: Horrible hack:
// When added to a BMenuField, a BPopUpMenu is the child of
// a _BMCMenuBar_ to "fake" the menu hierarchy
bool inMenuField = dynamic_cast<_BMCMenuBar_*>(superMenu) != NULL;
// Offset the menu field menu window left by the width of the checkmark
// so that the text when the menu is closed lines up with the text when
// the menu is open.
if (inMenuField)
frame.OffsetBy(-8.0f, 0.0f);
if (superMenu == NULL || superItem == NULL || inMenuField) {
// just move the window on screen
if (frame.bottom > screenFrame.bottom)
frame.OffsetBy(0, screenFrame.bottom - frame.bottom);
else if (frame.top < screenFrame.top)
frame.OffsetBy(0, -frame.top);
if (frame.right > screenFrame.right) {
frame.OffsetBy(screenFrame.right - frame.right, 0);
fExtraMenuData->frameShiftedLeft = true;
}
else if (frame.left < screenFrame.left)
frame.OffsetBy(-frame.left, 0);
} else if (superMenu->Layout() == B_ITEMS_IN_COLUMN) {
if (frame.right > screenFrame.right
|| superMenu->fExtraMenuData->frameShiftedLeft) {
frame.OffsetBy(-superItem->Frame().Width() - frame.Width() - 2, 0);
fExtraMenuData->frameShiftedLeft = true;
}
if (frame.left < 0)
frame.OffsetBy(-frame.left + 6, 0);
if (frame.bottom > screenFrame.bottom)
frame.OffsetBy(0, screenFrame.bottom - frame.bottom);
} else {
if (frame.bottom > screenFrame.bottom) {
float spaceBelow = screenFrame.bottom - frame.top;
float spaceOver = frame.top - screenFrame.top
- superItem->Frame().Height();
if (spaceOver > spaceBelow) {
frame.OffsetBy(0, -superItem->Frame().Height()
- frame.Height() - 3);
}
}
if (frame.right > screenFrame.right)
frame.OffsetBy(screenFrame.right - frame.right, 0);
}
if (scrollOn != NULL) {
// basically, if this returns false, it means
// that the menu frame won't fit completely inside the screen
// TODO: Scrolling will currently only work up/down,
// not left/right
*scrollOn = screenFrame.top > frame.top
|| screenFrame.bottom < frame.bottom;
}
return frame;
}
void
BMenu::DrawItems(BRect updateRect)
{
int32 itemCount = fItems.CountItems();
for (int32 i = 0; i < itemCount; i++) {
BMenuItem* item = ItemAt(i);
if (item->Frame().Intersects(updateRect))
item->Draw();
}
}
int
BMenu::_State(BMenuItem** item) const
{
if (fState == MENU_STATE_TRACKING || fState == MENU_STATE_CLOSED)
return fState;
if (fSelected != NULL && fSelected->Submenu() != NULL)
return fSelected->Submenu()->_State(item);
return fState;
}
void
BMenu::_InvokeItem(BMenuItem* item, bool now)
{
if (!item->IsEnabled())
return;
// Do the "selected" animation
// TODO: Doesn't work. This is supposed to highlight
// and dehighlight the item, works on beos but not on haiku.
if (!item->Submenu() && LockLooper()) {
snooze(50000);
item->Select(true);
Window()->UpdateIfNeeded();
snooze(50000);
item->Select(false);
Window()->UpdateIfNeeded();
snooze(50000);
item->Select(true);
Window()->UpdateIfNeeded();
snooze(50000);
item->Select(false);
Window()->UpdateIfNeeded();
UnlockLooper();
}
// Lock the root menu window before calling BMenuItem::Invoke()
BMenu* parent = this;
BMenu* rootMenu = NULL;
do {
rootMenu = parent;
parent = rootMenu->Supermenu();
} while (parent != NULL);
if (rootMenu->LockLooper()) {
item->Invoke();
rootMenu->UnlockLooper();
}
}
bool
BMenu::_OverSuper(BPoint location)
{
if (!Supermenu())
return false;
return fSuperbounds.Contains(location);
}
bool
BMenu::_OverSubmenu(BMenuItem* item, BPoint loc)
{
if (item == NULL)
return false;
BMenu* subMenu = item->Submenu();
if (subMenu == NULL || subMenu->Window() == NULL)
return false;
// assume that loc is in screen coordinates
if (subMenu->Window()->Frame().Contains(loc))
return true;
return subMenu->_OverSubmenu(subMenu->fSelected, loc);
}
BMenuWindow*
BMenu::_MenuWindow()
{
#if USE_CACHED_MENUWINDOW
if (fCachedMenuWindow == NULL) {
char windowName[64];
snprintf(windowName, 64, "%s cached menu", Name());
fCachedMenuWindow = new (nothrow) BMenuWindow(windowName);
}
#endif
return fCachedMenuWindow;
}
void
BMenu::_DeleteMenuWindow()
{
if (fCachedMenuWindow != NULL) {
fCachedMenuWindow->Lock();
fCachedMenuWindow->Quit();
fCachedMenuWindow = NULL;
}
}
BMenuItem*
BMenu::_HitTestItems(BPoint where, BPoint slop) const
{
// TODO: Take "slop" into account ?
// if the point doesn't lie within the menu's
// bounds, bail out immediately
if (!Bounds().Contains(where))
return NULL;
int32 itemCount = CountItems();
for (int32 i = 0; i < itemCount; i++) {
BMenuItem* item = ItemAt(i);
if (item->Frame().Contains(where)
&& dynamic_cast(item) == NULL) {
return item;
}
}
return NULL;
}
BRect
BMenu::_Superbounds() const
{
return fSuperbounds;
}
void
BMenu::_CacheFontInfo()
{
font_height fh;
GetFontHeight(&fh);
fAscent = fh.ascent;
fDescent = fh.descent;
fFontHeight = ceilf(fh.ascent + fh.descent + fh.leading);
}
void
BMenu::_ItemMarked(BMenuItem* item)
{
if (IsRadioMode()) {
for (int32 i = 0; i < CountItems(); i++) {
if (ItemAt(i) != item)
ItemAt(i)->SetMarked(false);
}
}
if (IsLabelFromMarked() && Superitem() != NULL)
Superitem()->SetLabel(item->Label());
}
void
BMenu::_Install(BWindow* target)
{
for (int32 i = 0; i < CountItems(); i++)
ItemAt(i)->Install(target);
}
void
BMenu::_Uninstall()
{
for (int32 i = 0; i < CountItems(); i++)
ItemAt(i)->Uninstall();
}
void
BMenu::_SelectItem(BMenuItem* item, bool showSubmenu, bool selectFirstItem,
bool keyDown)
{
// Avoid deselecting and then reselecting the same item
// which would cause flickering
if (item != fSelected) {
if (fSelected != NULL) {
fSelected->Select(false);
BMenu* subMenu = fSelected->Submenu();
if (subMenu != NULL && subMenu->Window() != NULL)
subMenu->_Hide();
}
fSelected = item;
if (fSelected != NULL)
fSelected->Select(true);
}
if (fSelected != NULL && showSubmenu) {
BMenu* subMenu = fSelected->Submenu();
if (subMenu != NULL && subMenu->Window() == NULL) {
if (!subMenu->_Show(selectFirstItem, keyDown)) {
// something went wrong, deselect the item
fSelected->Select(false);
fSelected = NULL;
}
}
}
}
bool
BMenu::_SelectNextItem(BMenuItem* item, bool forward)
{
if (CountItems() == 0) // cannot select next item in an empty menu
return false;
BMenuItem* nextItem = _NextItem(item, forward);
if (nextItem == NULL)
return false;
_SelectItem(nextItem, dynamic_cast(this) != NULL);
if (LockLooper()) {
be_app->ObscureCursor();
UnlockLooper();
}
return true;
}
BMenuItem*
BMenu::_NextItem(BMenuItem* item, bool forward) const
{
const int32 numItems = fItems.CountItems();
if (numItems == 0)
return NULL;
int32 index = fItems.IndexOf(item);
int32 loopCount = numItems;
while (--loopCount) {
// Cycle through menu items in the given direction...
if (forward)
index++;
else
index--;
// ... wrap around...
if (index < 0)
index = numItems - 1;
else if (index >= numItems)
index = 0;
// ... and return the first suitable item found.
BMenuItem* nextItem = ItemAt(index);
if (nextItem->IsEnabled())
return nextItem;
}
// If no other suitable item was found, return NULL.
return NULL;
}
void
BMenu::_SetStickyMode(bool sticky)
{
if (fStickyMode == sticky)
return;
fStickyMode = sticky;
if (fSuper != NULL) {
// propagate the status to the super menu
fSuper->_SetStickyMode(sticky);
} else {
// TODO: Ugly hack, but it needs to be done in this method
BMenuBar* menuBar = dynamic_cast(this);
if (sticky && menuBar != NULL && menuBar->LockLooper()) {
// If we are switching to sticky mode,
// steal the focus from the current focus view
// (needed to handle keyboard navigation)
menuBar->_StealFocus();
menuBar->UnlockLooper();
}
}
}
bool
BMenu::_IsStickyMode() const
{
return fStickyMode;
}
void
BMenu::_GetShiftKey(uint32 &value) const
{
// TODO: Move into init_interface_kit().
// Currently we can't do that, as get_modifier_key() blocks forever
// when called on input_server initialization, since it tries
// to send a synchronous message to itself (input_server is
// a BApplication)
if (get_modifier_key(B_LEFT_SHIFT_KEY, &value) != B_OK)
value = 0x4b;
}
void
BMenu::_GetControlKey(uint32 &value) const
{
// TODO: Move into init_interface_kit().
// Currently we can't do that, as get_modifier_key() blocks forever
// when called on input_server initialization, since it tries
// to send a synchronous message to itself (input_server is
// a BApplication)
if (get_modifier_key(B_LEFT_CONTROL_KEY, &value) != B_OK)
value = 0x5c;
}
void
BMenu::_GetCommandKey(uint32 &value) const
{
// TODO: Move into init_interface_kit().
// Currently we can't do that, as get_modifier_key() blocks forever
// when called on input_server initialization, since it tries
// to send a synchronous message to itself (input_server is
// a BApplication)
if (get_modifier_key(B_LEFT_COMMAND_KEY, &value) != B_OK)
value = 0x66;
}
void
BMenu::_GetOptionKey(uint32 &value) const
{
// TODO: Move into init_interface_kit().
// Currently we can't do that, as get_modifier_key() blocks forever
// when called on input_server initialization, since it tries
// to send a synchronous message to itself (input_server is
// a BApplication)
if (get_modifier_key(B_LEFT_OPTION_KEY, &value) != B_OK)
value = 0x5d;
}
void
BMenu::_GetMenuKey(uint32 &value) const
{
// TODO: Move into init_interface_kit().
// Currently we can't do that, as get_modifier_key() blocks forever
// when called on input_server initialization, since it tries
// to send a synchronous message to itself (input_server is
// a BApplication)
if (get_modifier_key(B_MENU_KEY, &value) != B_OK)
value = 0x68;
}
void
BMenu::_CalcTriggers()
{
BPrivate::TriggerList triggerList;
// Gathers the existing triggers set by the user
for (int32 i = 0; i < CountItems(); i++) {
char trigger = ItemAt(i)->Trigger();
if (trigger != 0)
triggerList.AddTrigger(trigger);
}
// Set triggers for items which don't have one yet
for (int32 i = 0; i < CountItems(); i++) {
BMenuItem* item = ItemAt(i);
if (item->Trigger() == 0) {
uint32 trigger;
int32 index;
if (_ChooseTrigger(item->Label(), index, trigger, triggerList))
item->SetAutomaticTrigger(index, trigger);
}
}
}
bool
BMenu::_ChooseTrigger(const char* title, int32& index, uint32& trigger,
BPrivate::TriggerList& triggers)
{
if (title == NULL)
return false;
index = 0;
uint32 c;
const char* nextCharacter, *character;
// two runs: first we look out for alphanumeric ASCII characters
nextCharacter = title;
character = nextCharacter;
while ((c = BUnicodeChar::FromUTF8(&nextCharacter)) != 0) {
if (!(c < 128 && BUnicodeChar::IsAlNum(c)) || triggers.HasTrigger(c)) {
character = nextCharacter;
continue;
}
trigger = BUnicodeChar::ToLower(c);
index = (int32)(character - title);
return triggers.AddTrigger(c);
}
// then, if we still haven't found something, we accept anything
nextCharacter = title;
character = nextCharacter;
while ((c = BUnicodeChar::FromUTF8(&nextCharacter)) != 0) {
if (BUnicodeChar::IsSpace(c) || triggers.HasTrigger(c)) {
character = nextCharacter;
continue;
}
trigger = BUnicodeChar::ToLower(c);
index = (int32)(character - title);
return triggers.AddTrigger(c);
}
return false;
}
void
BMenu::_UpdateWindowViewSize(const bool &move)
{
BMenuWindow* window = static_cast(Window());
if (window == NULL)
return;
if (dynamic_cast(this) != NULL)
return;
if (!fResizeToFit)
return;
bool scroll = false;
const BPoint screenLocation = move ? ScreenLocation()
: window->Frame().LeftTop();
BRect frame = _CalcFrame(screenLocation, &scroll);
ResizeTo(frame.Width(), frame.Height());
if (fItems.CountItems() > 0) {
if (!scroll) {
if (fLayout == B_ITEMS_IN_COLUMN)
window->DetachScrollers();
window->ResizeTo(Bounds().Width(), Bounds().Height());
} else {
// Resize the window to fit the screen without overflowing the
// frame, and attach scrollers to our cached BMenuWindow.
BScreen screen(window);
frame = frame & screen.Frame();
window->ResizeTo(Bounds().Width(), frame.Height());
// we currently only support scrolling for B_ITEMS_IN_COLUMN
if (fLayout == B_ITEMS_IN_COLUMN) {
window->AttachScrollers();
BMenuItem* selectedItem = FindMarked();
if (selectedItem != NULL) {
// scroll to the selected item
if (Supermenu() == NULL) {
window->TryScrollTo(selectedItem->Frame().top);
} else {
BPoint point = selectedItem->Frame().LeftTop();
BPoint superPoint = Superitem()->Frame().LeftTop();
Supermenu()->ConvertToScreen(&superPoint);
ConvertToScreen(&point);
window->TryScrollTo(point.y - superPoint.y);
}
}
}
}
} else {
_CacheFontInfo();
window->ResizeTo(StringWidth(BPrivate::kEmptyMenuLabel)
+ fPad.left + fPad.right,
fFontHeight + fPad.top + fPad.bottom);
}
if (move)
window->MoveTo(frame.LeftTop());
}
bool
BMenu::_AddDynamicItems(bool keyDown)
{
bool addAborted = false;
if (AddDynamicItem(B_INITIAL_ADD)) {
BMenuItem* superItem = Superitem();
BMenu* superMenu = Supermenu();
do {
if (superMenu != NULL
&& !superMenu->_OkToProceed(superItem, keyDown)) {
AddDynamicItem(B_ABORT);
addAborted = true;
break;
}
} while (AddDynamicItem(B_PROCESSING));
}
return addAborted;
}
bool
BMenu::_OkToProceed(BMenuItem* item, bool keyDown)
{
BPoint where;
uint32 buttons;
GetMouse(&where, &buttons, false);
bool stickyMode = _IsStickyMode();
// Quit if user clicks the mouse button in sticky mode
// or releases the mouse button in nonsticky mode
// or moves the pointer over another item
// TODO: I added the check for BMenuBar to solve a problem with Deskbar.
// BeOS seems to do something similar. This could also be a bug in
// Deskbar, though.
if ((buttons != 0 && stickyMode)
|| ((dynamic_cast(this) == NULL
&& (buttons == 0 && !stickyMode))
|| ((_HitTestItems(where) != item) && !keyDown))) {
return false;
}
return true;
}
bool
BMenu::_CustomTrackingWantsToQuit()
{
if (fExtraMenuData != NULL && fExtraMenuData->trackingHook != NULL
&& fExtraMenuData->trackingState != NULL) {
return fExtraMenuData->trackingHook(this,
fExtraMenuData->trackingState);
}
return false;
}
void
BMenu::_QuitTracking(bool onlyThis)
{
_SelectItem(NULL);
if (BMenuBar* menuBar = dynamic_cast(this))
menuBar->_RestoreFocus();
fState = MENU_STATE_CLOSED;
if (!onlyThis) {
// Close the whole menu hierarchy
if (Supermenu() != NULL)
Supermenu()->fState = MENU_STATE_CLOSED;
if (_IsStickyMode())
_SetStickyMode(false);
if (LockLooper()) {
be_app->ShowCursor();
UnlockLooper();
}
}
_Hide();
}
// #pragma mark - menu_info functions
// TODO: Maybe the following two methods would fit better into
// InterfaceDefs.cpp
// In R5, they do all the work client side, we let the app_server handle the
// details.
status_t
set_menu_info(menu_info* info)
{
if (!info)
return B_BAD_VALUE;
BPrivate::AppServerLink link;
link.StartMessage(AS_SET_MENU_INFO);
link.Attach(*info);
status_t status = B_ERROR;
if (link.FlushWithReply(status) == B_OK && status == B_OK)
BMenu::sMenuInfo = *info;
// Update also the local copy, in case anyone relies on it
return status;
}
status_t
get_menu_info(menu_info* info)
{
if (!info)
return B_BAD_VALUE;
BPrivate::AppServerLink link;
link.StartMessage(AS_GET_MENU_INFO);
status_t status = B_ERROR;
if (link.FlushWithReply(status) == B_OK && status == B_OK)
link.Read(info);
return status;
}
extern "C" void
B_IF_GCC_2(InvalidateLayout__5BMenub,_ZN5BMenu16InvalidateLayoutEb)(
BMenu* menu, bool descendants)
{
menu->InvalidateLayout();
}