3120635996e503fd43feb4bc24d3c031c43911bf
[fanfix.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 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 // 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
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
79 // Window menu
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;
88
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
98 // Other
99 public static final int MID_REPAINT = 50;
100
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
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
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
152 // ------------------------------------------------------------------------
153 // Event handlers ---------------------------------------------------------
154 // ------------------------------------------------------------------------
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;
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;
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;
212
213 // See if we should activate a different menu item
214 for (TWidget widget: getChildren()) {
215 if ((mouse.isMouse1())
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) {
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 ) {
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)) {
263 getApplication().switchMenu(true);
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
276 if (!keypress.getKey().isFnKey()
277 && !keypress.getKey().isAlt()
278 && !keypress.getKey().isCtrl()) {
279 for (TWidget widget: getChildren()) {
280 TMenuItem item = (TMenuItem) widget;
281 if ((item.getMnemonic() != null)
282 && (Character.toLowerCase(item.getMnemonic().getShortcut())
283 == Character.toLowerCase(keypress.getKey().getChar()))
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()) {
295 if (widget.isActive()) {
296 widget.handleEvent(keypress);
297 return;
298 }
299 }
300 }
301
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
378 /**
379 * Convenience function to add a menu item.
380 *
381 * @param id menu item ID. Must be greater than 1024.
382 * @param label menu item label
383 * @return the new menu item
384 */
385 public TMenuItem addItem(final int id, final String label) {
386 assert (id >= 1024);
387 return addItemInternal(id, label, null);
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 */
398 public TMenuItem addItem(final int id, final String label,
399 final TKeypress key) {
400
401 assert (id >= 1024);
402 return addItemInternal(id, label, key);
403 }
404
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
422 /**
423 * Convenience function to add a custom menu item.
424 *
425 * @param id menu item ID. Must be greater than 1024.
426 * @param label menu item label
427 * @param key global keyboard accelerator
428 * @return the new menu item
429 */
430 private TMenuItem addItemInternal(final int id, final String label,
431 final TKeypress key) {
432
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
448 int newY = getChildren().size() + 1;
449 assert (newY < getHeight());
450
451 TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
452 menuItem.setKey(key);
453 menuItem.setEnabled(enabled);
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 }
461 getApplication().addMenuItem(menuItem);
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 */
474 public TMenuItem addDefaultItem(final int id) {
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) {
487 assert (id >= 0);
488 assert (id < 1024);
489
490 String label;
491 TKeypress key = null;
492
493 switch (id) {
494
495 case MID_EXIT:
496 label = i18n.getString("menuExit");
497 key = kbAltX;
498 break;
499
500 case MID_SHELL:
501 label = i18n.getString("menuShell");
502 break;
503
504 case MID_OPEN_FILE:
505 label = i18n.getString("menuOpen");
506 key = kbF3;
507 break;
508
509 case MID_CUT:
510 label = i18n.getString("menuCut");
511 key = kbCtrlX;
512 break;
513 case MID_COPY:
514 label = i18n.getString("menuCopy");
515 key = kbCtrlC;
516 break;
517 case MID_PASTE:
518 label = i18n.getString("menuPaste");
519 key = kbCtrlV;
520 break;
521 case MID_CLEAR:
522 label = i18n.getString("menuClear");
523 // key = kbDel;
524 break;
525
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
540 case MID_TILE:
541 label = i18n.getString("menuWindowTile");
542 break;
543 case MID_CASCADE:
544 label = i18n.getString("menuWindowCascade");
545 break;
546 case MID_CLOSE_ALL:
547 label = i18n.getString("menuWindowCloseAll");
548 break;
549 case MID_WINDOW_MOVE:
550 label = i18n.getString("menuWindowMove");
551 key = kbCtrlF5;
552 break;
553 case MID_WINDOW_ZOOM:
554 label = i18n.getString("menuWindowZoom");
555 key = kbF5;
556 break;
557 case MID_WINDOW_NEXT:
558 label = i18n.getString("menuWindowNext");
559 key = kbF6;
560 break;
561 case MID_WINDOW_PREVIOUS:
562 label = i18n.getString("menuWindowPrevious");
563 key = kbShiftF6;
564 break;
565 case MID_WINDOW_CLOSE:
566 label = i18n.getString("menuWindowClose");
567 key = kbCtrlW;
568 break;
569
570 case MID_HELP_CONTENTS:
571 label = i18n.getString("menuHelpContents");
572 break;
573 case MID_HELP_INDEX:
574 label = i18n.getString("menuHelpIndex");
575 key = kbShiftF1;
576 break;
577 case MID_HELP_SEARCH:
578 label = i18n.getString("menuHelpSearch");
579 key = kbCtrlF1;
580 break;
581 case MID_HELP_PREVIOUS:
582 label = i18n.getString("menuHelpPrevious");
583 key = kbAltF1;
584 break;
585 case MID_HELP_HELP:
586 label = i18n.getString("menuHelpHelp");
587 break;
588 case MID_HELP_ACTIVE_FILE:
589 label = i18n.getString("menuHelpActive");
590 break;
591 case MID_ABOUT:
592 label = i18n.getString("menuHelpAbout");
593 break;
594
595 case MID_REPAINT:
596 label = i18n.getString("menuRepaintDesktop");
597 break;
598
599 default:
600 throw new IllegalArgumentException("Invalid menu ID: " + id);
601 }
602
603 return addItemInternal(id, label, key, enabled);
604 }
605
606 /**
607 * Convenience function to add a menu separator.
608 */
609 public void addSeparator() {
610 int newY = getChildren().size() + 1;
611 assert (newY < getHeight());
612
613 // We just have to construct it, don't need to hang onto what it
614 // makes.
615 new TMenuSeparator(this, 1, newY);
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 "&amp;", e.g. "&amp;File"
624 * @return the new sub-menu
625 */
626 public TSubMenu addSubMenu(final String title) {
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 }