Files
haiku-beta6/src/kits/interface/Menu.cpp
T
Stephan Aßmus 2bcccf5a74 Patch in part by "yourpalal":
* Set the MENU_STATE_CLOSED state upon BMenu initialization.
 * When drawing the label, use the parent bounds, including
   item margins to truncate the label, when the parent menu is
   closed.

Thanks a lot for the patch!

Changes by myself:
 * Cache the MenuPrivate instance in BMenuItem::DrawContent().
 * Use Window()->UpdateIfNeeded() to animate the flashing invoked menu item,
   which makes a lot more sense, the comment about it working in BeOS is
   probably due to item->Select() probably drawing outside of an update cycle.
 * The trigger invokation in keyboard handling didn't break out of the loop
   after invoking an item. Probably didn't matter since triggers are hopefully
   not assigned to more than one menu item. :-)
 * Use the same trick as BMenuBar to avoid interfering with keyboard navigation
   in the BMenu::_Track() hook.


git-svn-id: file:///srv/svn/repos/haiku/haiku/trunk@35739 a95241bf-73f2-0310-859d-f6bbb57e9c96
2010-03-03 17:43:31 +00:00

2830 lines
58 KiB
C++

/*
* Copyright 2001-2009, Haiku Inc. All rights reserved.
* Distributed under the terms of the MIT license.
*
* Authors:
* Marc Flerackers ([email protected])
* Stefano Ceccherini ([email protected])
* Rene Gollent ([email protected])
* Stephan Aßmus <[email protected]>
*/
#include <Menu.h>
#include <new>
#include <ctype.h>
#include <string.h>
#include <ControlLook.h>
#include <Debug.h>
#include <File.h>
#include <FindDirectory.h>
#include <Layout.h>
#include <LayoutUtils.h>
#include <MenuBar.h>
#include <MenuItem.h>
#include <Messenger.h>
#include <Path.h>
#include <PropertyInfo.h>
#include <Screen.h>
#include <ScrollBar.h>
#include <Window.h>
#include <AppServerLink.h>
#include <binary_compatibility/Interface.h>
#include <BMCPrivate.h>
#include <MenuPrivate.h>
#include <MenuWindow.h>
#include <ServerProtocol.h>
#include "utf8_functions.h"
#define USE_CACHED_MENUWINDOW 1
using std::nothrow;
using BPrivate::BMenuWindow;
namespace BPrivate {
class TriggerList {
public:
TriggerList() {}
~TriggerList() {}
// TODO: make this work with Unicode characters!
bool HasTrigger(uint32 c)
{ return fList.HasItem((void*)tolower(c)); }
bool AddTrigger(uint32 c)
{ return fList.AddItem((void*)tolower(c)); }
private:
BList fList;
};
class ExtraMenuData {
public:
menu_tracking_hook trackingHook;
void* trackingState;
ExtraMenuData(menu_tracking_hook func, void* state)
{
trackingHook = func;
trackingState = state;
}
};
} // namespace BPrivate
menu_info BMenu::sMenuInfo;
bool BMenu::sAltAsCommandKey;
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."
},
{}
};
const char* BPrivate::kEmptyMenuLabel = "<empty>";
struct BMenu::LayoutData {
BSize preferred;
uint32 lastResizingMode;
};
// #pragma mark -
BMenu::BMenu(const char* name, menu_layout layout)
:
BView(BRect(0, 0, 0, 0), name, 0, B_WILL_DRAW),
fChosenItem(NULL),
fPad(14.0f, 2.0f, 20.0f, 0.0f),
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),
fRedrawAfterSticky(false),
fAttachAborted(false)
{
_InitData(NULL);
}
BMenu::BMenu(const char* name, float width, float height)
:
BView(BRect(0.0f, width, 0.0f, height), 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),
fRedrawAfterSticky(false),
fAttachAborted(false)
{
_InitData(NULL);
}
BMenu::BMenu(BMessage* archive)
:
BView(archive),
fChosenItem(NULL),
fPad(14.0f, 2.0f, 20.0f, 0.0f),
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),
fRedrawAfterSticky(false),
fAttachAborted(false)
{
_InitData(archive);
}
BMenu::~BMenu()
{
_DeleteMenuWindow();
RemoveItems(0, CountItems(), true);
delete fInitMatrixSize;
delete fExtraMenuData;
delete fLayoutData;
}
// #pragma mark -
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;
}
// #pragma mark -
void
BMenu::AttachedToWindow()
{
BView::AttachedToWindow();
// TODO: Move into init_interface_kit().
// Currently we can't do that, as get_key_map() blocks forever
// when called on input_server initialization, since it tries
// to send a synchronous message to itself (input_server is
// a BApplication)
BMenu::sAltAsCommandKey = true;
key_map* keys = NULL;
char* chars = NULL;
get_key_map(&keys, &chars);
if (keys == NULL || keys->left_command_key != 0x5d
|| keys->left_control_key != 0x5c)
BMenu::sAltAsCommandKey = false;
free(chars);
free(keys);
BMenuItem* superItem = Superitem();
BMenu* superMenu = Supermenu();
if (AddDynamicItem(B_INITIAL_ADD)) {
do {
if (superMenu != NULL && !superMenu->_OkToProceed(superItem)) {
AddDynamicItem(B_ABORT);
fAttachAborted = true;
break;
}
} while (AddDynamicItem(B_PROCESSING));
}
if (!fAttachAborted) {
_CacheFontInfo();
_LayoutItems(0);
_UpdateWindowViewSize(false);
}
}
void
BMenu::DetachedFromWindow()
{
BView::DetachedFromWindow();
}
void
BMenu::AllAttached()
{
BView::AllAttached();
}
void
BMenu::AllDetached()
{
BView::AllDetached();
}
// #pragma mark -
void
BMenu::Draw(BRect updateRect)
{
if (_RelayoutIfNeeded()) {
Invalidate();
return;
}
DrawBackground(updateRect);
_DrawItems(updateRect);
}
void
BMenu::MessageReceived(BMessage* msg)
{
switch (msg->what) {
case B_MOUSE_WHEEL_CHANGED:
{
float deltaY = 0;
msg->FindFloat("be:wheel_delta_y", &deltaY);
if (deltaY == 0)
return;
BMenuWindow* window = dynamic_cast<BMenuWindow*>(Window());
if (window == NULL)
return;
float largeStep;
float smallStep;
window->GetSteps(&smallStep, &largeStep);
// pressing the option/command/control key scrolls faster
if (modifiers() & (B_OPTION_KEY | B_COMMAND_KEY | B_CONTROL_KEY))
deltaY *= largeStep;
else
deltaY *= smallStep;
window->TryScrollBy(deltaY);
break;
}
default:
BView::MessageReceived(msg);
break;
}
}
void
BMenu::KeyDown(const char* bytes, int32 numBytes)
{
// TODO: Test how it works on beos and implement it correctly
switch (bytes[0]) {
case B_UP_ARROW:
if (fLayout == B_ITEMS_IN_COLUMN)
_SelectNextItem(fSelected, false);
break;
case B_DOWN_ARROW:
if (fLayout == B_ITEMS_IN_COLUMN)
_SelectNextItem(fSelected, true);
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<BMenuBar*>(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 msgr(Supermenu());
msgr.SendMessage(Window()->CurrentMessage());
} else
Supermenu()->_SelectItem(item, false, false);
}
}
break;
case B_RIGHT_ARROW:
if (fLayout == B_ITEMS_IN_ROW)
_SelectNextItem(fSelected, true);
else {
if (fSelected && fSelected->Submenu()) {
_SelectItem(fSelected, true, true);
} else if (dynamic_cast<BMenuBar*>(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 msgr(Supermenu());
msgr.SendMessage(Window()->CurrentMessage());
}
}
break;
case B_PAGE_UP:
case B_PAGE_DOWN:
{
BMenuWindow* window = dynamic_cast<BMenuWindow*>(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) {
_InvokeItem(fSelected);
_QuitTracking(false);
}
break;
case B_ESCAPE:
_QuitTracking();
break;
default:
{
uint32 trigger = UTF8ToCharCode(&bytes);
for (uint32 i = CountItems(); i-- > 0;) {
BMenuItem* item = ItemAt(i);
if (item->fTriggerIndex < 0 || item->fTrigger != trigger)
continue;
_InvokeItem(item);
break;
}
break;
}
}
}
// #pragma mark -
BSize
BMenu::MinSize()
{
_ValidatePreferredSize();
BSize size = (GetLayout() ? GetLayout()->MinSize()
: fLayoutData->preferred);
return BLayoutUtils::ComposeSize(ExplicitMinSize(), size);
}
BSize
BMenu::MaxSize()
{
_ValidatePreferredSize();
BSize size = (GetLayout() ? GetLayout()->MaxSize()
: fLayoutData->preferred);
return BLayoutUtils::ComposeSize(ExplicitMaxSize(), size);
}
BSize
BMenu::PreferredSize()
{
_ValidatePreferredSize();
BSize size = (GetLayout() ? 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()) {
BView::DoLayout();
return;
}
if (_RelayoutIfNeeded())
Invalidate();
}
void
BMenu::FrameMoved(BPoint new_position)
{
BView::FrameMoved(new_position);
}
void
BMenu::FrameResized(float new_width, float new_height)
{
BView::FrameResized(new_width, new_height);
}
void
BMenu::InvalidateLayout()
{
InvalidateLayout(false);
}
void
BMenu::InvalidateLayout(bool descendants)
{
fUseCachedMenuLayout = false;
fLayoutData->preferred.Set(B_SIZE_UNSET, B_SIZE_UNSET);
BView::InvalidateLayout(descendants);
}
// #pragma mark -
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 || !_AddItem(item, index))
return false;
InvalidateLayout();
if (LockLooper()) {
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)
return false;
item->fBounds = frame;
int32 index = CountItems();
if (!_AddItem(item, index)) {
return false;
}
if (LockLooper()) {
if (!Window()->IsHidden()) {
_LayoutItems(index);
Invalidate();
}
UnlockLooper();
}
return true;
}
bool
BMenu::AddItem(BMenu* submenu)
{
BMenuItem* item = new (nothrow) BMenuItem(submenu);
if (!item)
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)
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)
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<BMenuItem*>(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(0, 0, item, 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<BMenuItem*>(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<BMenuItem*>(fItems.ItemAt(index));
}
BMenu*
BMenu::SubmenuAt(int32 index) const
{
BMenuItem* item = static_cast<BMenuItem*>(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 enabled)
{
if (fEnabled == enabled)
return;
fEnabled = enabled;
if (fSuperitem)
fSuperitem->SetEnabled(enabled);
}
void
BMenu::SetRadioMode(bool flag)
{
fRadioMode = flag;
if (!flag)
SetLabelFromMarked(false);
}
void
BMenu::SetTriggersEnabled(bool flag)
{
fTriggerEnabled = flag;
}
void
BMenu::SetMaxContentWidth(float width)
{
fMaxContentWidth = width;
}
void
BMenu::SetLabelFromMarked(bool flag)
{
fDynamicName = flag;
if (flag)
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 fRedrawAfterSticky;
}
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;
}
BMenu*
BMenu::Supermenu() const
{
return fSuper;
}
BMenuItem*
BMenu::Superitem() const
{
return fSuperitem;
}
// #pragma mark -
BHandler*
BMenu::ResolveSpecifier(BMessage* msg, int32 index, BMessage* specifier,
int32 form, const char* property)
{
BPropertyInfo propInfo(sPropList);
BHandler* target = NULL;
switch (propInfo.FindMatch(msg, 0, specifier, form, property)) {
case B_ERROR:
break;
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
target = this;
break;
case 8:
// TODO: redirect to menu
target = this;
break;
case 9:
case 10:
case 11:
case 12:
target = this;
break;
case 13:
// TODO: redirect to menuitem
target = this;
break;
}
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_INVALIDATE_LAYOUT:
{
perform_data_invalidate_layout* data
= (perform_data_invalidate_layout*)_data;
BMenu::InvalidateLayout(data->descendants);
return B_OK;
}
case PERFORM_CODE_DO_LAYOUT:
{
BMenu::DoLayout();
return B_OK;
}
}
return BView::Perform(code, _data);
}
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),
fRedrawAfterSticky(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;
}
bool
BMenu::AddDynamicItem(add_state state)
{
// Implemented in subclasses
return false;
}
void
BMenu::DrawBackground(BRect update)
{
if (be_control_look != NULL) {
rgb_color base = sMenuInfo.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, update, base, 0,
borders);
return;
}
rgb_color oldColor = HighColor();
SetHighColor(sMenuInfo.background_color);
FillRect(Bounds() & update, B_SOLID_HIGH);
SetHighColor(oldColor);
}
void
BMenu::SetTrackingHook(menu_tracking_hook func, void* state)
{
delete fExtraMenuData;
fExtraMenuData = new (nothrow) BPrivate::ExtraMenuData(func, state);
}
void BMenu::_ReservedMenu3() {}
void BMenu::_ReservedMenu4() {}
void BMenu::_ReservedMenu5() {}
void BMenu::_ReservedMenu6() {}
void
BMenu::_InitData(BMessage* archive)
{
// 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);
fLayoutData = new LayoutData;
fLayoutData->lastResizingMode = ResizingMode();
SetLowColor(sMenuInfo.background_color);
SetViewColor(sMenuInfo.background_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<BMenuItem*>(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)
{
// 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()) {
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));
window->Unlock();
}
return true;
}
void
BMenu::_Hide()
{
BMenuWindow* window = dynamic_cast<BMenuWindow*>(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
// Delete the menu window used by our submenus
_DeleteMenuWindow();
}
const static bigtime_t kOpenSubmenuDelay = 225000;
const static bigtime_t kNavigationAreaTimeout = 1000000;
const static bigtime_t kHysteresis = 200000;
// TODO: Test and reduce if needed.
const static int32 kMouseMotionThreshold = 15;
// TODO: Same as above. Actually, we could get rid of the kHysteresis
BMenuItem*
BMenu::_Track(int* action, long start)
{
// TODO: cleanup
BMenuItem* item = NULL;
BRect navAreaRectAbove, navAreaRectBelow;
bigtime_t selectedTime = system_time();
bigtime_t navigationAreaTime = 0;
fState = MENU_STATE_TRACKING;
if (fSuper != NULL)
fSuper->fState = MENU_STATE_TRACKING_SUBMENU;
BPoint location;
uint32 buttons = 0;
if (LockLooper()) {
GetMouse(&location, &buttons);
UnlockLooper();
}
int32 mouseSpeed = 0;
bigtime_t pollTime = system_time();
bool releasedOnce = buttons == 0;
while (fState != MENU_STATE_CLOSED) {
if (_CustomTrackingWantsToQuit())
break;
if (!LockLooper())
break;
BMenuWindow* window = static_cast<BMenuWindow*>(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 menu is inside this menu,
// then if it's over a super menu.
bool overSub = _OverSubmenu(fSelected, screenLocation);
item = _HitTestItems(location, B_ORIGIN);
if (overSub) {
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();
int submenuAction = MENU_STATE_TRACKING;
BMenu* submenu = fSelected->Submenu();
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;
}
if (!LockLooper())
break;
} else if (item != NULL) {
_UpdateStateOpenSelect(item, location, navAreaRectAbove,
navAreaRectBelow, selectedTime, navigationAreaTime);
if (!releasedOnce)
releasedOnce = true;
} else if (_OverSuper(screenLocation)) {
fState = MENU_STATE_TRACKING;
UnlockLooper();
break;
} else {
// 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 (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 interfer with keyboard menu navigation
do {
snooze(snoozeAmount);
if (!LockLooper())
break;
GetMouse(&newLocation, &newButtons, true);
UnlockLooper();
} while (newLocation == location && newButtons == buttons);
bigtime_t newPollTime = system_time();
// mouseSpeed in px per ms
// (actually point_distance returns the square of the distance,
// so it's more px^2 per ms)
mouseSpeed = (int32)(point_distance(newLocation, location) * 1000
/ (newPollTime - pollTime));
pollTime = newPollTime;
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;
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());
fSelected->Submenu()->LockLooper();
BRect 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 (!inNavAreaRectAbove && !inNavAreaRectBelow) {
_SelectItem(item, false);
navAreaRectAbove = BRect();
navAreaRectBelow = BRect();
selectedTime = system_time();
navigationAreaTime = 0;
return;
}
BRect menuBounds = ConvertToScreen(Bounds());
fSelected->Submenu()->LockLooper();
BRect submenuBounds = fSelected->Submenu()->ConvertToScreen(
fSelected->Submenu()->Bounds());
fSelected->Submenu()->UnlockLooper();
float xOffset;
// navAreaRectAbove and navAreaRectBelow have the same X
// position and width, so it doesn't matter which one we use to
// calculate the X offset
if (menuBounds.left < submenuBounds.left)
xOffset = position.x - navAreaRectAbove.left;
else
xOffset = navAreaRectAbove.right - position.x;
bool inNavArea;
if (inNavAreaRectAbove) {
float yOffset = navAreaRectAbove.bottom - position.y;
float ratio = navAreaRectAbove.Width() / navAreaRectAbove.Height();
inNavArea = yOffset <= xOffset / ratio;
} else {
float yOffset = navAreaRectBelow.bottom - position.y;
float ratio = navAreaRectBelow.Width() / navAreaRectBelow.Height();
inNavArea = yOffset >= (navAreaRectBelow.Height() - xOffset
/ ratio);
}
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 = min_c(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 = max_c(0, index);
for (; i >= index; i--) {
item = static_cast<BMenuItem*>(fItems.ItemAt(i));
if (item != NULL) {
if (fItems.RemoveItem(item)) {
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);
return true;
}
return false;
}
void
BMenu::_LayoutItems(int32 index)
{
_CalcTriggers();
float width, 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);
}
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:
_ComputeColumnLayout(index, bestFit, moveItems, frame);
break;
case B_ITEMS_IN_ROW:
_ComputeRowLayout(index, bestFit, moveItems, frame);
break;
case B_ITEMS_IN_MATRIX:
_ComputeMatrixLayout(frame);
break;
default:
break;
}
// change width depending on resize mode
BSize size;
if ((ResizingMode() & B_FOLLOW_LEFT_RIGHT) == B_FOLLOW_LEFT_RIGHT) {
if (Parent())
size.width = Parent()->Frame().Width() + 1;
else if (Window())
size.width = Window()->Frame().Width() + 1;
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& frame)
{
BFont font;
GetFont(&font);
bool command = false;
bool control = false;
bool shift = false;
bool option = false;
if (index > 0)
frame = ItemAt(index - 1)->Frame();
else
frame.Set(0, 0, 0, -1);
for (; index < fItems.CountItems(); index++) {
BMenuItem* item = ItemAt(index);
float width, height;
item->GetContentSize(&width, &height);
if (item->fModifiers && item->fShortcutChar) {
width += font.Size();
if (item->fModifiers & B_COMMAND_KEY)
command = true;
if (item->fModifiers & B_CONTROL_KEY)
control = true;
if (item->fModifiers & B_SHIFT_KEY)
shift = true;
if (item->fModifiers & B_OPTION_KEY)
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)
width += item->Frame().Height();
frame.right = max_c(frame.right, width + fPad.left + fPad.right);
frame.bottom = item->fBounds.bottom;
}
if (command)
frame.right += 17;
if (control)
frame.right += 17;
if (option)
frame.right += 17;
if (shift)
frame.right += 22;
if (fMaxContentWidth > 0)
frame.right = min_c(frame.right, fMaxContentWidth);
if (moveItems) {
for (int32 i = 0; i < fItems.CountItems(); i++)
ItemAt(i)->fBounds.right = frame.right;
}
frame.top = 0;
frame.right = ceilf(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 = max_c(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 = min_c(frame.left, item->Frame().left);
frame.right = max_c(frame.right, item->Frame().right);
frame.top = min_c(frame.top, item->Frame().top);
frame.bottom = max_c(frame.bottom, item->Frame().bottom);
}
}
}
// 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();
bool scroll = false;
// TODO: Horrible hack:
// When added to a BMenuField, a BPopUpMenu is the child of
// a _BMCMenuBar_ to "fake" the menu hierarchy
if (superMenu == NULL || superItem == NULL
|| dynamic_cast<_BMCMenuBar_*>(superMenu) != NULL) {
// 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);
else if (frame.left < screenFrame.left)
frame.OffsetBy(-frame.left, 0);
} else if (superMenu->Layout() == B_ITEMS_IN_COLUMN) {
if (frame.right > screenFrame.right)
frame.OffsetBy(-superItem->Frame().Width() - frame.Width() - 2, 0);
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) {
if (scrollOn != NULL && superMenu != NULL
&& dynamic_cast<BMenuBar*>(superMenu) != NULL
&& frame.top < (screenFrame.bottom - 80)) {
scroll = true;
} else {
frame.OffsetBy(0, -superItem->Frame().Height()
- frame.Height() - 3);
}
}
if (frame.right > screenFrame.right)
frame.OffsetBy(screenFrame.right - frame.right, 0);
}
if (!scroll) {
// 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
scroll = screenFrame.Height() < frame.Height();
}
if (scrollOn != NULL)
*scrollOn = scroll;
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;
// we assume that loc is in screen coords {
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->IsEnabled() && item->Frame().Contains(where))
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);
}
InvalidateLayout();
}
if (IsLabelFromMarked() && Superitem())
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* menuItem, bool showSubmenu, bool selectFirstItem)
{
// Avoid deselecting and then reselecting the same item
// which would cause flickering
if (menuItem != fSelected) {
if (fSelected != NULL) {
fSelected->Select(false);
BMenu* subMenu = fSelected->Submenu();
if (subMenu != NULL && subMenu->Window() != NULL)
subMenu->_Hide();
}
fSelected = menuItem;
if (fSelected != NULL)
fSelected->Select(true);
}
if (fSelected != NULL && showSubmenu) {
BMenu* subMenu = fSelected->Submenu();
if (subMenu != NULL && subMenu->Window() == NULL) {
if (!subMenu->_Show(selectFirstItem)) {
// 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;
bool openMenu = false;
if (dynamic_cast<BMenuBar*>(this) != NULL)
openMenu = true;
_SelectItem(nextItem, openMenu);
return true;
}
BMenuItem*
BMenu::_NextItem(BMenuItem* item, bool forward) const
{
// go to next item, and skip over disabled items such as separators
int32 index = fItems.IndexOf(item);
const int32 numItems = fItems.CountItems();
if (index < 0) {
if (forward)
index = -1;
else
index = numItems;
}
int32 startIndex = index;
do {
if (forward)
index++;
else
index--;
// cycle through menu items
if (index < 0)
index = numItems - 1;
else if (index >= numItems)
index = 0;
} while (!ItemAt(index)->IsEnabled() && index != startIndex);
if (index == startIndex) {
// We are back where we started and no item was enabled.
return NULL;
}
return ItemAt(index);
}
void
BMenu::_SetIgnoreHidden(bool on)
{
fIgnoreHidden = on;
}
void
BMenu::_SetStickyMode(bool on)
{
if (fStickyMode == on)
return;
fStickyMode = on;
// If we are switching to sticky mode, propagate the status
// back to the super menu
if (fSuper != NULL)
fSuper->_SetStickyMode(on);
else {
// TODO: Ugly hack, but it needs to be done right here in this method
BMenuBar* menuBar = dynamic_cast<BMenuBar*>(this);
if (on && menuBar != NULL && menuBar->LockLooper()) {
// 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::_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;
uint32 c;
// two runs: first we look out for uppercase letters
// TODO: support Unicode characters correctly!
for (uint32 i = 0; (c = title[i]) != '\0'; i++) {
if (!IsInsideGlyph(c) && isupper(c) && !triggers.HasTrigger(c)) {
index = i;
trigger = tolower(c);
return triggers.AddTrigger(c);
}
}
// then, if we still haven't found anything, we accept them all
index = 0;
while ((c = UTF8ToCharCode(&title)) != 0) {
if (!isspace(c) && !triggers.HasTrigger(c)) {
trigger = tolower(c);
return triggers.AddTrigger(c);
}
index++;
}
return false;
}
void
BMenu::_UpdateWindowViewSize(bool updatePosition)
{
BMenuWindow* window = static_cast<BMenuWindow*>(Window());
if (window == NULL)
return;
if (dynamic_cast<BMenuBar*>(this) != NULL)
return;
if (!fResizeToFit)
return;
bool scroll = false;
const BPoint screenLocation = updatePosition
? ScreenLocation() : window->Frame().LeftTop();
BRect frame = _CalcFrame(screenLocation, &scroll);
ResizeTo(frame.Width(), frame.Height());
if (fItems.CountItems() > 0) {
if (!scroll) {
window->ResizeTo(Bounds().Width(), Bounds().Height());
} else {
BScreen screen(window);
// If we need scrolling, resize the window to fit the screen and
// attach scrollers to our cached BMenuWindow.
if (dynamic_cast<BMenuBar*>(Supermenu()) == NULL || frame.top < 0) {
window->ResizeTo(Bounds().Width(), screen.Frame().Height());
frame.top = 0;
} else {
// Or, in case our parent was a BMenuBar enable scrolling with
// normal size.
window->ResizeTo(Bounds().Width(),
screen.Frame().bottom - frame.top);
}
window->AttachScrollers();
}
} else {
_CacheFontInfo();
window->ResizeTo(StringWidth(BPrivate::kEmptyMenuLabel)
+ fPad.left + fPad.right,
fFontHeight + fPad.top + fPad.bottom);
}
if (updatePosition)
window->MoveTo(frame.LeftTop());
}
bool
BMenu::_OkToProceed(BMenuItem* item)
{
BPoint where;
ulong 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<BMenuBar*>(this) == NULL
&& (buttons == 0 && !stickyMode)) || _HitTestItems(where) != item))
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<BMenuBar*>(this))
menuBar->_RestoreFocus();
fChosenItem = NULL;
fState = MENU_STATE_CLOSED;
// Close the whole menu hierarchy
if (!onlyThis && _IsStickyMode())
_SetStickyMode(false);
_Hide();
}
// #pragma mark -
// 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<menu_info>(*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<menu_info>(info);
return status;
}