c1509d55bafa220805b74ecc7f3a1045b8d8de84
[nikiroo-utils.git] / src / jexer / menu / TMenu.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * License: LGPLv3 or later
5 *
6 * This module is licensed under the GNU Lesser General Public License
7 * Version 3. Please see the file "COPYING" in this directory for more
8 * information about the GNU Lesser General Public License Version 3.
9 *
10 * Copyright (C) 2015 Kevin Lamonte
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Lesser General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU Lesser General Public
23 * License along with this program; if not, see
24 * http://www.gnu.org/licenses/, or write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
26 * 02110-1301 USA
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
30 */
31 package jexer.menu;
32
33 import jexer.TApplication;
34 import jexer.TKeypress;
35 import jexer.TWidget;
36 import jexer.TWindow;
37 import jexer.bits.CellAttributes;
38 import jexer.bits.GraphicsChars;
39 import jexer.bits.MnemonicString;
40 import jexer.event.TKeypressEvent;
41 import jexer.event.TMouseEvent;
42 import static jexer.TKeypress.*;
43
44 /**
45 * TMenu is a top-level collection of TMenuItems.
46 */
47 public final class TMenu extends TWindow {
48
49 /**
50 * If true, this is a sub-menu. Note package private access.
51 */
52 boolean isSubMenu = false;
53
54 /**
55 * The shortcut and title.
56 */
57 private MnemonicString mnemonic;
58
59 /**
60 * Get the mnemonic string.
61 *
62 * @return the full mnemonic string
63 */
64 public MnemonicString getMnemonic() {
65 return mnemonic;
66 }
67
68 // Reserved menu item IDs
69 public static final int MID_UNUSED = -1;
70
71 // File menu
72 public static final int MID_EXIT = 1;
73 public static final int MID_QUIT = MID_EXIT;
74 public static final int MID_OPEN_FILE = 2;
75 public static final int MID_SHELL = 3;
76
77 // Edit menu
78 public static final int MID_CUT = 10;
79 public static final int MID_COPY = 11;
80 public static final int MID_PASTE = 12;
81 public static final int MID_CLEAR = 13;
82
83 // Window menu
84 public static final int MID_TILE = 20;
85 public static final int MID_CASCADE = 21;
86 public static final int MID_CLOSE_ALL = 22;
87 public static final int MID_WINDOW_MOVE = 23;
88 public static final int MID_WINDOW_ZOOM = 24;
89 public static final int MID_WINDOW_NEXT = 25;
90 public static final int MID_WINDOW_PREVIOUS = 26;
91 public static final int MID_WINDOW_CLOSE = 27;
92
93 /**
94 * Public constructor.
95 *
96 * @param parent parent application
97 * @param x column relative to parent
98 * @param y row relative to parent
99 * @param label mnemonic menu title. Label must contain a keyboard
100 * shortcut (mnemonic), denoted by prefixing a letter with "&",
101 * e.g. "&File"
102 */
103 public TMenu(final TApplication parent, final int x, final int y,
104 final String label) {
105
106 super(parent, label, x, y, parent.getScreen().getWidth(),
107 parent.getScreen().getHeight());
108
109 // My parent constructor added me as a window, get rid of that
110 parent.closeWindow(this);
111
112 // Setup the menu shortcut
113 mnemonic = new MnemonicString(label);
114 setTitle(mnemonic.getRawLabel());
115 assert (mnemonic.getShortcutIdx() >= 0);
116
117 // Recompute width and height to reflect an empty menu
118 setWidth(getTitle().length() + 4);
119 setHeight(2);
120
121 setActive(false);
122 }
123
124 /**
125 * Draw a top-level menu with title and menu items.
126 */
127 @Override
128 public void draw() {
129 CellAttributes background = getTheme().getColor("tmenu");
130
131 assert (isAbsoluteActive());
132
133 // Fill in the interior background
134 for (int i = 0; i < getHeight(); i++) {
135 hLineXY(0, i, getWidth(), ' ', background);
136 }
137
138 // Draw the box
139 char cTopLeft;
140 char cTopRight;
141 char cBottomLeft;
142 char cBottomRight;
143 char cHSide;
144
145 cTopLeft = GraphicsChars.ULCORNER;
146 cTopRight = GraphicsChars.URCORNER;
147 cBottomLeft = GraphicsChars.LLCORNER;
148 cBottomRight = GraphicsChars.LRCORNER;
149 cHSide = GraphicsChars.SINGLE_BAR;
150
151 // Place the corner characters
152 putCharXY(1, 0, cTopLeft, background);
153 putCharXY(getWidth() - 2, 0, cTopRight, background);
154 putCharXY(1, getHeight() - 1, cBottomLeft, background);
155 putCharXY(getWidth() - 2, getHeight() - 1, cBottomRight, background);
156
157 // Draw the box lines
158 hLineXY(1 + 1, 0, getWidth() - 4, cHSide, background);
159 hLineXY(1 + 1, getHeight() - 1, getWidth() - 4, cHSide, background);
160
161 // Draw a shadow
162 getScreen().drawBoxShadow(0, 0, getWidth(), getHeight());
163 }
164
165 /**
166 * Handle mouse button presses.
167 *
168 * @param mouse mouse button event
169 */
170 @Override
171 public void onMouseDown(final TMouseEvent mouse) {
172 this.mouse = mouse;
173
174 // Pass to children
175 for (TWidget widget: getChildren()) {
176 if (widget.mouseWouldHit(mouse)) {
177 // Dispatch to this child, also activate it
178 activate(widget);
179
180 // Set x and y relative to the child's coordinates
181 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
182 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
183 widget.handleEvent(mouse);
184 return;
185 }
186 }
187 }
188
189 /**
190 * Handle mouse button releases.
191 *
192 * @param mouse mouse button release event
193 */
194 @Override
195 public void onMouseUp(final TMouseEvent mouse) {
196 this.mouse = mouse;
197
198 // Pass to children
199 for (TWidget widget: getChildren()) {
200 if (widget.mouseWouldHit(mouse)) {
201 // Dispatch to this child, also activate it
202 activate(widget);
203
204 // Set x and y relative to the child's coordinates
205 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
206 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
207 widget.handleEvent(mouse);
208 return;
209 }
210 }
211 }
212
213 /**
214 * Handle mouse movements.
215 *
216 * @param mouse mouse motion event
217 */
218 @Override
219 public void onMouseMotion(final TMouseEvent mouse) {
220 this.mouse = mouse;
221
222 // See if we should activate a different menu item
223 for (TWidget widget: getChildren()) {
224 if ((mouse.isMouse1())
225 && (widget.mouseWouldHit(mouse))
226 ) {
227 // Activate this menu item
228 activate(widget);
229 if (widget instanceof TSubMenu) {
230 ((TSubMenu) widget).dispatch();
231 }
232 return;
233 }
234 }
235 }
236
237 /**
238 * Handle keystrokes.
239 *
240 * @param keypress keystroke event
241 */
242 @Override
243 public void onKeypress(final TKeypressEvent keypress) {
244
245 /*
246 System.err.printf("keypress: %s active child: %s\n", keypress,
247 getActiveChild());
248 */
249
250 if (getActiveChild() != this) {
251 if ((getActiveChild() instanceof TSubMenu)
252 || (getActiveChild() instanceof TMenu)
253 ) {
254 getActiveChild().onKeypress(keypress);
255 return;
256 }
257 }
258
259 if (keypress.equals(kbEsc)) {
260 getApplication().closeMenu();
261 return;
262 }
263 if (keypress.equals(kbDown)) {
264 switchWidget(true);
265 return;
266 }
267 if (keypress.equals(kbUp)) {
268 switchWidget(false);
269 return;
270 }
271 if (keypress.equals(kbRight)) {
272 getApplication().switchMenu(true);
273 return;
274 }
275 if (keypress.equals(kbLeft)) {
276 if (isSubMenu) {
277 getApplication().closeSubMenu();
278 } else {
279 getApplication().switchMenu(false);
280 }
281 return;
282 }
283
284 // Switch to a menuItem if it has an mnemonic
285 if (!keypress.getKey().isFnKey()
286 && !keypress.getKey().isAlt()
287 && !keypress.getKey().isCtrl()) {
288 for (TWidget widget: getChildren()) {
289 TMenuItem item = (TMenuItem) widget;
290 if ((item.getMnemonic() != null)
291 && (Character.toLowerCase(item.getMnemonic().getShortcut())
292 == Character.toLowerCase(keypress.getKey().getChar()))
293 ) {
294 // Send an enter keystroke to it
295 activate(item);
296 item.handleEvent(new TKeypressEvent(kbEnter));
297 return;
298 }
299 }
300 }
301
302 // Dispatch the keypress to an active widget
303 for (TWidget widget: getChildren()) {
304 if (widget.isActive()) {
305 widget.handleEvent(keypress);
306 return;
307 }
308 }
309 }
310
311 /**
312 * Convenience function to add a menu item.
313 *
314 * @param id menu item ID. Must be greater than 1024.
315 * @param label menu item label
316 * @return the new menu item
317 */
318 public TMenuItem addItem(final int id, final String label) {
319 assert (id >= 1024);
320 return addItemInternal(id, label, null);
321 }
322
323 /**
324 * Convenience function to add a custom menu item.
325 *
326 * @param id menu item ID. Must be greater than 1024.
327 * @param label menu item label
328 * @param key global keyboard accelerator
329 * @return the new menu item
330 */
331 public TMenuItem addItem(final int id, final String label,
332 final TKeypress key) {
333
334 assert (id >= 1024);
335 return addItemInternal(id, label, key);
336 }
337
338 /**
339 * Convenience function to add a custom menu item.
340 *
341 * @param id menu item ID. Must be greater than 1024.
342 * @param label menu item label
343 * @param key global keyboard accelerator
344 * @return the new menu item
345 */
346 private TMenuItem addItemInternal(final int id, final String label,
347 final TKeypress key) {
348
349 int newY = getChildren().size() + 1;
350 assert (newY < getHeight());
351
352 TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
353 menuItem.setKey(key);
354 setHeight(getHeight() + 1);
355 if (menuItem.getWidth() + 2 > getWidth()) {
356 setWidth(menuItem.getWidth() + 2);
357 }
358 for (TWidget widget: getChildren()) {
359 widget.setWidth(getWidth() - 2);
360 }
361 getApplication().addMenuItem(menuItem);
362 getApplication().recomputeMenuX();
363 activate(0);
364 return menuItem;
365 }
366
367 /**
368 * Convenience function to add one of the default menu items.
369 *
370 * @param id menu item ID. Must be between 0 (inclusive) and 1023
371 * (inclusive).
372 * @return the new menu item
373 */
374 public TMenuItem addDefaultItem(final int id) {
375 assert (id >= 0);
376 assert (id < 1024);
377
378 String label;
379 TKeypress key = null;
380
381 switch (id) {
382
383 case MID_EXIT:
384 label = "E&xit";
385 key = kbAltX;
386 break;
387
388 case MID_SHELL:
389 label = "O&S Shell";
390 break;
391
392 case MID_OPEN_FILE:
393 label = "&Open";
394 key = kbAltO;
395 break;
396
397 case MID_CUT:
398 label = "Cu&t";
399 key = kbCtrlX;
400 break;
401 case MID_COPY:
402 label = "&Copy";
403 key = kbCtrlC;
404 break;
405 case MID_PASTE:
406 label = "&Paste";
407 key = kbCtrlV;
408 break;
409 case MID_CLEAR:
410 label = "C&lear";
411 // key = kbDel;
412 break;
413
414 case MID_TILE:
415 label = "&Tile";
416 break;
417 case MID_CASCADE:
418 label = "C&ascade";
419 break;
420 case MID_CLOSE_ALL:
421 label = "Cl&ose All";
422 break;
423 case MID_WINDOW_MOVE:
424 label = "&Size/Move";
425 key = kbCtrlF5;
426 break;
427 case MID_WINDOW_ZOOM:
428 label = "&Zoom";
429 key = kbF5;
430 break;
431 case MID_WINDOW_NEXT:
432 label = "&Next";
433 key = kbF6;
434 break;
435 case MID_WINDOW_PREVIOUS:
436 label = "&Previous";
437 key = kbShiftF6;
438 break;
439 case MID_WINDOW_CLOSE:
440 label = "&Close";
441 // key = kbCtrlW;
442 break;
443
444 default:
445 throw new IllegalArgumentException("Invalid menu ID: " + id);
446 }
447
448 return addItemInternal(id, label, key);
449 }
450
451 /**
452 * Convenience function to add a menu separator.
453 */
454 public void addSeparator() {
455 int newY = getChildren().size() + 1;
456 assert (newY < getHeight());
457
458 // We just have to construct it, don't need to hang onto what it
459 // makes.
460 new TMenuSeparator(this, 1, newY);
461 setHeight(getHeight() + 1);
462 }
463
464 /**
465 * Convenience function to add a sub-menu.
466 *
467 * @param title menu title. Title must contain a keyboard shortcut,
468 * denoted by prefixing a letter with "&", e.g. "&File"
469 * @return the new sub-menu
470 */
471 public TSubMenu addSubMenu(final String title) {
472 int newY = getChildren().size() + 1;
473 assert (newY < getHeight());
474
475 TSubMenu subMenu = new TSubMenu(this, title, 1, newY);
476 setHeight(getHeight() + 1);
477 if (subMenu.getWidth() + 2 > getWidth()) {
478 setWidth(subMenu.getWidth() + 2);
479 }
480 for (TWidget widget: getChildren()) {
481 widget.setWidth(getWidth() - 2);
482 }
483 getApplication().recomputeMenuX();
484 activate(0);
485 subMenu.menu.setX(getX() + getWidth() - 2);
486
487 return subMenu;
488 }
489
490 }