75871c078d6f7d8bc98408fa896462532a0b9204
[nikiroo-utils.git] / TMenu.java
1 /**
2 * Jexer - Java Text User Interface
3 *
4 * License: LGPLv3 or later
5 *
6 * This module is licensed under the GNU Lesser General Public License
7 * Version 3. Please see the file "COPYING" in this directory for more
8 * information about the GNU Lesser General Public License Version 3.
9 *
10 * Copyright (C) 2015 Kevin Lamonte
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Lesser General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU Lesser General Public
23 * License along with this program; if not, see
24 * http://www.gnu.org/licenses/, or write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
26 * 02110-1301 USA
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
30 */
31 package jexer.menu;
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 * If true, this is a sub-menu. Note package private access.
51 */
52 boolean isSubMenu = false;
53
54 /**
55 * The shortcut and title.
56 */
57 private MnemonicString mnemonic;
58
59 /**
60 * Get the mnemonic string.
61 *
62 * @return the full mnemonic string
63 */
64 public MnemonicString getMnemonic() {
65 return mnemonic;
66 }
67
68 // Reserved menu item IDs
69 public static final int MID_UNUSED = -1;
70
71 // File menu
72 public static final int MID_EXIT = 1;
73 public static final int MID_QUIT = MID_EXIT;
74 public static final int MID_OPEN_FILE = 2;
75 public static final int MID_SHELL = 3;
76
77 // Edit menu
78 public static final int MID_CUT = 10;
79 public static final int MID_COPY = 11;
80 public static final int MID_PASTE = 12;
81 public static final int MID_CLEAR = 13;
82
83 // Window menu
84 public static final int MID_TILE = 20;
85 public static final int MID_CASCADE = 21;
86 public static final int MID_CLOSE_ALL = 22;
87 public static final int MID_WINDOW_MOVE = 23;
88 public static final int MID_WINDOW_ZOOM = 24;
89 public static final int MID_WINDOW_NEXT = 25;
90 public static final int MID_WINDOW_PREVIOUS = 26;
91 public static final int MID_WINDOW_CLOSE = 27;
92
93 /**
94 * Public constructor.
95 *
96 * @param parent parent application
97 * @param x column relative to parent
98 * @param y row relative to parent
99 * @param label mnemonic menu title. Label must contain a keyboard
100 * shortcut (mnemonic), denoted by prefixing a letter with "&",
101 * e.g. "&File"
102 */
103 public TMenu(final TApplication parent, final int x, final int y,
104 final String label) {
105
106 super(parent, label, x, y, parent.getScreen().getWidth(),
107 parent.getScreen().getHeight());
108
109 // My parent constructor added me as a window, get rid of that
110 parent.closeWindow(this);
111
112 // Setup the menu shortcut
113 mnemonic = new MnemonicString(label);
114 setTitle(mnemonic.getRawLabel());
115 assert (mnemonic.getShortcutIdx() >= 0);
116
117 // Recompute width and height to reflect an empty menu
118 setWidth(getTitle().length() + 4);
119 setHeight(2);
120
121 setActive(false);
122 }
123
124 /**
125 * Draw a top-level menu with title and menu items.
126 */
127 @Override
128 public void draw() {
129 CellAttributes menuColor;
130 CellAttributes background = getTheme().getColor("tmenu");
131
132 if (getAbsoluteActive()) {
133 menuColor = getTheme().getColor("tmenu.highlighted");
134 } else {
135 menuColor = getTheme().getColor("tmenu");
136 }
137
138 assert (getAbsoluteActive());
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 setRepaint();
181
182 // Pass to children
183 for (TWidget widget: getChildren()) {
184 if (widget.mouseWouldHit(mouse)) {
185 // Dispatch to this child, also activate it
186 activate(widget);
187
188 // Set x and y relative to the child's coordinates
189 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
190 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
191 widget.handleEvent(mouse);
192 return;
193 }
194 }
195 }
196
197 /**
198 * Handle mouse button releases.
199 *
200 * @param mouse mouse button release event
201 */
202 @Override
203 public void onMouseUp(final TMouseEvent mouse) {
204 this.mouse = mouse;
205 setRepaint();
206
207 // Pass to children
208 for (TWidget widget: getChildren()) {
209 if (widget.mouseWouldHit(mouse)) {
210 // Dispatch to this child, also activate it
211 activate(widget);
212
213 // Set x and y relative to the child's coordinates
214 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
215 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
216 widget.handleEvent(mouse);
217 return;
218 }
219 }
220 }
221
222 /**
223 * Handle mouse movements.
224 *
225 * @param mouse mouse motion event
226 */
227 @Override
228 public void onMouseMotion(final TMouseEvent mouse) {
229 this.mouse = mouse;
230 setRepaint();
231
232 // See if we should activate a different menu item
233 for (TWidget widget: getChildren()) {
234 if ((mouse.getMouse1())
235 && (widget.mouseWouldHit(mouse))
236 ) {
237 // Activate this menu item
238 activate(widget);
239 if (widget instanceof TSubMenu) {
240 ((TSubMenu) widget).dispatch();
241 }
242 return;
243 }
244 }
245 }
246
247 /**
248 * Handle keystrokes.
249 *
250 * @param keypress keystroke event
251 */
252 @Override
253 public void onKeypress(final TKeypressEvent keypress) {
254 if (getActiveChild() != null) {
255 if (getActiveChild() instanceof TSubMenu) {
256 getActiveChild().onKeypress(keypress);
257 return;
258 }
259 }
260
261 if (keypress.equals(kbEsc)) {
262 getApplication().closeMenu();
263 return;
264 }
265 if (keypress.equals(kbDown)) {
266 switchWidget(true);
267 return;
268 }
269 if (keypress.equals(kbUp)) {
270 switchWidget(false);
271 return;
272 }
273 if (keypress.equals(kbRight)) {
274 if (!isSubMenu) {
275 getApplication().switchMenu(true);
276 }
277 return;
278 }
279 if (keypress.equals(kbLeft)) {
280 if (isSubMenu) {
281 getApplication().closeSubMenu();
282 } else {
283 getApplication().switchMenu(false);
284 }
285 return;
286 }
287
288 // Switch to a menuItem if it has an mnemonic
289 if (!keypress.getKey().getIsKey()
290 && !keypress.getKey().getAlt()
291 && !keypress.getKey().getCtrl()) {
292 for (TWidget widget: getChildren()) {
293 TMenuItem item = (TMenuItem) widget;
294 if ((item.getMnemonic() != null)
295 && (Character.toLowerCase(item.getMnemonic().getShortcut())
296 == Character.toLowerCase(keypress.getKey().getCh()))
297 ) {
298 // Send an enter keystroke to it
299 activate(item);
300 item.handleEvent(new TKeypressEvent(kbEnter));
301 return;
302 }
303 }
304 }
305
306 // Dispatch the keypress to an active widget
307 for (TWidget widget: getChildren()) {
308 if (widget.getActive()) {
309 setRepaint();
310 widget.handleEvent(keypress);
311 return;
312 }
313 }
314 }
315
316 /**
317 * Convenience function to add a custom menu item.
318 *
319 * @param id menu item ID. Must be greater than 1024.
320 * @param label menu item label
321 * @param key global keyboard accelerator
322 * @return the new menu item
323 */
324 public final TMenuItem addItem(final int id, final String label,
325 final TKeypress key) {
326
327 assert (id >= 1024);
328 return addItemInternal(id, label, key);
329 }
330
331 /**
332 * Convenience function to add a custom menu item.
333 *
334 * @param id menu item ID. Must be greater than 1024.
335 * @param label menu item label
336 * @param key global keyboard accelerator
337 * @return the new menu item
338 */
339 private TMenuItem addItemInternal(final int id, final String label,
340 final TKeypress key) {
341
342 int newY = getChildren().size() + 1;
343 assert (newY < getHeight());
344
345 TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
346 menuItem.setKey(key);
347 setHeight(getHeight() + 1);
348 if (menuItem.getWidth() + 2 > getWidth()) {
349 setWidth(menuItem.getWidth() + 2);
350 }
351 for (TWidget widget: getChildren()) {
352 widget.setWidth(getWidth() - 2);
353 }
354 getApplication().addAccelerator(menuItem, toLower(key));
355 getApplication().recomputeMenuX();
356 activate(0);
357 return menuItem;
358 }
359
360 /**
361 * Convenience function to add a menu item.
362 *
363 * @param id menu item ID. Must be greater than 1024.
364 * @param label menu item label
365 * @return the new menu item
366 */
367 public final TMenuItem addItem(final int id, final String label) {
368 assert (id >= 1024);
369 return addItemInternal(id, label);
370 }
371
372 /**
373 * Convenience function to add a menu item.
374 *
375 * @param id menu item ID
376 * @param label menu item label
377 * @return the new menu item
378 */
379 private TMenuItem addItemInternal(final int id, final String label) {
380 int newY = getChildren().size() + 1;
381 assert (newY < getHeight());
382
383 TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
384 setHeight(getHeight() + 1);
385 if (menuItem.getWidth() + 2 > getWidth()) {
386 setWidth(menuItem.getWidth() + 2);
387 }
388 for (TWidget widget: getChildren()) {
389 widget.setWidth(getWidth() - 2);
390 }
391 getApplication().recomputeMenuX();
392 activate(0);
393 return menuItem;
394 }
395
396 /**
397 * Convenience function to add one of the default menu items.
398 *
399 * @param id menu item ID. Must be between 0 (inclusive) and 1023
400 * (inclusive).
401 * @return the new menu item
402 */
403 public final TMenuItem addDefaultItem(final int id) {
404 assert (id >= 0);
405 assert (id < 1024);
406
407 String label;
408 TKeypress key = null;
409 boolean hasKey = true;
410
411 switch (id) {
412
413 case MID_EXIT:
414 label = "E&xit";
415 key = kbAltX;
416 break;
417
418 case MID_SHELL:
419 label = "O&S Shell";
420 hasKey = false;
421 break;
422
423 case MID_OPEN_FILE:
424 label = "&Open";
425 key = kbAltO;
426 break;
427
428 case MID_CUT:
429 label = "Cu&t";
430 key = kbCtrlX;
431 break;
432 case MID_COPY:
433 label = "&Copy";
434 key = kbCtrlC;
435 break;
436 case MID_PASTE:
437 label = "&Paste";
438 key = kbCtrlV;
439 break;
440 case MID_CLEAR:
441 label = "C&lear";
442 key = kbDel;
443 break;
444
445 case MID_TILE:
446 label = "&Tile";
447 hasKey = false;
448 break;
449 case MID_CASCADE:
450 label = "C&ascade";
451 hasKey = false;
452 break;
453 case MID_CLOSE_ALL:
454 label = "Cl&ose All";
455 hasKey = false;
456 break;
457 case MID_WINDOW_MOVE:
458 label = "&Size/Move";
459 key = kbCtrlF5;
460 break;
461 case MID_WINDOW_ZOOM:
462 label = "&Zoom";
463 key = kbF5;
464 break;
465 case MID_WINDOW_NEXT:
466 label = "&Next";
467 key = kbF6;
468 break;
469 case MID_WINDOW_PREVIOUS:
470 label = "&Previous";
471 key = kbShiftF6;
472 break;
473 case MID_WINDOW_CLOSE:
474 label = "&Close";
475 key = kbCtrlW;
476 break;
477
478 default:
479 throw new IllegalArgumentException("Invalid menu ID: " + id);
480 }
481
482 if (hasKey) {
483 return addItemInternal(id, label, key);
484 }
485 return addItemInternal(id, label);
486 }
487
488 /**
489 * Convenience function to add a menu separator.
490 */
491 public final void addSeparator() {
492 int newY = getChildren().size() + 1;
493 assert (newY < getHeight());
494
495 TMenuItem menuItem = new TMenuSeparator(this, 1, newY);
496 setHeight(getHeight() + 1);
497 }
498
499 /**
500 * Convenience function to add a sub-menu.
501 *
502 * @param title menu title. Title must contain a keyboard shortcut,
503 * denoted by prefixing a letter with "&", e.g. "&File"
504 * @return the new sub-menu
505 */
506 public final TSubMenu addSubMenu(final String title) {
507 int newY = getChildren().size() + 1;
508 assert (newY < getHeight());
509
510 TSubMenu subMenu = new TSubMenu(this, title, 1, newY);
511 setHeight(getHeight() + 1);
512 if (subMenu.getWidth() + 2 > getWidth()) {
513 setWidth(subMenu.getWidth() + 2);
514 }
515 for (TWidget widget: getChildren()) {
516 widget.setWidth(getWidth() - 2);
517 }
518 getApplication().recomputeMenuX();
519 activate(0);
520 subMenu.menu.setX(getX() + getWidth() - 2);
521
522 return subMenu;
523 }
524
525 }