#38 fix Swing deadlock
[fanfix.git] / src / jexer / menu / TMenu.java
CommitLineData
daa4106c 1/*
928811d8
KL
2 * Jexer - Java Text User Interface
3 *
e16dda65 4 * The MIT License (MIT)
928811d8 5 *
a69ed767 6 * Copyright (C) 2019 Kevin Lamonte
928811d8 7 *
e16dda65
KL
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:
928811d8 14 *
e16dda65
KL
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
928811d8 17 *
e16dda65
KL
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.
928811d8
KL
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29package jexer.menu;
30
339652cc
KL
31import java.util.ResourceBundle;
32
928811d8
KL
33import jexer.TApplication;
34import jexer.TKeypress;
35import jexer.TWidget;
36import jexer.TWindow;
37import jexer.bits.CellAttributes;
38import jexer.bits.GraphicsChars;
39import jexer.bits.MnemonicString;
40import jexer.event.TKeypressEvent;
41import jexer.event.TMouseEvent;
42import static jexer.TKeypress.*;
43
44/**
45 * TMenu is a top-level collection of TMenuItems.
46 */
051e2913 47public class TMenu extends TWindow {
928811d8 48
339652cc
KL
49 /**
50 * Translated strings.
51 */
52 private static final ResourceBundle i18n = ResourceBundle.getBundle(TMenu.class.getName());
53
d36057df
KL
54 // ------------------------------------------------------------------------
55 // Constants --------------------------------------------------------------
56 // ------------------------------------------------------------------------
928811d8
KL
57
58 // Reserved menu item IDs
59 public static final int MID_UNUSED = -1;
60
e23ea538
KL
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
928811d8 66 // File menu
e23ea538
KL
67 public static final int MID_NEW = 10;
68 public static final int MID_EXIT = 11;
928811d8 69 public static final int MID_QUIT = MID_EXIT;
e23ea538
KL
70 public static final int MID_OPEN_FILE = 12;
71 public static final int MID_SHELL = 13;
928811d8
KL
72
73 // Edit menu
e23ea538
KL
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;
928811d8 78
d36057df 79 // Search menu
e23ea538
KL
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;
d36057df 84
928811d8 85 // Window menu
e23ea538
KL
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;
928811d8 94
55d2b2c2 95 // Help menu
e23ea538
KL
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;
1c19fdea 103
d36057df
KL
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
928811d8
KL
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
43ad7b6c
KL
134 * shortcut (mnemonic), denoted by prefixing a letter with "&",
135 * e.g. "&File"
928811d8
KL
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
928811d8
KL
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
d36057df
KL
155 // ------------------------------------------------------------------------
156 // Event handlers ---------------------------------------------------------
157 // ------------------------------------------------------------------------
928811d8
KL
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;
a69ed767 167 super.onMouseDown(mouse);
928811d8
KL
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;
928811d8
KL
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;
928811d8
KL
216
217 // See if we should activate a different menu item
218 for (TWidget widget: getChildren()) {
7c870d89 219 if ((mouse.isMouse1())
928811d8
KL
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) {
91c9a837
KL
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 ) {
928811d8
KL
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)) {
91c9a837 267 getApplication().switchMenu(true);
928811d8
KL
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
7c870d89
KL
280 if (!keypress.getKey().isFnKey()
281 && !keypress.getKey().isAlt()
282 && !keypress.getKey().isCtrl()) {
928811d8
KL
283 for (TWidget widget: getChildren()) {
284 TMenuItem item = (TMenuItem) widget;
285 if ((item.getMnemonic() != null)
286 && (Character.toLowerCase(item.getMnemonic().getShortcut())
7c870d89 287 == Character.toLowerCase(keypress.getKey().getChar()))
928811d8
KL
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()) {
7c870d89 299 if (widget.isActive()) {
928811d8
KL
300 widget.handleEvent(keypress);
301 return;
302 }
303 }
304 }
305
d36057df
KL
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
a69ed767 348 drawBoxShadow(0, 0, getWidth(), getHeight());
d36057df
KL
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
928811d8 382 /**
efb7af1f 383 * Convenience function to add a menu item.
928811d8
KL
384 *
385 * @param id menu item ID. Must be greater than 1024.
386 * @param label menu item label
928811d8
KL
387 * @return the new menu item
388 */
329fd62e 389 public TMenuItem addItem(final int id, final String label) {
928811d8 390 assert (id >= 1024);
efb7af1f 391 return addItemInternal(id, label, null);
928811d8
KL
392 }
393
a69ed767
KL
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
928811d8
KL
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 */
329fd62e 417 public TMenuItem addItem(final int id, final String label,
928811d8
KL
418 final TKeypress key) {
419
efb7af1f
KL
420 assert (id >= 1024);
421 return addItemInternal(id, label, key);
928811d8
KL
422 }
423
68c5cd6b
KL
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
928811d8 441 /**
efb7af1f 442 * Convenience function to add a custom menu item.
928811d8
KL
443 *
444 * @param id menu item ID. Must be greater than 1024.
445 * @param label menu item label
efb7af1f 446 * @param key global keyboard accelerator
928811d8
KL
447 * @return the new menu item
448 */
efb7af1f
KL
449 private TMenuItem addItemInternal(final int id, final String label,
450 final TKeypress key) {
928811d8 451
d36057df
KL
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
928811d8
KL
467 int newY = getChildren().size() + 1;
468 assert (newY < getHeight());
469
470 TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
efb7af1f 471 menuItem.setKey(key);
d36057df 472 menuItem.setEnabled(enabled);
928811d8
KL
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 }
efb7af1f 480 getApplication().addMenuItem(menuItem);
928811d8
KL
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 */
329fd62e 493 public TMenuItem addDefaultItem(final int id) {
d36057df
KL
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) {
928811d8
KL
506 assert (id >= 0);
507 assert (id < 1024);
508
509 String label;
510 TKeypress key = null;
928811d8
KL
511
512 switch (id) {
513
e23ea538
KL
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
928811d8 530 case MID_EXIT:
339652cc 531 label = i18n.getString("menuExit");
928811d8
KL
532 key = kbAltX;
533 break;
534
535 case MID_SHELL:
339652cc 536 label = i18n.getString("menuShell");
928811d8
KL
537 break;
538
539 case MID_OPEN_FILE:
339652cc 540 label = i18n.getString("menuOpen");
b2d49e0f 541 key = kbF3;
928811d8
KL
542 break;
543
544 case MID_CUT:
339652cc 545 label = i18n.getString("menuCut");
928811d8
KL
546 key = kbCtrlX;
547 break;
548 case MID_COPY:
339652cc 549 label = i18n.getString("menuCopy");
928811d8
KL
550 key = kbCtrlC;
551 break;
552 case MID_PASTE:
339652cc 553 label = i18n.getString("menuPaste");
928811d8
KL
554 key = kbCtrlV;
555 break;
556 case MID_CLEAR:
339652cc 557 label = i18n.getString("menuClear");
30bd4abd 558 // key = kbDel;
928811d8
KL
559 break;
560
d36057df
KL
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");
978a5d8f 569 key = kbCtrlL;
d36057df
KL
570 break;
571 case MID_GOTO_LINE:
572 label = i18n.getString("menuGotoLine");
d36057df
KL
573 break;
574
928811d8 575 case MID_TILE:
339652cc 576 label = i18n.getString("menuWindowTile");
928811d8
KL
577 break;
578 case MID_CASCADE:
339652cc 579 label = i18n.getString("menuWindowCascade");
928811d8
KL
580 break;
581 case MID_CLOSE_ALL:
339652cc 582 label = i18n.getString("menuWindowCloseAll");
928811d8
KL
583 break;
584 case MID_WINDOW_MOVE:
339652cc 585 label = i18n.getString("menuWindowMove");
928811d8
KL
586 key = kbCtrlF5;
587 break;
588 case MID_WINDOW_ZOOM:
339652cc 589 label = i18n.getString("menuWindowZoom");
928811d8
KL
590 key = kbF5;
591 break;
592 case MID_WINDOW_NEXT:
339652cc 593 label = i18n.getString("menuWindowNext");
928811d8
KL
594 key = kbF6;
595 break;
596 case MID_WINDOW_PREVIOUS:
339652cc 597 label = i18n.getString("menuWindowPrevious");
928811d8
KL
598 key = kbShiftF6;
599 break;
600 case MID_WINDOW_CLOSE:
339652cc 601 label = i18n.getString("menuWindowClose");
5dfd1c11 602 key = kbCtrlW;
928811d8
KL
603 break;
604
55d2b2c2 605 case MID_HELP_CONTENTS:
339652cc 606 label = i18n.getString("menuHelpContents");
55d2b2c2
KL
607 break;
608 case MID_HELP_INDEX:
339652cc 609 label = i18n.getString("menuHelpIndex");
55d2b2c2
KL
610 key = kbShiftF1;
611 break;
612 case MID_HELP_SEARCH:
339652cc 613 label = i18n.getString("menuHelpSearch");
55d2b2c2
KL
614 key = kbCtrlF1;
615 break;
616 case MID_HELP_PREVIOUS:
339652cc 617 label = i18n.getString("menuHelpPrevious");
55d2b2c2
KL
618 key = kbAltF1;
619 break;
620 case MID_HELP_HELP:
339652cc 621 label = i18n.getString("menuHelpHelp");
55d2b2c2
KL
622 break;
623 case MID_HELP_ACTIVE_FILE:
339652cc 624 label = i18n.getString("menuHelpActive");
55d2b2c2
KL
625 break;
626 case MID_ABOUT:
339652cc 627 label = i18n.getString("menuHelpAbout");
55d2b2c2
KL
628 break;
629
928811d8
KL
630 default:
631 throw new IllegalArgumentException("Invalid menu ID: " + id);
632 }
633
d36057df 634 return addItemInternal(id, label, key, enabled);
928811d8
KL
635 }
636
637 /**
638 * Convenience function to add a menu separator.
639 */
329fd62e 640 public void addSeparator() {
928811d8
KL
641 int newY = getChildren().size() + 1;
642 assert (newY < getHeight());
643
cf9af8df
KL
644 // We just have to construct it, don't need to hang onto what it
645 // makes.
646 new TMenuSeparator(this, 1, newY);
928811d8
KL
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,
43ad7b6c 654 * denoted by prefixing a letter with "&amp;", e.g. "&amp;File"
928811d8
KL
655 * @return the new sub-menu
656 */
329fd62e 657 public TSubMenu addSubMenu(final String title) {
928811d8
KL
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}