mirror of https://github.com/buggins/dlangui.git
990 lines
34 KiB
D
990 lines
34 KiB
D
// Written in the D programming language.
|
|
|
|
/**
|
|
This module contains menu widgets implementation.
|
|
|
|
MenuItem - menu item properties container - to hold hierarchy of menu.
|
|
MainMenu - main menu widget
|
|
PopupMenu - popup menu widget
|
|
|
|
Synopsis:
|
|
|
|
----
|
|
import dlangui.widgets.popup;
|
|
|
|
----
|
|
|
|
Copyright: Vadim Lopatin, 2014
|
|
License: Boost License 1.0
|
|
Authors: Vadim Lopatin, coolreader.org@gmail.com
|
|
*/
|
|
module dlangui.widgets.menu;
|
|
|
|
import dlangui.core.events;
|
|
import dlangui.widgets.controls;
|
|
import dlangui.widgets.layouts;
|
|
import dlangui.widgets.lists;
|
|
import dlangui.widgets.popup;
|
|
|
|
// define DebugMenus for menu debug messages
|
|
//debug = DebugMenus;
|
|
|
|
/// menu item type
|
|
enum MenuItemType {
|
|
/// normal menu item
|
|
Normal,
|
|
/// menu item - checkbox
|
|
Check,
|
|
/// menu item - radio button
|
|
Radio,
|
|
/// menu separator (horizontal line)
|
|
Separator,
|
|
/// submenu - contains child items
|
|
Submenu
|
|
}
|
|
|
|
/// interface to handle menu item click
|
|
interface MenuItemClickHandler {
|
|
bool onMenuItemClick(MenuItem item);
|
|
}
|
|
|
|
/// interface to handle menu item action
|
|
interface MenuItemActionHandler {
|
|
bool onMenuItemAction(const Action action);
|
|
}
|
|
|
|
|
|
/// menu item properties
|
|
class MenuItem {
|
|
protected bool _checked;
|
|
protected bool _enabled;
|
|
protected MenuItemType _type = MenuItemType.Normal;
|
|
protected Action _action;
|
|
protected MenuItem[] _subitems;
|
|
protected MenuItem _parent;
|
|
/// handle menu item click (parameter is MenuItem)
|
|
Signal!MenuItemClickHandler menuItemClick;
|
|
/// handle menu item click action (parameter is Action)
|
|
Signal!MenuItemActionHandler menuItemAction;
|
|
/// item action id, 0 if no action
|
|
@property int id() const { return _action is null ? 0 : _action.id; }
|
|
/// returns count of submenu items
|
|
@property int subitemCount() {
|
|
return cast(int)_subitems.length;
|
|
}
|
|
/// returns subitem index for item, -1 if item is not direct subitem of this
|
|
@property int subitemIndex(MenuItem item) {
|
|
for (int i = 0; i < _subitems.length; i++)
|
|
if (_subitems[i] is item)
|
|
return i;
|
|
return -1;
|
|
}
|
|
/// returns submenu item by index
|
|
MenuItem subitem(int index) {
|
|
return _subitems[index];
|
|
}
|
|
|
|
/// map key to action
|
|
Action findKeyAction(uint keyCode, uint flags) {
|
|
if (_action) {
|
|
if (_action.checkAccelerator(keyCode, flags))
|
|
return _action;
|
|
}
|
|
for (int i = 0; i < subitemCount; i++) {
|
|
Action a = subitem(i).findKeyAction(keyCode, flags);
|
|
if (a)
|
|
return a;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@property MenuItemType type() const {
|
|
if (id == SEPARATOR_ACTION_ID)
|
|
return MenuItemType.Separator;
|
|
if (_subitems.length > 0) // if there are children, force type to Submenu
|
|
return MenuItemType.Submenu;
|
|
return _type;
|
|
}
|
|
|
|
/// set new MenuItemType
|
|
@property MenuItem type(MenuItemType type) {
|
|
_type = type;
|
|
return this;
|
|
}
|
|
|
|
/// get check for checkbox or radio button item
|
|
@property bool checked() {
|
|
return _checked;
|
|
}
|
|
/// check radio button with specified index, uncheck other radio buttons in group (group consists of sequence of radio button items; other item type - end of group)
|
|
protected void checkRadioButton(int index) {
|
|
// find bounds of group
|
|
int start = index;
|
|
int end = index;
|
|
for (; start > 0 && _subitems[start - 1].type == MenuItemType.Radio; start--) {
|
|
// do nothing
|
|
}
|
|
for (; end < _subitems.length - 1 && _subitems[end + 1].type == MenuItemType.Radio; end++) {
|
|
// do nothing
|
|
}
|
|
// check item with specified index, uncheck others
|
|
for (int i = start; i <= end; i++)
|
|
_subitems[i]._checked = (i == index);
|
|
}
|
|
/// set check for checkbox or radio button item
|
|
@property MenuItem checked(bool flg) {
|
|
if (_checked == flg)
|
|
return this;
|
|
_checked = flg;
|
|
if (flg && _parent && type == MenuItemType.Radio) {
|
|
int index = _parent.subitemIndex(this);
|
|
if (index >= 0) {
|
|
_parent.checkRadioButton(index);
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/// get hotkey character from label (e.g. 'F' for item labeled "&File"), 0 if no hotkey
|
|
dchar getHotkey() {
|
|
static import std.uni;
|
|
dstring s = label;
|
|
dchar ch = 0;
|
|
for (int i = 0; i < s.length - 1; i++) {
|
|
if (s[i] == '&') {
|
|
ch = s[i + 1];
|
|
break;
|
|
}
|
|
}
|
|
return std.uni.toUpper(ch);
|
|
}
|
|
|
|
/// find subitem by hotkey character, returns subitem index, -1 if not found
|
|
int findSubitemByHotkey(dchar ch) {
|
|
static import std.uni;
|
|
if (!ch)
|
|
return -1;
|
|
ch = std.uni.toUpper(ch);
|
|
for (int i = 0; i < _subitems.length; i++) {
|
|
if (_subitems[i].getHotkey() == ch)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// Add separator item
|
|
MenuItem addSeparator() {
|
|
return add(new Action(SEPARATOR_ACTION_ID));
|
|
}
|
|
|
|
/// adds submenu item
|
|
MenuItem add(MenuItem subitem) {
|
|
_subitems ~= subitem;
|
|
subitem._parent = this;
|
|
return this;
|
|
}
|
|
/// adds submenu item(s) from one or more actions (will return item for last action)
|
|
MenuItem add(Action[] subitemActions...) {
|
|
MenuItem res = null;
|
|
foreach(subitemAction; subitemActions) {
|
|
res = add(new MenuItem(subitemAction));
|
|
}
|
|
return res;
|
|
}
|
|
/// adds submenu item(s) from one or more actions (will return item for last action)
|
|
MenuItem add(const Action[] subitemActions...) {
|
|
MenuItem res = null;
|
|
foreach(subitemAction; subitemActions) {
|
|
res = add(new MenuItem(subitemAction));
|
|
}
|
|
return res;
|
|
}
|
|
/// returns text description for first accelerator of action; null if no accelerators
|
|
@property dstring acceleratorText() {
|
|
if (!_action)
|
|
return null;
|
|
return _action.acceleratorText;
|
|
}
|
|
/// returns true if item is submenu (contains subitems)
|
|
@property bool isSubmenu() {
|
|
return _subitems.length > 0;
|
|
}
|
|
/// returns item label
|
|
@property UIString label() {
|
|
return _action !is null ? _action.labelValue : UIString("");
|
|
}
|
|
/// returns item action
|
|
@property const(Action) action() const { return _action; }
|
|
/// sets item action
|
|
@property MenuItem action(Action a) { _action = a; return this; }
|
|
|
|
/// menu item Enabled flag
|
|
@property bool enabled() { return _enabled && type != MenuItemType.Separator; }
|
|
/// menu item Enabled flag
|
|
@property MenuItem enabled(bool enabled) {
|
|
_enabled = enabled;
|
|
return this;
|
|
}
|
|
|
|
/// handle menu item click
|
|
Signal!(void, MenuItem) onMenuItem;
|
|
/// prepare for opening of submenu, return true if opening is allowed
|
|
Signal!(bool, MenuItem) openingSubmenu;
|
|
|
|
/// call to update state for action (if action is assigned for widget)
|
|
void updateActionState(Widget w) {
|
|
//import dlangui.widgets.editors;
|
|
if (_action) {
|
|
//if (_action.id == EditorActions.Copy) {
|
|
// Log.d("Requesting Copy action. Old state: ", _action.state);
|
|
//}
|
|
w.updateActionState(_action, true);
|
|
_enabled = _action.state.enabled;
|
|
_checked = _action.state.checked;
|
|
}
|
|
for (int i = 0; i < _subitems.length; i++) {
|
|
_subitems[i].updateActionState(w);
|
|
}
|
|
}
|
|
|
|
this() {
|
|
_enabled = true;
|
|
}
|
|
this(Action action) {
|
|
_action = action;
|
|
_enabled = true;
|
|
}
|
|
this(const Action action) {
|
|
_action = action.clone;
|
|
_enabled = true;
|
|
}
|
|
~this() {
|
|
// TODO
|
|
}
|
|
}
|
|
|
|
/// widget to draw menu item
|
|
class MenuItemWidget : WidgetGroupDefaultDrawing {
|
|
protected bool _mainMenu;
|
|
protected MenuItem _item;
|
|
protected ImageWidget _icon;
|
|
protected TextWidget _accel;
|
|
protected TextWidget _label;
|
|
protected int _labelWidth;
|
|
protected int _iconWidth;
|
|
protected int _accelWidth;
|
|
protected int _height;
|
|
@property MenuItem item() { return _item; }
|
|
void setSubitemSizes(int maxLabelWidth, int maxHeight, int maxIconWidth, int maxAccelWidth) {
|
|
_labelWidth = maxLabelWidth;
|
|
_height = maxHeight;
|
|
_iconWidth = maxIconWidth;
|
|
_accelWidth = maxAccelWidth;
|
|
}
|
|
void measureSubitems(ref int maxLabelWidth, ref int maxHeight, ref int maxIconWidth, ref int maxAccelWidth) {
|
|
_label.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED);
|
|
if (maxLabelWidth < _label.measuredWidth)
|
|
maxLabelWidth = _label.measuredWidth;
|
|
if (maxHeight < _label.measuredHeight)
|
|
maxHeight = _label.measuredHeight;
|
|
if (_icon) {
|
|
_icon.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED);
|
|
if (maxIconWidth < _icon.measuredWidth)
|
|
maxIconWidth = _icon.measuredWidth;
|
|
if (maxHeight < _icon.measuredHeight)
|
|
maxHeight = _icon.measuredHeight;
|
|
}
|
|
if (_accel) {
|
|
_accel.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED);
|
|
if (maxAccelWidth < _accel.measuredWidth)
|
|
maxAccelWidth = _accel.measuredWidth;
|
|
if (maxHeight < _accel.measuredHeight)
|
|
maxHeight = _accel.measuredHeight;
|
|
}
|
|
}
|
|
/// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
|
|
override void measure(int parentWidth, int parentHeight) {
|
|
updateState();
|
|
Rect m = margins;
|
|
Rect p = padding;
|
|
// calc size constraints for children
|
|
int pwidth = parentWidth;
|
|
int pheight = parentHeight;
|
|
if (parentWidth != SIZE_UNSPECIFIED)
|
|
pwidth -= m.left + m.right + p.left + p.right;
|
|
if (parentHeight != SIZE_UNSPECIFIED)
|
|
pheight -= m.top + m.bottom + p.top + p.bottom;
|
|
if (_labelWidth)
|
|
measuredContent(parentWidth, parentHeight, _iconWidth + _labelWidth + _accelWidth, _height); // for vertical (popup menu)
|
|
else {
|
|
_label.measure(pwidth, pheight);
|
|
measuredContent(parentWidth, parentHeight, _label.measuredWidth, _label.measuredHeight); // for horizonral (main) menu
|
|
}
|
|
}
|
|
|
|
/// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
|
|
override void layout(Rect rc) {
|
|
_needLayout = false;
|
|
if (visibility == Visibility.Gone) {
|
|
return;
|
|
}
|
|
_pos = rc;
|
|
applyMargins(rc);
|
|
applyPadding(rc);
|
|
Rect labelRc = rc;
|
|
Rect iconRc = rc;
|
|
Rect accelRc = rc;
|
|
iconRc.right = iconRc.left + _iconWidth;
|
|
accelRc.left = accelRc.right - _accelWidth;
|
|
labelRc.left += _iconWidth;
|
|
labelRc.right -= _accelWidth;
|
|
if (_icon)
|
|
_icon.layout(iconRc);
|
|
if (_accel)
|
|
_accel.layout(accelRc);
|
|
_label.layout(labelRc);
|
|
}
|
|
|
|
protected void updateState() {
|
|
if (_item.enabled)
|
|
setState(State.Enabled);
|
|
else
|
|
resetState(State.Enabled);
|
|
if (_item.checked)
|
|
setState(State.Checked);
|
|
else
|
|
resetState(State.Checked);
|
|
}
|
|
|
|
///// call to update state for action (if action is assigned for widget)
|
|
//override void updateActionState(bool force = false) {
|
|
// if (!_item.action)
|
|
// return;
|
|
// super.updateActionState(_item._action, force);
|
|
// _item.enabled = _item._action.state.enabled;
|
|
// _item.checked = _item._action.state.checked;
|
|
// updateState();
|
|
//}
|
|
|
|
this(MenuItem item, bool mainMenu) {
|
|
id="menuitem";
|
|
_mainMenu = mainMenu;
|
|
_item = item;
|
|
styleId = STYLE_MENU_ITEM;
|
|
updateState();
|
|
string iconId = _item.action !is null ? _item.action.iconId : "";
|
|
if (_item.type == MenuItemType.Check)
|
|
iconId = "btn_check";
|
|
else if (_item.type == MenuItemType.Radio)
|
|
iconId = "btn_radio";
|
|
// icon
|
|
if (_item.action && iconId.length) {
|
|
_icon = new ImageWidget("MENU_ICON", iconId);
|
|
_icon.styleId = STYLE_MENU_ICON;
|
|
_icon.state = State.Parent;
|
|
addChild(_icon);
|
|
}
|
|
// label
|
|
_label = new TextWidget("MENU_LABEL");
|
|
_label.text = _item.label;
|
|
_label.styleId = _mainMenu ? "MAIN_MENU_LABEL" : "MENU_LABEL";
|
|
_label.state = State.Parent;
|
|
addChild(_label);
|
|
// accelerator
|
|
dstring acc = _item.acceleratorText;
|
|
if (_item.isSubmenu && !mainMenu) {
|
|
version (Windows) {
|
|
acc = ">"d;
|
|
//acc = "►"d;
|
|
} else {
|
|
acc = "‣"d;
|
|
}
|
|
}
|
|
if (acc !is null) {
|
|
_accel = new TextWidget("MENU_ACCEL");
|
|
_accel.styleId = STYLE_MENU_ACCEL;
|
|
_accel.text = acc;
|
|
_accel.state = State.Parent;
|
|
if (_item.isSubmenu && !mainMenu)
|
|
_accel.alignment = Align.Right | Align.VCenter;
|
|
addChild(_accel);
|
|
}
|
|
trackHover = true;
|
|
clickable = true;
|
|
}
|
|
}
|
|
|
|
/// base class for menus
|
|
class MenuWidgetBase : ListWidget {
|
|
protected MenuWidgetBase _parentMenu;
|
|
protected MenuItem _item;
|
|
protected PopupMenu _openedMenu;
|
|
protected PopupWidget _openedPopup;
|
|
protected int _openedPopupIndex;
|
|
|
|
/// menu item click listener
|
|
Signal!MenuItemClickHandler menuItemClick;
|
|
/// menu item action listener
|
|
Signal!MenuItemActionHandler menuItemAction;
|
|
|
|
this(MenuWidgetBase parentMenu, MenuItem item, Orientation orientation) {
|
|
_parentMenu = parentMenu;
|
|
this.orientation = orientation;
|
|
id = "popup_menu";
|
|
styleId = STYLE_POPUP_MENU;
|
|
menuItems = item;
|
|
}
|
|
|
|
@property void menuItems(MenuItem item) {
|
|
if (_item) {
|
|
destroy(_item);
|
|
_item = null;
|
|
}
|
|
_item = item;
|
|
WidgetListAdapter adapter = new WidgetListAdapter();
|
|
if (item) {
|
|
for (int i=0; i < _item.subitemCount; i++) {
|
|
MenuItem subitem = _item.subitem(i);
|
|
MenuItemWidget widget = new MenuItemWidget(subitem, orientation == Orientation.Horizontal);
|
|
if (orientation == Orientation.Horizontal)
|
|
widget.styleId = STYLE_MAIN_MENU_ITEM;
|
|
widget.parent = this;
|
|
adapter.add(widget);
|
|
}
|
|
}
|
|
ownAdapter = adapter;
|
|
requestLayout();
|
|
}
|
|
|
|
@property protected bool isMainMenu() {
|
|
return _orientation == Orientation.Horizontal;
|
|
}
|
|
|
|
/// call to update state for action (if action is assigned for widget)
|
|
override void updateActionState(bool force = false) {
|
|
for (int i = 0; i < itemCount; i++) {
|
|
MenuItemWidget w = cast(MenuItemWidget)itemWidget(i);
|
|
if (w)
|
|
w.updateActionState(force);
|
|
}
|
|
}
|
|
|
|
/// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
|
|
override void measure(int parentWidth, int parentHeight) {
|
|
if (_orientation == Orientation.Horizontal) {
|
|
// for horizontal (main) menu, don't align items
|
|
super.measure(parentWidth, parentHeight);
|
|
return;
|
|
}
|
|
|
|
if (visibility == Visibility.Gone) {
|
|
_measuredWidth = _measuredHeight = 0;
|
|
return;
|
|
}
|
|
int maxLabelWidth;
|
|
int maxHeight;
|
|
int maxIconWidth;
|
|
int maxAccelWidth;
|
|
/// find max dimensions for item icon and accelerator sizes
|
|
for (int i = 0; i < itemCount; i++) {
|
|
MenuItemWidget w = cast(MenuItemWidget)itemWidget(i);
|
|
if (w)
|
|
w.measureSubitems(maxLabelWidth, maxHeight, maxIconWidth, maxAccelWidth);
|
|
}
|
|
/// set equal dimensions for item icon and accelerator sizes
|
|
for (int i = 0; i < itemCount; i++) {
|
|
MenuItemWidget w = cast(MenuItemWidget)itemWidget(i);
|
|
if (w)
|
|
w.setSubitemSizes(maxLabelWidth, maxHeight, maxIconWidth, maxAccelWidth);
|
|
}
|
|
super.measure(parentWidth, parentHeight);
|
|
}
|
|
|
|
protected void performUndoSelection() {
|
|
selectItem(-1);
|
|
setHoverItem(-1);
|
|
}
|
|
|
|
protected void onPopupClosed(PopupWidget p) {
|
|
debug(DebugMenus) Log.d("menu ", id, " onPopupClosed selectionChanging=", _selectionChangingInProgress);
|
|
if (_openedPopup) {
|
|
if (_openedPopup is p) {
|
|
_openedMenu.onPopupClosed(p);
|
|
//bool undoSelection = _openedPopupIndex == _selectedItemIndex;
|
|
_openedPopup = null;
|
|
_openedMenu = null;
|
|
//if (undoSelection) {
|
|
// performUndoSelection();
|
|
//}
|
|
if (!isMainMenu)
|
|
window.setFocus(this);
|
|
//else
|
|
// performUndoSelection();
|
|
if (isMainMenu && !_selectionChangingInProgress)
|
|
close();
|
|
} else if (thisPopup is p) {
|
|
_openedPopup.close();
|
|
_openedPopup = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void openSubmenu(int index, MenuItemWidget itemWidget, bool selectFirstItem) {
|
|
debug(DebugMenus) Log.d("menu", id, " open submenu ", index);
|
|
if (_openedPopup !is null) {
|
|
if (_openedPopupIndex == index) {
|
|
if (selectFirstItem) {
|
|
window.setFocus(_openedMenu);
|
|
_openedMenu.selectItem(0);
|
|
}
|
|
return;
|
|
} else {
|
|
_openedPopup.close();
|
|
_openedPopup = null;
|
|
}
|
|
}
|
|
|
|
PopupMenu popupMenu = new PopupMenu(itemWidget.item, this);
|
|
PopupWidget popup = window.showPopup(popupMenu, itemWidget, orientation == Orientation.Horizontal ? PopupAlign.Below : PopupAlign.Right);
|
|
requestActionsUpdate();
|
|
popup.popupClosed = &onPopupClosed;
|
|
popup.flags = PopupFlags.CloseOnClickOutside;
|
|
_openedPopup = popup;
|
|
_openedMenu = popupMenu;
|
|
_openedPopupIndex = index;
|
|
_selectedItemIndex = index;
|
|
if (selectFirstItem) {
|
|
debug(DebugMenus) Log.d("menu: selecting first item");
|
|
window.setFocus(popupMenu);
|
|
_openedMenu.selectItem(0);
|
|
}
|
|
}
|
|
|
|
protected bool _selectionChangingInProgress;
|
|
/// override to handle change of selection
|
|
override protected void selectionChanged(int index, int previouslySelectedItem = -1) {
|
|
debug(DebugMenus) Log.d("menu ", id, " selectionChanged ", index, ", ", previouslySelectedItem, " _selectedItemIndex=", _selectedItemIndex);
|
|
_selectionChangingInProgress = true;
|
|
if (index >= 0)
|
|
setFocus();
|
|
MenuItemWidget itemWidget = index >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(index) : null;
|
|
MenuItemWidget prevWidget = previouslySelectedItem >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(previouslySelectedItem) : null;
|
|
bool popupWasOpen = false;
|
|
if (prevWidget !is null) {
|
|
if (_openedPopup !is null) {
|
|
_openedPopup.close();
|
|
_openedPopup = null;
|
|
popupWasOpen = true;
|
|
}
|
|
}
|
|
if (itemWidget !is null) {
|
|
if (itemWidget.item.isSubmenu()) {
|
|
if (_selectOnHover || popupWasOpen) {
|
|
openSubmenu(index, itemWidget, _orientation == Orientation.Horizontal); // for main menu, select first item
|
|
}
|
|
} else {
|
|
// normal item
|
|
}
|
|
}
|
|
_selectionChangingInProgress = false;
|
|
}
|
|
|
|
protected void handleMenuItemClick(MenuItem item) {
|
|
// precessing for CheckBox and RadioButton menus
|
|
if (item.type == MenuItemType.Check) {
|
|
item.checked = !item.checked;
|
|
} else if (item.type == MenuItemType.Radio) {
|
|
item.checked = true;
|
|
}
|
|
MenuItem p = item;
|
|
while (p) {
|
|
if (p.menuItemClick.assigned) {
|
|
p.menuItemClick(item);
|
|
break;
|
|
}
|
|
if (p.menuItemAction.assigned && item.action) {
|
|
p.menuItemAction(item.action);
|
|
break;
|
|
}
|
|
p = p._parent;
|
|
}
|
|
}
|
|
|
|
protected void onMenuItem(MenuItem item) {
|
|
debug(DebugMenus) Log.d("onMenuItem ", item.action.label);
|
|
if (_openedPopup !is null) {
|
|
_openedPopup.close();
|
|
_openedPopup = null;
|
|
}
|
|
if (_parentMenu !is null)
|
|
_parentMenu.onMenuItem(item);
|
|
else {
|
|
// top level handling
|
|
Log.d("onMenuItem ", item.id);
|
|
selectItem(-1);
|
|
setHoverItem(-1);
|
|
selectOnHover = false;
|
|
|
|
// copy menu item click listeners
|
|
Signal!MenuItemClickHandler onMenuItemClickListenerCopy = menuItemClick;
|
|
// copy item action listeners
|
|
Signal!MenuItemActionHandler onMenuItemActionListenerCopy = menuItemAction;
|
|
|
|
handleMenuItemClick(item);
|
|
|
|
PopupWidget popup = cast(PopupWidget)parent;
|
|
if (popup)
|
|
popup.close();
|
|
|
|
// this pointer now can be invalid - if popup removed
|
|
if (onMenuItemClickListenerCopy.assigned)
|
|
if (onMenuItemClickListenerCopy(item))
|
|
return;
|
|
// this pointer now can be invalid - if popup removed
|
|
if (onMenuItemActionListenerCopy.assigned)
|
|
onMenuItemActionListenerCopy(item.action);
|
|
}
|
|
}
|
|
|
|
@property MenuItemWidget selectedMenuItemWidget() {
|
|
return _selectedItemIndex >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(_selectedItemIndex) : null;
|
|
}
|
|
|
|
/// override to handle mouse up on item
|
|
override protected void itemClicked(int index) {
|
|
MenuItemWidget itemWidget = index >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(index) : null;
|
|
if (itemWidget !is null) {
|
|
debug(DebugMenus) Log.d("Menu ", id, " Item clicked ", itemWidget.item.action.id);
|
|
if (itemWidget.item.isSubmenu()) {
|
|
// submenu clicked
|
|
if (_clickOnButtonDown && _openedPopup !is null && _openedMenu._item is itemWidget.item) {
|
|
|
|
if (_selectedItemIndex == index) {
|
|
_openedMenu.setFocus();
|
|
return;
|
|
}
|
|
|
|
// second click on main menu opened item
|
|
_openedPopup.close();
|
|
_openedPopup = null;
|
|
//selectItem(-1);
|
|
selectOnHover = false;
|
|
} else {
|
|
openSubmenu(index, itemWidget, _orientation == Orientation.Horizontal); // for main menu, select first item
|
|
selectOnHover = true;
|
|
}
|
|
} else {
|
|
// normal item
|
|
onMenuItem(itemWidget.item);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// returns popup this menu is located in
|
|
@property PopupWidget thisPopup() {
|
|
return cast(PopupWidget)parent;
|
|
}
|
|
|
|
protected int _menuToggleState;
|
|
protected Widget _menuTogglePreviousFocus;
|
|
|
|
/// override to handle specific actions state (e.g. change enabled state for supported actions)
|
|
override bool handleActionStateRequest(const Action a) {
|
|
if (_menuTogglePreviousFocus) {
|
|
Log.d("Menu.handleActionStateRequest forwarding to ", _menuTogglePreviousFocus);
|
|
bool res = _menuTogglePreviousFocus.handleActionStateRequest(a);
|
|
Log.d("Menu.handleActionStateRequest forwarding handled successful: ", a.state.toString);
|
|
return res;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// list navigation using keys
|
|
override bool onKeyEvent(KeyEvent event) {
|
|
if (orientation == Orientation.Horizontal) {
|
|
// no special processing
|
|
if (event.action == KeyAction.KeyDown) {
|
|
if (event.keyCode == KeyCode.ESCAPE) {
|
|
close();
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
// for vertical (popup) menu
|
|
if (!focused)
|
|
return false;
|
|
if (event.action == KeyAction.KeyDown) {
|
|
if (event.keyCode == KeyCode.LEFT) {
|
|
if (_parentMenu !is null) {
|
|
if (_parentMenu.orientation == Orientation.Vertical) {
|
|
if (thisPopup !is null) {
|
|
//int selectedItem = _selectedItemIndex;
|
|
// back to parent menu on Left key
|
|
thisPopup.close();
|
|
//if (selectedItem >= 0)
|
|
// selectItem(selectedItem);
|
|
return true;
|
|
}
|
|
} else {
|
|
// parent is main menu
|
|
_parentMenu.moveSelection(-1);
|
|
return true;
|
|
}
|
|
}
|
|
return true;
|
|
} else if (event.keyCode == KeyCode.RIGHT) {
|
|
MenuItemWidget thisItem = selectedMenuItemWidget();
|
|
if (thisItem !is null && thisItem.item.isSubmenu) {
|
|
openSubmenu(_selectedItemIndex, thisItem, true);
|
|
return true;
|
|
} else if (_parentMenu !is null && _parentMenu.orientation == Orientation.Horizontal) {
|
|
_parentMenu.moveSelection(1);
|
|
return true;
|
|
}
|
|
return true;
|
|
} else if (event.keyCode == KeyCode.ESCAPE) {
|
|
close();
|
|
return true;
|
|
}
|
|
} else if (event.action == KeyAction.KeyUp) {
|
|
if (event.keyCode == KeyCode.LEFT || event.keyCode == KeyCode.RIGHT) {
|
|
return true;
|
|
}
|
|
} else if (event.action == KeyAction.Text && event.flags == 0) {
|
|
dchar ch = event.text[0];
|
|
int index = _item.findSubitemByHotkey(ch);
|
|
if (index >= 0) {
|
|
itemClicked(index);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
if (_selectedItemIndex >= 0 && event.action == KeyAction.KeyDown && /*event.flags == 0 &&*/ (event.keyCode == KeyCode.RETURN || event.keyCode == KeyCode.SPACE)) {
|
|
itemClicked(_selectedItemIndex);
|
|
return true;
|
|
}
|
|
bool res = super.onKeyEvent(event);
|
|
return res;
|
|
}
|
|
/// closes this menu - handle ESC key
|
|
void close() {
|
|
if (thisPopup !is null)
|
|
thisPopup.close();
|
|
}
|
|
|
|
/// map key to action
|
|
override Action findKeyAction(uint keyCode, uint flags) {
|
|
if (!_item)
|
|
return null;
|
|
Action action = _item.findKeyAction(keyCode, flags);
|
|
return action;
|
|
}
|
|
|
|
}
|
|
|
|
/// main menu (horizontal)
|
|
class MainMenu : MenuWidgetBase {
|
|
|
|
this() {
|
|
super(null, null, Orientation.Horizontal);
|
|
id = "MAIN_MENU";
|
|
styleId = STYLE_MAIN_MENU;
|
|
_clickOnButtonDown = true;
|
|
selectOnHover = false;
|
|
}
|
|
|
|
this(MenuItem item) {
|
|
super(null, item, Orientation.Horizontal);
|
|
id = "MAIN_MENU";
|
|
styleId = STYLE_MAIN_MENU;
|
|
_clickOnButtonDown = true;
|
|
selectOnHover = false;
|
|
}
|
|
|
|
/// call to update state for action (if action is assigned for widget)
|
|
override void updateActionState(bool force) {
|
|
//Log.d("MainMenu: updateActionState");
|
|
//_item.updateActionState(this);
|
|
|
|
}
|
|
|
|
/// override and return true to track key events even when not focused
|
|
@property override bool wantsKeyTracking() {
|
|
return true;
|
|
}
|
|
|
|
/// get text flags (bit set of TextFlag enum values)
|
|
@property override uint textFlags() {
|
|
// override text flags for main menu
|
|
if (_selectedItemIndex >= 0)
|
|
return TextFlag.UnderlineHotKeys | TextFlag.HotKeys;
|
|
else
|
|
return TextFlag.UnderlineHotKeysWhenAltPressed | TextFlag.HotKeys;
|
|
}
|
|
|
|
protected int _menuToggleState;
|
|
protected Widget _menuTogglePreviousFocus;
|
|
|
|
override protected void onMenuItem(MenuItem item) {
|
|
debug(DebugMenus) Log.d("MainMenu.onMenuItem ", item.action.label);
|
|
|
|
// copy menu item click listeners
|
|
Signal!MenuItemClickHandler onMenuItemClickListenerCopy = menuItemClick;
|
|
// copy item action listeners
|
|
Signal!MenuItemActionHandler onMenuItemActionListenerCopy = menuItemAction;
|
|
|
|
deactivate();
|
|
|
|
handleMenuItemClick(item);
|
|
|
|
// this pointer now can be invalid - if popup removed
|
|
if (onMenuItemClickListenerCopy.assigned)
|
|
if (onMenuItemClickListenerCopy(item))
|
|
return;
|
|
// this pointer now can be invalid - if popup removed
|
|
if (onMenuItemActionListenerCopy.assigned)
|
|
onMenuItemActionListenerCopy(item.action);
|
|
}
|
|
|
|
/// return true if main menu is activated (focused or has open submenu)
|
|
@property bool activated() {
|
|
return focused || _selectedItemIndex >= 0 || _openedPopup !is null;
|
|
}
|
|
|
|
override protected void performUndoSelection() {
|
|
deactivate();
|
|
}
|
|
|
|
/// closes this menu - ESC handling
|
|
override void close() {
|
|
debug(DebugMenus) Log.d("menu ", id, " close called");
|
|
if (_openedPopup !is null) {
|
|
_openedPopup.close();
|
|
_openedPopup = null;
|
|
} else
|
|
deactivate();
|
|
}
|
|
|
|
/// request relayout of widget and its children
|
|
//override void requestLayout() {
|
|
// Log.d("MainMenu.requestLayout is called");
|
|
// super.requestLayout();
|
|
//}
|
|
|
|
/// bring focus to main menu, if not yet activated
|
|
void activate() {
|
|
debug(DebugMenus) Log.d("activating main menu");
|
|
if (activated)
|
|
return;
|
|
window.setFocus(this);
|
|
selectItem(0);
|
|
}
|
|
|
|
/// close and remove focus, if activated
|
|
void deactivate(bool force = false) {
|
|
debug(DebugMenus) Log.d("deactivating main menu");
|
|
if (!activated && !force)
|
|
return;
|
|
if (_openedPopup !is null) {
|
|
_openedPopup.close();
|
|
_openedPopup = null;
|
|
}
|
|
selectItem(-1);
|
|
setHoverItem(-1);
|
|
selectOnHover = false;
|
|
window.setFocus(_menuTogglePreviousFocus);
|
|
}
|
|
|
|
/// activate or deactivate main menu, return true if it has been activated
|
|
bool toggle() {
|
|
if (activated) {
|
|
// unfocus
|
|
deactivate();
|
|
return false;
|
|
} else {
|
|
// focus
|
|
activate();
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
/// override to handle focus changes
|
|
override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) {
|
|
debug(DebugMenus) Log.d("menu ", id, "handling focus change to ", focused);
|
|
if (focused && _openedPopup is null) {
|
|
// activating!
|
|
_menuTogglePreviousFocus = window.focusedWidget;
|
|
//updateActionState(true);
|
|
Log.d("MainMenu: updateActionState");
|
|
_item.updateActionState(this);
|
|
}
|
|
super.handleFocusChange(focused);
|
|
}
|
|
|
|
/// list navigation using keys
|
|
override bool onKeyEvent(KeyEvent event) {
|
|
// handle MainMenu activation / deactivation (Alt, Esc...)
|
|
bool toggleMenu = false;
|
|
bool isAlt = event.keyCode == KeyCode.ALT || event.keyCode == KeyCode.LALT || event.keyCode == KeyCode.RALT;
|
|
bool noOtherModifiers = !(event.flags & (KeyFlag.Shift | KeyFlag.Control));
|
|
|
|
if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.ESCAPE && event.flags == 0 && activated) {
|
|
deactivate();
|
|
return true;
|
|
}
|
|
if (event.action == KeyAction.Text && (event.flags & KeyFlag.Alt) && !(event.flags & KeyFlag.Shift) && !(event.flags & KeyFlag.Shift)) {
|
|
dchar ch = event.text[0];
|
|
int index = _item.findSubitemByHotkey(ch);
|
|
if (index >= 0) {
|
|
activate();
|
|
itemClicked(index);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (event.action == KeyAction.KeyDown && isAlt && noOtherModifiers) {
|
|
_menuToggleState = 1;
|
|
} else if (event.action == KeyAction.KeyUp && isAlt && noOtherModifiers) {
|
|
if (_menuToggleState == 1)
|
|
toggleMenu = true;
|
|
_menuToggleState = 0;
|
|
} else {
|
|
_menuToggleState = 0;
|
|
}
|
|
if (toggleMenu) {
|
|
toggle();
|
|
return true;
|
|
}
|
|
if (!focused)
|
|
return false;
|
|
if (_selectedItemIndex >= 0 && event.action == KeyAction.KeyDown && ((event.keyCode == KeyCode.DOWN) || (event.keyCode == KeyCode.SPACE) || (event.keyCode == KeyCode.RETURN))) {
|
|
itemClicked(_selectedItemIndex);
|
|
return true;
|
|
}
|
|
return super.onKeyEvent(event);
|
|
}
|
|
|
|
override @property protected uint overrideStateForItem() {
|
|
uint res = state;
|
|
if (_openedPopup)
|
|
res |= State.Focused; // main menu with opened popup as focused for items display
|
|
return res;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/// popup menu widget (vertical layout of items)
|
|
class PopupMenu : MenuWidgetBase {
|
|
|
|
this(MenuItem item, MenuWidgetBase parentMenu = null) {
|
|
super(parentMenu, item, Orientation.Vertical);
|
|
id = "POPUP_MENU";
|
|
styleId = STYLE_POPUP_MENU;
|
|
selectOnHover = true;
|
|
}
|
|
}
|