Fix ClassCastException
[nikiroo-utils.git] / src / jexer / menu / TMenu.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2017 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.menu;
30
31 import java.util.ResourceBundle;
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 * Translated strings.
51 */
52 private static final ResourceBundle i18n = ResourceBundle.getBundle(TMenu.class.getName());
53
54 /**
55 * If true, this is a sub-menu. Note package private access.
56 */
57 boolean isSubMenu = false;
58
59 /**
60 * The shortcut and title.
61 */
62 private MnemonicString mnemonic;
63
64 /**
65 * Get the mnemonic string.
66 *
67 * @return the full mnemonic string
68 */
69 public MnemonicString getMnemonic() {
70 return mnemonic;
71 }
72
73 // Reserved menu item IDs
74 public static final int MID_UNUSED = -1;
75
76 // File menu
77 public static final int MID_EXIT = 1;
78 public static final int MID_QUIT = MID_EXIT;
79 public static final int MID_OPEN_FILE = 2;
80 public static final int MID_SHELL = 3;
81
82 // Edit menu
83 public static final int MID_CUT = 10;
84 public static final int MID_COPY = 11;
85 public static final int MID_PASTE = 12;
86 public static final int MID_CLEAR = 13;
87
88 // Window menu
89 public static final int MID_TILE = 20;
90 public static final int MID_CASCADE = 21;
91 public static final int MID_CLOSE_ALL = 22;
92 public static final int MID_WINDOW_MOVE = 23;
93 public static final int MID_WINDOW_ZOOM = 24;
94 public static final int MID_WINDOW_NEXT = 25;
95 public static final int MID_WINDOW_PREVIOUS = 26;
96 public static final int MID_WINDOW_CLOSE = 27;
97
98 // Help menu
99 public static final int MID_HELP_CONTENTS = 40;
100 public static final int MID_HELP_INDEX = 41;
101 public static final int MID_HELP_SEARCH = 42;
102 public static final int MID_HELP_PREVIOUS = 43;
103 public static final int MID_HELP_HELP = 44;
104 public static final int MID_HELP_ACTIVE_FILE = 45;
105 public static final int MID_ABOUT = 46;
106
107 // Other
108 public static final int MID_REPAINT = 50;
109
110 /**
111 * Public constructor.
112 *
113 * @param parent parent application
114 * @param x column relative to parent
115 * @param y row relative to parent
116 * @param label mnemonic menu title. Label must contain a keyboard
117 * shortcut (mnemonic), denoted by prefixing a letter with "&",
118 * e.g. "&File"
119 */
120 public TMenu(final TApplication parent, final int x, final int y,
121 final String label) {
122
123 super(parent, label, x, y, parent.getScreen().getWidth(),
124 parent.getScreen().getHeight());
125
126 // Setup the menu shortcut
127 mnemonic = new MnemonicString(label);
128 setTitle(mnemonic.getRawLabel());
129 assert (mnemonic.getShortcutIdx() >= 0);
130
131 // Recompute width and height to reflect an empty menu
132 setWidth(getTitle().length() + 4);
133 setHeight(2);
134
135 setActive(false);
136 }
137
138 /**
139 * Draw a top-level menu with title and menu items.
140 */
141 @Override
142 public void draw() {
143 CellAttributes background = getTheme().getColor("tmenu");
144
145 assert (isAbsoluteActive());
146
147 // Fill in the interior background
148 for (int i = 0; i < getHeight(); i++) {
149 hLineXY(0, i, getWidth(), ' ', background);
150 }
151
152 // Draw the box
153 char cTopLeft;
154 char cTopRight;
155 char cBottomLeft;
156 char cBottomRight;
157 char cHSide;
158
159 cTopLeft = GraphicsChars.ULCORNER;
160 cTopRight = GraphicsChars.URCORNER;
161 cBottomLeft = GraphicsChars.LLCORNER;
162 cBottomRight = GraphicsChars.LRCORNER;
163 cHSide = GraphicsChars.SINGLE_BAR;
164
165 // Place the corner characters
166 putCharXY(1, 0, cTopLeft, background);
167 putCharXY(getWidth() - 2, 0, cTopRight, background);
168 putCharXY(1, getHeight() - 1, cBottomLeft, background);
169 putCharXY(getWidth() - 2, getHeight() - 1, cBottomRight, background);
170
171 // Draw the box lines
172 hLineXY(1 + 1, 0, getWidth() - 4, cHSide, background);
173 hLineXY(1 + 1, getHeight() - 1, getWidth() - 4, cHSide, background);
174
175 // Draw a shadow
176 getScreen().drawBoxShadow(0, 0, getWidth(), getHeight());
177 }
178
179 /**
180 * Handle mouse button presses.
181 *
182 * @param mouse mouse button event
183 */
184 @Override
185 public void onMouseDown(final TMouseEvent mouse) {
186 this.mouse = mouse;
187
188 // Pass to children
189 for (TWidget widget: getChildren()) {
190 if (widget.mouseWouldHit(mouse)) {
191 // Dispatch to this child, also activate it
192 activate(widget);
193
194 // Set x and y relative to the child's coordinates
195 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
196 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
197 widget.handleEvent(mouse);
198 return;
199 }
200 }
201 }
202
203 /**
204 * Handle mouse button releases.
205 *
206 * @param mouse mouse button release event
207 */
208 @Override
209 public void onMouseUp(final TMouseEvent mouse) {
210 this.mouse = mouse;
211
212 // Pass to children
213 for (TWidget widget: getChildren()) {
214 if (widget.mouseWouldHit(mouse)) {
215 // Dispatch to this child, also activate it
216 activate(widget);
217
218 // Set x and y relative to the child's coordinates
219 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
220 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
221 widget.handleEvent(mouse);
222 return;
223 }
224 }
225 }
226
227 /**
228 * Handle mouse movements.
229 *
230 * @param mouse mouse motion event
231 */
232 @Override
233 public void onMouseMotion(final TMouseEvent mouse) {
234 this.mouse = mouse;
235
236 // See if we should activate a different menu item
237 for (TWidget widget: getChildren()) {
238 if ((mouse.isMouse1())
239 && (widget.mouseWouldHit(mouse))
240 ) {
241 // Activate this menu item
242 activate(widget);
243 if (widget instanceof TSubMenu) {
244 ((TSubMenu) widget).dispatch();
245 }
246 return;
247 }
248 }
249 }
250
251 /**
252 * Handle keystrokes.
253 *
254 * @param keypress keystroke event
255 */
256 @Override
257 public void onKeypress(final TKeypressEvent keypress) {
258
259 /*
260 System.err.printf("keypress: %s active child: %s\n", keypress,
261 getActiveChild());
262 */
263
264 if (getActiveChild() != this) {
265 if ((getActiveChild() instanceof TSubMenu)
266 || (getActiveChild() instanceof TMenu)
267 ) {
268 getActiveChild().onKeypress(keypress);
269 return;
270 }
271 }
272
273 if (keypress.equals(kbEsc)) {
274 getApplication().closeMenu();
275 return;
276 }
277 if (keypress.equals(kbDown)) {
278 switchWidget(true);
279 return;
280 }
281 if (keypress.equals(kbUp)) {
282 switchWidget(false);
283 return;
284 }
285 if (keypress.equals(kbRight)) {
286 getApplication().switchMenu(true);
287 return;
288 }
289 if (keypress.equals(kbLeft)) {
290 if (isSubMenu) {
291 getApplication().closeSubMenu();
292 } else {
293 getApplication().switchMenu(false);
294 }
295 return;
296 }
297
298 // Switch to a menuItem if it has an mnemonic
299 if (!keypress.getKey().isFnKey()
300 && !keypress.getKey().isAlt()
301 && !keypress.getKey().isCtrl()) {
302 for (TWidget widget: getChildren()) {
303 TMenuItem item = (TMenuItem) widget;
304 if ((item.getMnemonic() != null)
305 && (Character.toLowerCase(item.getMnemonic().getShortcut())
306 == Character.toLowerCase(keypress.getKey().getChar()))
307 ) {
308 // Send an enter keystroke to it
309 activate(item);
310 item.handleEvent(new TKeypressEvent(kbEnter));
311 return;
312 }
313 }
314 }
315
316 // Dispatch the keypress to an active widget
317 for (TWidget widget: getChildren()) {
318 if (widget.isActive()) {
319 widget.handleEvent(keypress);
320 return;
321 }
322 }
323 }
324
325 /**
326 * Convenience function to add a menu item.
327 *
328 * @param id menu item ID. Must be greater than 1024.
329 * @param label menu item label
330 * @return the new menu item
331 */
332 public TMenuItem addItem(final int id, final String label) {
333 assert (id >= 1024);
334 return addItemInternal(id, label, null);
335 }
336
337 /**
338 * Convenience function to add a custom menu item.
339 *
340 * @param id menu item ID. Must be greater than 1024.
341 * @param label menu item label
342 * @param key global keyboard accelerator
343 * @return the new menu item
344 */
345 public TMenuItem addItem(final int id, final String label,
346 final TKeypress key) {
347
348 assert (id >= 1024);
349 return addItemInternal(id, label, key);
350 }
351
352 /**
353 * Convenience function to add a custom menu item.
354 *
355 * @param id menu item ID. Must be greater than 1024.
356 * @param label menu item label
357 * @param key global keyboard accelerator
358 * @return the new menu item
359 */
360 private TMenuItem addItemInternal(final int id, final String label,
361 final TKeypress key) {
362
363 int newY = getChildren().size() + 1;
364 assert (newY < getHeight());
365
366 TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
367 menuItem.setKey(key);
368 setHeight(getHeight() + 1);
369 if (menuItem.getWidth() + 2 > getWidth()) {
370 setWidth(menuItem.getWidth() + 2);
371 }
372 for (TWidget widget: getChildren()) {
373 widget.setWidth(getWidth() - 2);
374 }
375 getApplication().addMenuItem(menuItem);
376 getApplication().recomputeMenuX();
377 activate(0);
378 return menuItem;
379 }
380
381 /**
382 * Convenience function to add one of the default menu items.
383 *
384 * @param id menu item ID. Must be between 0 (inclusive) and 1023
385 * (inclusive).
386 * @return the new menu item
387 */
388 public TMenuItem addDefaultItem(final int id) {
389 assert (id >= 0);
390 assert (id < 1024);
391
392 String label;
393 TKeypress key = null;
394
395 switch (id) {
396
397 case MID_EXIT:
398 label = i18n.getString("menuExit");
399 key = kbAltX;
400 break;
401
402 case MID_SHELL:
403 label = i18n.getString("menuShell");
404 break;
405
406 case MID_OPEN_FILE:
407 label = i18n.getString("menuOpen");
408 key = kbAltO;
409 break;
410
411 case MID_CUT:
412 label = i18n.getString("menuCut");
413 key = kbCtrlX;
414 break;
415 case MID_COPY:
416 label = i18n.getString("menuCopy");
417 key = kbCtrlC;
418 break;
419 case MID_PASTE:
420 label = i18n.getString("menuPaste");
421 key = kbCtrlV;
422 break;
423 case MID_CLEAR:
424 label = i18n.getString("menuClear");
425 // key = kbDel;
426 break;
427
428 case MID_TILE:
429 label = i18n.getString("menuWindowTile");
430 break;
431 case MID_CASCADE:
432 label = i18n.getString("menuWindowCascade");
433 break;
434 case MID_CLOSE_ALL:
435 label = i18n.getString("menuWindowCloseAll");
436 break;
437 case MID_WINDOW_MOVE:
438 label = i18n.getString("menuWindowMove");
439 key = kbCtrlF5;
440 break;
441 case MID_WINDOW_ZOOM:
442 label = i18n.getString("menuWindowZoom");
443 key = kbF5;
444 break;
445 case MID_WINDOW_NEXT:
446 label = i18n.getString("menuWindowNext");
447 key = kbF6;
448 break;
449 case MID_WINDOW_PREVIOUS:
450 label = i18n.getString("menuWindowPrevious");
451 key = kbShiftF6;
452 break;
453 case MID_WINDOW_CLOSE:
454 label = i18n.getString("menuWindowClose");
455 key = kbCtrlW;
456 break;
457
458 case MID_HELP_CONTENTS:
459 label = i18n.getString("menuHelpContents");
460 break;
461 case MID_HELP_INDEX:
462 label = i18n.getString("menuHelpIndex");
463 key = kbShiftF1;
464 break;
465 case MID_HELP_SEARCH:
466 label = i18n.getString("menuHelpSearch");
467 key = kbCtrlF1;
468 break;
469 case MID_HELP_PREVIOUS:
470 label = i18n.getString("menuHelpPrevious");
471 key = kbAltF1;
472 break;
473 case MID_HELP_HELP:
474 label = i18n.getString("menuHelpHelp");
475 break;
476 case MID_HELP_ACTIVE_FILE:
477 label = i18n.getString("menuHelpActive");
478 break;
479 case MID_ABOUT:
480 label = i18n.getString("menuHelpAbout");
481 break;
482
483 case MID_REPAINT:
484 label = i18n.getString("menuRepaintDesktop");
485 break;
486
487 default:
488 throw new IllegalArgumentException("Invalid menu ID: " + id);
489 }
490
491 return addItemInternal(id, label, key);
492 }
493
494 /**
495 * Convenience function to add a menu separator.
496 */
497 public void addSeparator() {
498 int newY = getChildren().size() + 1;
499 assert (newY < getHeight());
500
501 // We just have to construct it, don't need to hang onto what it
502 // makes.
503 new TMenuSeparator(this, 1, newY);
504 setHeight(getHeight() + 1);
505 }
506
507 /**
508 * Convenience function to add a sub-menu.
509 *
510 * @param title menu title. Title must contain a keyboard shortcut,
511 * denoted by prefixing a letter with "&", e.g. "&File"
512 * @return the new sub-menu
513 */
514 public TSubMenu addSubMenu(final String title) {
515 int newY = getChildren().size() + 1;
516 assert (newY < getHeight());
517
518 TSubMenu subMenu = new TSubMenu(this, title, 1, newY);
519 setHeight(getHeight() + 1);
520 if (subMenu.getWidth() + 2 > getWidth()) {
521 setWidth(subMenu.getWidth() + 2);
522 }
523 for (TWidget widget: getChildren()) {
524 widget.setWidth(getWidth() - 2);
525 }
526 getApplication().recomputeMenuX();
527 activate(0);
528 subMenu.menu.setX(getX() + getWidth() - 2);
529
530 return subMenu;
531 }
532
533 }