1 // Written in the D programming language.
2 
3 /**
4 DLANGUI library.
5 
6 This module contains menu widgets implementation.
7 
8 
9 
10 Synopsis:
11 
12 ----
13 import dlangui.widgets.popup;
14 
15 ----
16 
17 Copyright: Vadim Lopatin, 2014
18 License:   $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
19 Authors:   $(WEB coolreader.org, Vadim Lopatin)
20 */
21 module dlangui.widgets.menu;
22 
23 import dlangui.core.events;
24 import dlangui.widgets.controls;
25 import dlangui.widgets.layouts;
26 import dlangui.widgets.lists;
27 import dlangui.widgets.popup;
28 
29 /// menu item properties
30 class MenuItem {
31     protected bool _checkable;
32     protected bool _checked;
33     protected bool _enabled;
34     protected Action _action;
35     protected MenuItem[] _subitems;
36     /// item action id, 0 if no action
37     @property int id() { return _action is null ? 0 : _action.id; }
38     /// returns count of submenu items
39     @property int subitemCount() {
40         return cast(int)_subitems.length;
41     }
42     /// returns submenu item by index
43     MenuItem subitem(int index) {
44         return _subitems[index];
45     }
46     /// adds submenu item
47     MenuItem add(MenuItem subitem) {
48         _subitems ~= subitem;
49         return this;
50     }
51     /// adds submenu item from action
52     MenuItem add(Action subitemAction) {
53         _subitems ~= new MenuItem(subitemAction);
54         return this;
55     }
56     /// returns true if item is submenu (contains subitems)
57     @property bool isSubmenu() {
58         return _subitems.length > 0;
59     }
60     /// returns item label
61     @property UIString label() {
62         return _action.labelValue;
63     }
64     /// returns item action
65     @property const(Action) action() const { return _action; }
66     /// sets item action
67     @property MenuItem action(Action a) { _action = a; return this; }
68     this() {
69         _enabled = true;
70     }
71     this(Action action) {
72         _action = action;
73         _enabled = true;
74     }
75     ~this() {
76         // TODO
77     }
78 }
79 
80 /// widget to draw menu item
81 class MenuItemWidget : HorizontalLayout {
82     protected MenuItem _item;
83     protected TextWidget _label;
84     @property MenuItem item() { return _item; }
85     this(MenuItem item) {
86         id="menuitem";
87         _item = item;
88         styleId = "MENU_ITEM";
89         _label = new TextWidget("MENU_LABEL");
90         _label.text = _item.label;
91         addChild(_label);
92         trackHover = true;
93 		clickable = true;
94     }
95 }
96 
97 /// base class for menus
98 class MenuWidgetBase : ListWidget {
99 	protected MenuWidgetBase _parentMenu;
100     protected MenuItem _item;
101 	protected PopupMenu _openedMenu;
102 	protected PopupWidget _openedPopup;
103 	protected bool delegate(MenuItem item) _onMenuItemClickListener;
104     /// menu item click listener
105 	@property bool delegate(MenuItem item) onMenuItemListener() { return  _onMenuItemClickListener; }
106     /// menu item click listener
107 	@property MenuWidgetBase onMenuItemListener(bool delegate(MenuItem item) listener) { _onMenuItemClickListener = listener; return this; }
108 
109     this(MenuWidgetBase parentMenu, MenuItem item, Orientation orientation) {
110 		_parentMenu = parentMenu;
111         _item = item;
112 		this.orientation = orientation;
113         id = "popup_menu";
114         styleId = "POPUP_MENU";
115         WidgetListAdapter adapter = new WidgetListAdapter();
116         for (int i=0; i < _item.subitemCount; i++) {
117             MenuItem subitem = _item.subitem(i);
118             MenuItemWidget widget = new MenuItemWidget(subitem);
119 			if (orientation == Orientation.Horizontal)
120 				widget.styleId = "MAIN_MENU_ITEM";
121             adapter.widgets.add(widget);
122         }
123         ownAdapter = adapter;
124     }
125 
126     protected void onPopupClosed(PopupWidget p) {
127         _openedPopup = null;
128         _openedMenu = null;
129         selectItem(-1);
130         setHoverItem(-1);
131         window.setFocus(this);
132     }
133 
134 	protected void openSubmenu(MenuItemWidget itemWidget, bool selectFirstItem) {
135 		if (_openedPopup !is null) {
136 			_openedPopup.close();
137         }
138 		PopupMenu popupMenu = new PopupMenu(itemWidget.item, this);
139 		PopupWidget popup = window.showPopup(popupMenu, itemWidget, orientation == Orientation.Horizontal ? PopupAlign.Below :  PopupAlign.Right);
140         popup.onPopupCloseListener = &onPopupClosed;
141         popup.flags = PopupFlags.CloseOnClickOutside;
142 		_openedPopup = popup;
143 		_openedMenu = popupMenu;
144         window.setFocus(popupMenu);
145         if (selectFirstItem)
146             _openedMenu.selectItem(0);
147 	}
148 
149 	/// override to handle change of selection
150 	override protected void selectionChanged(int index, int previouslySelectedItem = -1) {
151 		MenuItemWidget itemWidget = index >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(index) : null;
152 		MenuItemWidget prevWidget = previouslySelectedItem >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(previouslySelectedItem) : null;
153 		if (prevWidget !is null) {
154 			if (_openedPopup !is null)
155 				_openedPopup.close();
156 		}
157 		if (itemWidget !is null) {
158 			if (itemWidget.item.isSubmenu()) {
159 				if (_selectOnHover) {
160 					openSubmenu(itemWidget, false);
161 				}
162 			} else {
163 				// normal item
164 			}
165 		}
166 	}
167 
168 	protected void onMenuItem(MenuItem item) {
169 		if (_openedPopup !is null) {
170 			_openedPopup.close();
171 			_openedPopup = null;
172 		}
173 		if (_parentMenu !is null)
174 			_parentMenu.onMenuItem(item);
175 		else {
176 			// top level handling
177 			Log.d("onMenuItem ", item.id);
178 			selectItem(-1);
179             setHoverItem(-1);
180 			selectOnHover = false;
181 			bool delegate(MenuItem item) listener = _onMenuItemClickListener;
182 			PopupWidget popup = cast(PopupWidget)parent;
183 			if (popup)
184 				popup.close();
185 			// this pointer now can be invalid - if popup removed
186 			if (listener !is null)
187 				listener(item);
188 		}
189 	}
190 
191     @property MenuItemWidget selectedMenuItemWidget() {
192         return _selectedItemIndex >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(_selectedItemIndex) : null;
193     }
194 
195 	/// override to handle mouse up on item
196 	override protected void itemClicked(int index) {
197 		MenuItemWidget itemWidget = index >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(index) : null;
198 		if (itemWidget !is null) {
199 			Log.d("Menu Item clicked ", itemWidget.item.action.id);
200 			if (itemWidget.item.isSubmenu()) {
201 				// submenu clicked
202 				if (_clickOnButtonDown && _openedPopup !is null && _openedMenu._item is itemWidget.item) {
203 					// second click on main menu opened item
204 					_openedPopup.close();
205 					_openedPopup = null;
206 					selectItem(-1);
207 					selectOnHover = false;
208 				} else {
209 					openSubmenu(itemWidget, false);
210 					selectOnHover = true;
211 				}
212 			} else {
213 				// normal item
214 				onMenuItem(itemWidget.item);
215 			}
216 		}
217 	}
218 
219     /// returns popup this menu is located in
220     @property PopupWidget thisPopup() {
221         return cast(PopupWidget)parent;
222     }
223 
224     /// list navigation using keys
225     override bool onKeyEvent(KeyEvent event) {
226         // TODO:
227         if (orientation == Orientation.Horizontal) {
228             if (event.action == KeyAction.KeyDown) {
229             }
230         } else {
231             if (event.action == KeyAction.KeyDown) {
232                 if (event.keyCode == KeyCode.LEFT) {
233                     if (_parentMenu !is null) {
234                         if (_parentMenu.orientation == Orientation.Vertical) {
235                             if (thisPopup !is null) {
236                                 // back to parent menu on Left key
237                                 thisPopup.close();
238                                 return true;
239                             }
240                         } else {
241                             // parent is main menu
242                             _parentMenu.moveSelection(-1);
243                             return true;
244                         }
245                     }
246                     return true;
247                 } else if (event.keyCode == KeyCode.RIGHT) {
248                     MenuItemWidget thisItem = selectedMenuItemWidget();
249                     if (thisItem !is null && thisItem.item.isSubmenu) {
250                         openSubmenu(thisItem, true);
251                         return true;
252                     } else if (_parentMenu !is null && _parentMenu.orientation == Orientation.Horizontal) {
253                         _parentMenu.moveSelection(1);
254                         return true;
255                     }
256                     return true;
257                 }
258             } else if (event.action == KeyAction.KeyUp) {
259                 if (event.keyCode == KeyCode.LEFT || event.keyCode == KeyCode.RIGHT) {
260                     return true;
261                 }
262             }
263         }
264         return super.onKeyEvent(event);
265     }
266 
267 }
268 
269 /// main menu (horizontal)
270 class MainMenu : MenuWidgetBase {
271 
272     this(MenuItem item) {
273 		super(null, item, Orientation.Horizontal);
274         id = "MAIN_MENU";
275         styleId = "MAIN_MENU";
276 		_clickOnButtonDown = true;
277     }
278 }
279 
280 /// popup menu widget (vertical layout of items)
281 class PopupMenu : MenuWidgetBase {
282 
283     this(MenuItem item, MenuWidgetBase parentMenu = null) {
284 		super(parentMenu, item, Orientation.Vertical);
285         id = "POPUP_MENU";
286         styleId = "POPUP_MENU";
287 		selectOnHover = true;
288     }
289 }