/* * 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; } }; typedef int (*compare_func)(const BMenuItem*, const BMenuItem*); struct MenuItemComparator { MenuItemComparator(compare_func compareFunc) : fCompareFunc(compareFunc) { } bool operator () (const BMenuItem* item1, const BMenuItem* item2) { return fCompareFunc(item1, item2) < 0; } private: compare_func fCompareFunc; }; } // 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*)) { BMenuItem** begin = (BMenuItem**)fItems.Items(); BMenuItem** end = begin + fItems.CountItems(); std::stable_sort(begin, end, BPrivate::MenuItemComparator(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(); }