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