misc fixes
[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) 2019 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 class TMenu extends TWindow {
48
49 /**
50 * Translated strings.
51 */
52 private static final ResourceBundle i18n = ResourceBundle.getBundle(TMenu.class.getName());
53
54 // ------------------------------------------------------------------------
55 // Constants --------------------------------------------------------------
56 // ------------------------------------------------------------------------
57
58 // Reserved menu item IDs
59 public static final int MID_UNUSED = -1;
60
61 // Tools menu
62 public static final int MID_REPAINT = 1;
63 public static final int MID_VIEW_IMAGE = 2;
64 public static final int MID_CHANGE_FONT = 3;
65
66 // File menu
67 public static final int MID_NEW = 10;
68 public static final int MID_EXIT = 11;
69 public static final int MID_QUIT = MID_EXIT;
70 public static final int MID_OPEN_FILE = 12;
71 public static final int MID_SHELL = 13;
72
73 // Edit menu
74 public static final int MID_CUT = 20;
75 public static final int MID_COPY = 21;
76 public static final int MID_PASTE = 22;
77 public static final int MID_CLEAR = 23;
78
79 // Search menu
80 public static final int MID_FIND = 30;
81 public static final int MID_REPLACE = 31;
82 public static final int MID_SEARCH_AGAIN = 32;
83 public static final int MID_GOTO_LINE = 33;
84
85 // Window menu
86 public static final int MID_TILE = 40;
87 public static final int MID_CASCADE = 41;
88 public static final int MID_CLOSE_ALL = 42;
89 public static final int MID_WINDOW_MOVE = 43;
90 public static final int MID_WINDOW_ZOOM = 44;
91 public static final int MID_WINDOW_NEXT = 45;
92 public static final int MID_WINDOW_PREVIOUS = 46;
93 public static final int MID_WINDOW_CLOSE = 47;
94
95 // Help menu
96 public static final int MID_HELP_CONTENTS = 50;
97 public static final int MID_HELP_INDEX = 51;
98 public static final int MID_HELP_SEARCH = 52;
99 public static final int MID_HELP_PREVIOUS = 53;
100 public static final int MID_HELP_HELP = 54;
101 public static final int MID_HELP_ACTIVE_FILE = 55;
102 public static final int MID_ABOUT = 56;
103
104 // ------------------------------------------------------------------------
105 // Variables --------------------------------------------------------------
106 // ------------------------------------------------------------------------
107
108 /**
109 * If true, this is a sub-menu. Note package private access.
110 */
111 boolean isSubMenu = false;
112
113 /**
114 * The X position of the menu's title.
115 */
116 private int titleX;
117
118 /**
119 * The shortcut and title.
120 */
121 private MnemonicString mnemonic;
122
123 // ------------------------------------------------------------------------
124 // Constructors -----------------------------------------------------------
125 // ------------------------------------------------------------------------
126
127 /**
128 * Public constructor.
129 *
130 * @param parent parent application
131 * @param x column relative to parent
132 * @param y row relative to parent
133 * @param label mnemonic menu title. Label must contain a keyboard
134 * shortcut (mnemonic), denoted by prefixing a letter with "&",
135 * e.g. "&File"
136 */
137 public TMenu(final TApplication parent, final int x, final int y,
138 final String label) {
139
140 super(parent, label, x, y, parent.getScreen().getWidth(),
141 parent.getScreen().getHeight());
142
143 // Setup the menu shortcut
144 mnemonic = new MnemonicString(label);
145 setTitle(mnemonic.getRawLabel());
146 assert (mnemonic.getShortcutIdx() >= 0);
147
148 // Recompute width and height to reflect an empty menu
149 setWidth(getTitle().length() + 4);
150 setHeight(2);
151
152 setActive(false);
153 }
154
155 // ------------------------------------------------------------------------
156 // Event handlers ---------------------------------------------------------
157 // ------------------------------------------------------------------------
158
159 /**
160 * Handle mouse button presses.
161 *
162 * @param mouse mouse button event
163 */
164 @Override
165 public void onMouseDown(final TMouseEvent mouse) {
166 this.mouse = mouse;
167 super.onMouseDown(mouse);
168
169 // Pass to children
170 for (TWidget widget: getChildren()) {
171 if (widget.mouseWouldHit(mouse)) {
172 // Dispatch to this child, also activate it
173 activate(widget);
174
175 // Set x and y relative to the child's coordinates
176 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
177 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
178 widget.handleEvent(mouse);
179 return;
180 }
181 }
182 }
183
184 /**
185 * Handle mouse button releases.
186 *
187 * @param mouse mouse button release event
188 */
189 @Override
190 public void onMouseUp(final TMouseEvent mouse) {
191 this.mouse = mouse;
192
193 // Pass to children
194 for (TWidget widget: getChildren()) {
195 if (widget.mouseWouldHit(mouse)) {
196 // Dispatch to this child, also activate it
197 activate(widget);
198
199 // Set x and y relative to the child's coordinates
200 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
201 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
202 widget.handleEvent(mouse);
203 return;
204 }
205 }
206 }
207
208 /**
209 * Handle mouse movements.
210 *
211 * @param mouse mouse motion event
212 */
213 @Override
214 public void onMouseMotion(final TMouseEvent mouse) {
215 this.mouse = mouse;
216
217 // See if we should activate a different menu item
218 for (TWidget widget: getChildren()) {
219 if ((mouse.isMouse1())
220 && (widget.mouseWouldHit(mouse))
221 ) {
222 // Activate this menu item
223 activate(widget);
224 if (widget instanceof TSubMenu) {
225 ((TSubMenu) widget).dispatch();
226 }
227 return;
228 }
229 }
230 }
231
232 /**
233 * Handle keystrokes.
234 *
235 * @param keypress keystroke event
236 */
237 @Override
238 public void onKeypress(final TKeypressEvent keypress) {
239
240 /*
241 System.err.printf("keypress: %s active child: %s\n", keypress,
242 getActiveChild());
243 */
244
245 if (getActiveChild() != this) {
246 if ((getActiveChild() instanceof TSubMenu)
247 || (getActiveChild() instanceof TMenu)
248 ) {
249 getActiveChild().onKeypress(keypress);
250 return;
251 }
252 }
253
254 if (keypress.equals(kbEsc)) {
255 getApplication().closeMenu();
256 return;
257 }
258 if (keypress.equals(kbDown)) {
259 switchWidget(true);
260 return;
261 }
262 if (keypress.equals(kbUp)) {
263 switchWidget(false);
264 return;
265 }
266 if (keypress.equals(kbRight)) {
267 getApplication().switchMenu(true);
268 return;
269 }
270 if (keypress.equals(kbLeft)) {
271 if (isSubMenu) {
272 getApplication().closeSubMenu();
273 } else {
274 getApplication().switchMenu(false);
275 }
276 return;
277 }
278
279 // Switch to a menuItem if it has an mnemonic
280 if (!keypress.getKey().isFnKey()
281 && !keypress.getKey().isAlt()
282 && !keypress.getKey().isCtrl()) {
283 for (TWidget widget: getChildren()) {
284 TMenuItem item = (TMenuItem) widget;
285 if ((item.getMnemonic() != null)
286 && (Character.toLowerCase(item.getMnemonic().getShortcut())
287 == Character.toLowerCase(keypress.getKey().getChar()))
288 ) {
289 // Send an enter keystroke to it
290 activate(item);
291 item.handleEvent(new TKeypressEvent(kbEnter));
292 return;
293 }
294 }
295 }
296
297 // Dispatch the keypress to an active widget
298 for (TWidget widget: getChildren()) {
299 if (widget.isActive()) {
300 widget.handleEvent(keypress);
301 return;
302 }
303 }
304 }
305
306 // ------------------------------------------------------------------------
307 // TWindow ----------------------------------------------------------------
308 // ------------------------------------------------------------------------
309
310 /**
311 * Draw a top-level menu with title and menu items.
312 */
313 @Override
314 public void draw() {
315 CellAttributes background = getTheme().getColor("tmenu");
316
317 assert (isAbsoluteActive());
318
319 // Fill in the interior background
320 for (int i = 0; i < getHeight(); i++) {
321 hLineXY(0, i, getWidth(), ' ', background);
322 }
323
324 // Draw the box
325 char cTopLeft;
326 char cTopRight;
327 char cBottomLeft;
328 char cBottomRight;
329 char cHSide;
330
331 cTopLeft = GraphicsChars.ULCORNER;
332 cTopRight = GraphicsChars.URCORNER;
333 cBottomLeft = GraphicsChars.LLCORNER;
334 cBottomRight = GraphicsChars.LRCORNER;
335 cHSide = GraphicsChars.SINGLE_BAR;
336
337 // Place the corner characters
338 putCharXY(1, 0, cTopLeft, background);
339 putCharXY(getWidth() - 2, 0, cTopRight, background);
340 putCharXY(1, getHeight() - 1, cBottomLeft, background);
341 putCharXY(getWidth() - 2, getHeight() - 1, cBottomRight, background);
342
343 // Draw the box lines
344 hLineXY(1 + 1, 0, getWidth() - 4, cHSide, background);
345 hLineXY(1 + 1, getHeight() - 1, getWidth() - 4, cHSide, background);
346
347 // Draw a shadow
348 drawBoxShadow(0, 0, getWidth(), getHeight());
349 }
350
351 // ------------------------------------------------------------------------
352 // TMenu ------------------------------------------------------------------
353 // ------------------------------------------------------------------------
354
355 /**
356 * Set the menu title X position.
357 *
358 * @param titleX the position
359 */
360 public void setTitleX(final int titleX) {
361 this.titleX = titleX;
362 }
363
364 /**
365 * Get the menu title X position.
366 *
367 * @return the position
368 */
369 public int getTitleX() {
370 return titleX;
371 }
372
373 /**
374 * Get the mnemonic string.
375 *
376 * @return the full mnemonic string
377 */
378 public MnemonicString getMnemonic() {
379 return mnemonic;
380 }
381
382 /**
383 * Convenience function to add a menu item.
384 *
385 * @param id menu item ID. Must be greater than 1024.
386 * @param label menu item label
387 * @return the new menu item
388 */
389 public TMenuItem addItem(final int id, final String label) {
390 assert (id >= 1024);
391 return addItemInternal(id, label, null);
392 }
393
394 /**
395 * Convenience function to add a menu item.
396 *
397 * @param id menu item ID. Must be greater than 1024.
398 * @param label menu item label
399 * @param enabled default state for enabled
400 * @return the new menu item
401 */
402 public TMenuItem addItem(final int id, final String label,
403 final boolean enabled) {
404
405 assert (id >= 1024);
406 return addItemInternal(id, label, null, enabled);
407 }
408
409 /**
410 * Convenience function to add a custom menu item.
411 *
412 * @param id menu item ID. Must be greater than 1024.
413 * @param label menu item label
414 * @param key global keyboard accelerator
415 * @return the new menu item
416 */
417 public TMenuItem addItem(final int id, final String label,
418 final TKeypress key) {
419
420 assert (id >= 1024);
421 return addItemInternal(id, label, key);
422 }
423
424 /**
425 * Convenience function to add a custom menu item.
426 *
427 * @param id menu item ID. Must be greater than 1024.
428 * @param label menu item label
429 * @param key global keyboard accelerator
430 * @param enabled default state for enabled
431 * @return the new menu item
432 */
433 public TMenuItem addItem(final int id, final String label,
434 final TKeypress key, final boolean enabled) {
435
436 TMenuItem item = addItem(id, label, key);
437 item.setEnabled(enabled);
438 return item;
439 }
440
441 /**
442 * Convenience function to add a custom menu item.
443 *
444 * @param id menu item ID. Must be greater than 1024.
445 * @param label menu item label
446 * @param key global keyboard accelerator
447 * @return the new menu item
448 */
449 private TMenuItem addItemInternal(final int id, final String label,
450 final TKeypress key) {
451
452 return addItemInternal(id, label, key, true);
453 }
454
455 /**
456 * Convenience function to add a custom menu item.
457 *
458 * @param id menu item ID. Must be greater than 1024.
459 * @param label menu item label
460 * @param key global keyboard accelerator
461 * @param enabled default state for enabled
462 * @return the new menu item
463 */
464 private TMenuItem addItemInternal(final int id, final String label,
465 final TKeypress key, final boolean enabled) {
466
467 int newY = getChildren().size() + 1;
468 assert (newY < getHeight());
469
470 TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
471 menuItem.setKey(key);
472 menuItem.setEnabled(enabled);
473 setHeight(getHeight() + 1);
474 if (menuItem.getWidth() + 2 > getWidth()) {
475 setWidth(menuItem.getWidth() + 2);
476 }
477 for (TWidget widget: getChildren()) {
478 widget.setWidth(getWidth() - 2);
479 }
480 getApplication().addMenuItem(menuItem);
481 getApplication().recomputeMenuX();
482 activate(0);
483 return menuItem;
484 }
485
486 /**
487 * Convenience function to add one of the default menu items.
488 *
489 * @param id menu item ID. Must be between 0 (inclusive) and 1023
490 * (inclusive).
491 * @return the new menu item
492 */
493 public TMenuItem addDefaultItem(final int id) {
494 return addDefaultItem(id, true);
495 }
496
497 /**
498 * Convenience function to add one of the default menu items.
499 *
500 * @param id menu item ID. Must be between 0 (inclusive) and 1023
501 * (inclusive).
502 * @param enabled default state for enabled
503 * @return the new menu item
504 */
505 public TMenuItem addDefaultItem(final int id, final boolean enabled) {
506 assert (id >= 0);
507 assert (id < 1024);
508
509 String label;
510 TKeypress key = null;
511
512 switch (id) {
513
514 case MID_REPAINT:
515 label = i18n.getString("menuRepaintDesktop");
516 break;
517
518 case MID_VIEW_IMAGE:
519 label = i18n.getString("menuViewImage");
520 break;
521
522 case MID_CHANGE_FONT:
523 label = i18n.getString("menuChangeFont");
524 break;
525
526 case MID_NEW:
527 label = i18n.getString("menuNew");
528 break;
529
530 case MID_EXIT:
531 label = i18n.getString("menuExit");
532 key = kbAltX;
533 break;
534
535 case MID_SHELL:
536 label = i18n.getString("menuShell");
537 break;
538
539 case MID_OPEN_FILE:
540 label = i18n.getString("menuOpen");
541 key = kbF3;
542 break;
543
544 case MID_CUT:
545 label = i18n.getString("menuCut");
546 key = kbCtrlX;
547 break;
548 case MID_COPY:
549 label = i18n.getString("menuCopy");
550 key = kbCtrlC;
551 break;
552 case MID_PASTE:
553 label = i18n.getString("menuPaste");
554 key = kbCtrlV;
555 break;
556 case MID_CLEAR:
557 label = i18n.getString("menuClear");
558 // key = kbDel;
559 break;
560
561 case MID_FIND:
562 label = i18n.getString("menuFind");
563 break;
564 case MID_REPLACE:
565 label = i18n.getString("menuReplace");
566 break;
567 case MID_SEARCH_AGAIN:
568 label = i18n.getString("menuSearchAgain");
569 break;
570 case MID_GOTO_LINE:
571 label = i18n.getString("menuGotoLine");
572 key = kbCtrlL;
573 break;
574
575 case MID_TILE:
576 label = i18n.getString("menuWindowTile");
577 break;
578 case MID_CASCADE:
579 label = i18n.getString("menuWindowCascade");
580 break;
581 case MID_CLOSE_ALL:
582 label = i18n.getString("menuWindowCloseAll");
583 break;
584 case MID_WINDOW_MOVE:
585 label = i18n.getString("menuWindowMove");
586 key = kbCtrlF5;
587 break;
588 case MID_WINDOW_ZOOM:
589 label = i18n.getString("menuWindowZoom");
590 key = kbF5;
591 break;
592 case MID_WINDOW_NEXT:
593 label = i18n.getString("menuWindowNext");
594 key = kbF6;
595 break;
596 case MID_WINDOW_PREVIOUS:
597 label = i18n.getString("menuWindowPrevious");
598 key = kbShiftF6;
599 break;
600 case MID_WINDOW_CLOSE:
601 label = i18n.getString("menuWindowClose");
602 key = kbCtrlW;
603 break;
604
605 case MID_HELP_CONTENTS:
606 label = i18n.getString("menuHelpContents");
607 break;
608 case MID_HELP_INDEX:
609 label = i18n.getString("menuHelpIndex");
610 key = kbShiftF1;
611 break;
612 case MID_HELP_SEARCH:
613 label = i18n.getString("menuHelpSearch");
614 key = kbCtrlF1;
615 break;
616 case MID_HELP_PREVIOUS:
617 label = i18n.getString("menuHelpPrevious");
618 key = kbAltF1;
619 break;
620 case MID_HELP_HELP:
621 label = i18n.getString("menuHelpHelp");
622 break;
623 case MID_HELP_ACTIVE_FILE:
624 label = i18n.getString("menuHelpActive");
625 break;
626 case MID_ABOUT:
627 label = i18n.getString("menuHelpAbout");
628 break;
629
630 default:
631 throw new IllegalArgumentException("Invalid menu ID: " + id);
632 }
633
634 return addItemInternal(id, label, key, enabled);
635 }
636
637 /**
638 * Convenience function to add a menu separator.
639 */
640 public void addSeparator() {
641 int newY = getChildren().size() + 1;
642 assert (newY < getHeight());
643
644 // We just have to construct it, don't need to hang onto what it
645 // makes.
646 new TMenuSeparator(this, 1, newY);
647 setHeight(getHeight() + 1);
648 }
649
650 /**
651 * Convenience function to add a sub-menu.
652 *
653 * @param title menu title. Title must contain a keyboard shortcut,
654 * denoted by prefixing a letter with "&amp;", e.g. "&amp;File"
655 * @return the new sub-menu
656 */
657 public TSubMenu addSubMenu(final String title) {
658 int newY = getChildren().size() + 1;
659 assert (newY < getHeight());
660
661 TSubMenu subMenu = new TSubMenu(this, title, 1, newY);
662 setHeight(getHeight() + 1);
663 if (subMenu.getWidth() + 2 > getWidth()) {
664 setWidth(subMenu.getWidth() + 2);
665 }
666 for (TWidget widget: getChildren()) {
667 widget.setWidth(getWidth() - 2);
668 }
669 getApplication().recomputeMenuX();
670 activate(0);
671 subMenu.menu.setX(getX() + getWidth() - 2);
672
673 return subMenu;
674 }
675
676 }