diff --git a/examples/example1/res/i18n/en.ini b/examples/example1/res/i18n/en.ini index d60ac061..94260b4c 100644 --- a/examples/example1/res/i18n/en.ini +++ b/examples/example1/res/i18n/en.ini @@ -1 +1,26 @@ -EXIT=Exit +EXIT=Exit +MENU_FILE=&File +MENU_FILE_OPEN=&Open +MENU_FILE_OPEN_RECENT=Open recent +MENU_FILE_SAVE=&Save +MENU_FILE_EXIT=E&xit +MENU_EDIT=&Edit +MENU_EDIT_COPY=&Copy +MENU_EDIT_PASTE=&Paste +MENU_EDIT_CUT=Cu&t +MENU_EDIT_UNDO=&Undo +MENU_EDIT_REDO=&Redo +MENU_EDIT_PREFERENCES=&Preferences +MENU_VIEW=&View +MENU_VIEW_LANGUAGE=Interface &Language +MENU_VIEW_LANGUAGE_EN=English +MENU_VIEW_LANGUAGE_RU=Russian +MENU_VIEW_THEME=&Theme +MENU_VIEW_THEME_DEFAULT=&Default +MENU_VIEW_THEME_CUSTOM1=&Custom 1 +MENU_WINDOW=&Window +MENU_WINDOW_PREFERENCES=&Preferences +MENU_HELP=&Help +MENU_HELP_VIEW_HELP=&View help +MENU_HELP_ABOUT=&About + diff --git a/examples/example1/src/main.d b/examples/example1/src/main.d index 988a20f2..d49d8e71 100644 --- a/examples/example1/src/main.d +++ b/examples/example1/src/main.d @@ -102,40 +102,52 @@ extern (C) int UIAppMain(string[] args) { static if (true) { VerticalLayout contentLayout = new VerticalLayout(); MenuItem mainMenuItems = new MenuItem(); - MenuItem fileItem = new MenuItem(new Action(1, "&File"d)); - fileItem.add(new Action(10, "&Open..."d, "document-open", KeyCode.KEY_O, KeyFlag.Control)); - fileItem.add(new Action(11, "&Save..."d, "document-save", KeyCode.KEY_S, KeyFlag.Control)); - MenuItem openRecentItem = new MenuItem(new Action(13, "Open recent..."d, "document-open-recent")); + MenuItem fileItem = new MenuItem(new Action(1, "MENU_FILE")); + fileItem.add(new Action(10, "MENU_FILE_OPEN"c, "document-open", KeyCode.KEY_O, KeyFlag.Control)); + fileItem.add(new Action(11, "MENU_FILE_SAVE"c, "document-save", KeyCode.KEY_S, KeyFlag.Control)); + MenuItem openRecentItem = new MenuItem(new Action(13, "MENU_FILE_OPEN_RECENT", "document-open-recent")); openRecentItem.add(new Action(100, "&1: File 1"d)); openRecentItem.add(new Action(101, "&2: File 2"d)); openRecentItem.add(new Action(102, "&3: File 3"d)); openRecentItem.add(new Action(103, "&4: File 4"d)); openRecentItem.add(new Action(104, "&5: File 5"d)); fileItem.add(openRecentItem); - fileItem.add(new Action(12, "E&xit"d, "document-close", KeyCode.KEY_X, KeyFlag.Alt)); - MenuItem editItem = new MenuItem(new Action(2, "&Edit"d)); - editItem.add(new Action(EditorActions.Copy, "Copy"d, "edit-copy", KeyCode.KEY_C, KeyFlag.Control)); - editItem.add(new Action(EditorActions.Paste, "Paste"d, "edit-paste", KeyCode.KEY_V, KeyFlag.Control)); - editItem.add(new Action(EditorActions.Cut, "Cut"d, "edit-cut", KeyCode.KEY_X, KeyFlag.Control)); - editItem.add(new Action(EditorActions.Undo, "Undo"d, "edit-undo", KeyCode.KEY_Z, KeyFlag.Control)); - editItem.add(new Action(EditorActions.Redo, "Redo"d, "edit-redo", KeyCode.KEY_Y, KeyFlag.Control)); - editItem.add(new Action(20, "Preferences..."d)); + fileItem.add(new Action(12, "MENU_FILE_EXIT"c, "document-close"c, KeyCode.KEY_X, KeyFlag.Alt)); + + MenuItem editItem = new MenuItem(new Action(2, "MENU_EDIT")); + editItem.add(new Action(EditorActions.Copy, "MENU_EDIT_COPY"c, "edit-copy", KeyCode.KEY_C, KeyFlag.Control)); + editItem.add(new Action(EditorActions.Paste, "MENU_EDIT_PASTE"c, "edit-paste", KeyCode.KEY_V, KeyFlag.Control)); + editItem.add(new Action(EditorActions.Cut, "MENU_EDIT_CUT"c, "edit-cut", KeyCode.KEY_X, KeyFlag.Control)); + editItem.add(new Action(EditorActions.Undo, "MENU_EDIT_UNDO"c, "edit-undo", KeyCode.KEY_Z, KeyFlag.Control)); + editItem.add(new Action(EditorActions.Redo, "MENU_EDIT_REDO"c, "edit-redo", KeyCode.KEY_Y, KeyFlag.Control)); + editItem.add(new Action(20, "MENU_EDIT_PREFERENCES")); MenuItem editPopupItem = new MenuItem(null); - editPopupItem.add(new Action(EditorActions.Copy, "Copy"d, "edit-copy", KeyCode.KEY_C, KeyFlag.Control)); - editPopupItem.add(new Action(EditorActions.Paste, "Paste"d, "edit-paste", KeyCode.KEY_V, KeyFlag.Control)); - editPopupItem.add(new Action(EditorActions.Cut, "Cut"d, "edit-cut", KeyCode.KEY_X, KeyFlag.Control)); - editPopupItem.add(new Action(EditorActions.Undo, "Undo"d, "edit-undo", KeyCode.KEY_Z, KeyFlag.Control)); - editPopupItem.add(new Action(EditorActions.Redo, "Redo"d, "edit-redo", KeyCode.KEY_Y, KeyFlag.Control)); + editPopupItem.add(new Action(EditorActions.Copy, "MENU_EDIT_COPY"c, "edit-copy", KeyCode.KEY_C, KeyFlag.Control)); + editPopupItem.add(new Action(EditorActions.Paste, "MENU_EDIT_PASTE"c, "edit-paste", KeyCode.KEY_V, KeyFlag.Control)); + editPopupItem.add(new Action(EditorActions.Cut, "MENU_EDIT_CUT"c, "edit-cut", KeyCode.KEY_X, KeyFlag.Control)); + editPopupItem.add(new Action(EditorActions.Undo, "MENU_EDIT_UNDO"c, "edit-undo", KeyCode.KEY_Z, KeyFlag.Control)); + editPopupItem.add(new Action(EditorActions.Redo, "MENU_EDIT_REDO"c, "edit-redo", KeyCode.KEY_Y, KeyFlag.Control)); - MenuItem windowItem = new MenuItem(new Action(3, "&Window"d)); - windowItem.add(new Action(30, "Preferences"d)); - MenuItem helpItem = new MenuItem(new Action(4, "Help"d)); - helpItem.add(new Action(40, "View Help"d)); - helpItem.add(new Action(41, "About"d)); + MenuItem viewItem = new MenuItem(new Action(60, "MENU_VIEW")); + MenuItem langItem = new MenuItem(new Action(61, "MENU_VIEW_LANGUAGE")); + langItem.add((new MenuItem(new Action(611, "MENU_VIEW_LANGUAGE_EN"))).type(MenuItemType.Radio).checked(true)); + langItem.add((new MenuItem(new Action(612, "MENU_VIEW_LANGUAGE_RU"))).type(MenuItemType.Radio)); + MenuItem themeItem = new MenuItem(new Action(62, "MENU_VIEW_THEME")); + themeItem.add((new MenuItem(new Action(621, "MENU_VIEW_THEME_DEFAULT"))).type(MenuItemType.Radio).checked(true)); + themeItem.add((new MenuItem(new Action(622, "MENU_VIEW_THEME_CUSTOM1"))).type(MenuItemType.Radio)); + viewItem.add(langItem); + viewItem.add(themeItem); + + MenuItem windowItem = new MenuItem(new Action(3, "MENU_WINDOW")); + windowItem.add(new Action(30, "MENU_WINDOW_PREFERENCES")); + MenuItem helpItem = new MenuItem(new Action(4, "MENU_HELP")); + helpItem.add(new Action(40, "MENU_HELP_VIEW_HELP")); + helpItem.add(new Action(41, "MENU_HELP_ABOUT")); mainMenuItems.add(fileItem); mainMenuItems.add(editItem); - mainMenuItems.add(windowItem); + mainMenuItems.add(viewItem); + mainMenuItems.add(windowItem); mainMenuItems.add(helpItem); MainMenu mainMenu = new MainMenu(mainMenuItems); mainMenu.onMenuItemClickListener = delegate(MenuItem item) { diff --git a/src/dlangui/widgets/menu.d b/src/dlangui/widgets/menu.d index 65e07930..82cf482c 100644 --- a/src/dlangui/widgets/menu.d +++ b/src/dlangui/widgets/menu.d @@ -28,24 +28,91 @@ import dlangui.widgets.layouts; import dlangui.widgets.lists; import dlangui.widgets.popup; +/// 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 +} + /// menu item properties class MenuItem { - protected bool _checkable; protected bool _checked; protected bool _enabled; + protected MenuItemType _type = MenuItemType.Normal; protected Action _action; protected MenuItem[] _subitems; + protected MenuItem _parent; /// item action id, 0 if no action @property int id() { 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]; } + @property MenuItemType type() const { + 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() { dstring s = label; @@ -74,12 +141,12 @@ class MenuItem { /// adds submenu item MenuItem add(MenuItem subitem) { _subitems ~= subitem; + subitem._parent = this; return this; } /// adds submenu item from action MenuItem add(Action subitemAction) { - _subitems ~= new MenuItem(subitemAction); - return this; + return add(new MenuItem(subitemAction)); } /// returns text description for first accelerator of action; null if no accelerators @property dstring acceleratorText() { @@ -101,7 +168,7 @@ class MenuItem { @property MenuItem action(Action a) { _action = a; return this; } /// menu item Enabled flag - @property bool enabled() { return _enabled; } + @property bool enabled() { return _enabled && type != MenuItemType.Separator; } /// menu item Enabled flag @property MenuItem enabled(bool enabled) { _enabled = enabled; @@ -166,6 +233,7 @@ class MenuItemWidget : WidgetGroup { } /// 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 @@ -206,6 +274,17 @@ class MenuItemWidget : WidgetGroup { _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); + } + /// Draw widget at its position to buffer override void onDraw(DrawBuf buf) { if (visibility != Visibility.Visible) @@ -214,6 +293,7 @@ class MenuItemWidget : WidgetGroup { Rect rc = _pos; applyMargins(rc); applyPadding(rc); + updateState(); auto saver = ClipRectSaver(buf, rc, alpha); for (int i = 0; i < _children.count; i++) { Widget item = _children.get(i); @@ -228,11 +308,15 @@ class MenuItemWidget : WidgetGroup { _mainMenu = mainMenu; _item = item; styleId = "MENU_ITEM"; - if (!item.enabled) - resetState(State.Enabled); + updateState(); + string iconId = _item.action.iconId; + if (_item.type == MenuItemType.Check) + iconId = "btn_check"; + else if (_item.type == MenuItemType.Radio) + iconId = "btn_radio"; // icon - if (_item.action && _item.action.iconId.length) { - _icon = new ImageWidget("MENU_ICON", _item.action.iconId); + if (_item.action && iconId.length) { + _icon = new ImageWidget("MENU_ICON", iconId); _icon.styleId = "MENU_ICON"; _icon.state = State.Parent; addChild(_icon); @@ -245,11 +329,15 @@ class MenuItemWidget : WidgetGroup { addChild(_label); // accelerator dstring acc = _item.acceleratorText; + if (_item.isSubmenu && !mainMenu) + acc = "‣"d; if (acc !is null) { _accel = new TextWidget("MENU_ACCEL"); _accel.styleId = "MENU_ACCEL"; _accel.text = acc; _accel.state = State.Parent; + if (_item.isSubmenu && !mainMenu) + _accel.alignment = Align.Right | Align.VCenter; addChild(_accel); } trackHover = true; @@ -401,6 +489,15 @@ class MenuWidgetBase : ListWidget { } } + 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; + } + } + protected void onMenuItem(MenuItem item) { debug Log.d("onMenuItem ", item.action.label); if (_openedPopup !is null) { @@ -424,6 +521,10 @@ class MenuWidgetBase : ListWidget { PopupWidget popup = cast(PopupWidget)parent; if (popup) popup.close(); + + handleMenuItemClick(item); + + // this pointer now can be invalid - if popup removed if (onMenuItemClickListenerCopy.assigned) if (onMenuItemClickListenerCopy(item)) @@ -566,6 +667,8 @@ class MainMenu : MenuWidgetBase { deactivate(); + handleMenuItemClick(item); + // this pointer now can be invalid - if popup removed if (onMenuItemClickListenerCopy.assigned) if (onMenuItemClickListenerCopy(item))