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