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