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 *
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 */
31package jexer.menu;
32
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
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() {
928811d8
KL
129 CellAttributes background = getTheme().getColor("tmenu");
130
7c870d89 131 assert (isAbsoluteActive());
928811d8
KL
132
133 // Fill in the interior background
134 for (int i = 0; i < getHeight(); i++) {
135 hLineXY(0, i, getWidth(), ' ', background);
136 }
137
138 // Draw the box
139 char cTopLeft;
140 char cTopRight;
141 char cBottomLeft;
142 char cBottomRight;
143 char cHSide;
144
145 cTopLeft = GraphicsChars.ULCORNER;
146 cTopRight = GraphicsChars.URCORNER;
147 cBottomLeft = GraphicsChars.LLCORNER;
148 cBottomRight = GraphicsChars.LRCORNER;
149 cHSide = GraphicsChars.SINGLE_BAR;
150
151 // Place the corner characters
152 putCharXY(1, 0, cTopLeft, background);
153 putCharXY(getWidth() - 2, 0, cTopRight, background);
154 putCharXY(1, getHeight() - 1, cBottomLeft, background);
155 putCharXY(getWidth() - 2, getHeight() - 1, cBottomRight, background);
156
157 // Draw the box lines
158 hLineXY(1 + 1, 0, getWidth() - 4, cHSide, background);
159 hLineXY(1 + 1, getHeight() - 1, getWidth() - 4, cHSide, background);
160
161 // Draw a shadow
162 getScreen().drawBoxShadow(0, 0, getWidth(), getHeight());
163 }
164
165 /**
166 * Handle mouse button presses.
167 *
168 * @param mouse mouse button event
169 */
170 @Override
171 public void onMouseDown(final TMouseEvent mouse) {
172 this.mouse = mouse;
928811d8
KL
173
174 // Pass to children
175 for (TWidget widget: getChildren()) {
176 if (widget.mouseWouldHit(mouse)) {
177 // Dispatch to this child, also activate it
178 activate(widget);
179
180 // Set x and y relative to the child's coordinates
181 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
182 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
183 widget.handleEvent(mouse);
184 return;
185 }
186 }
187 }
188
189 /**
190 * Handle mouse button releases.
191 *
192 * @param mouse mouse button release event
193 */
194 @Override
195 public void onMouseUp(final TMouseEvent mouse) {
196 this.mouse = mouse;
928811d8
KL
197
198 // Pass to children
199 for (TWidget widget: getChildren()) {
200 if (widget.mouseWouldHit(mouse)) {
201 // Dispatch to this child, also activate it
202 activate(widget);
203
204 // Set x and y relative to the child's coordinates
205 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
206 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
207 widget.handleEvent(mouse);
208 return;
209 }
210 }
211 }
212
213 /**
214 * Handle mouse movements.
215 *
216 * @param mouse mouse motion event
217 */
218 @Override
219 public void onMouseMotion(final TMouseEvent mouse) {
220 this.mouse = mouse;
928811d8
KL
221
222 // See if we should activate a different menu item
223 for (TWidget widget: getChildren()) {
7c870d89 224 if ((mouse.isMouse1())
928811d8
KL
225 && (widget.mouseWouldHit(mouse))
226 ) {
227 // Activate this menu item
228 activate(widget);
229 if (widget instanceof TSubMenu) {
230 ((TSubMenu) widget).dispatch();
231 }
232 return;
233 }
234 }
235 }
236
237 /**
238 * Handle keystrokes.
239 *
240 * @param keypress keystroke event
241 */
242 @Override
243 public void onKeypress(final TKeypressEvent keypress) {
91c9a837
KL
244
245 /*
246 System.err.printf("keypress: %s active child: %s\n", keypress,
247 getActiveChild());
248 */
249
250 if (getActiveChild() != this) {
251 if ((getActiveChild() instanceof TSubMenu)
252 || (getActiveChild() instanceof TMenu)
253 ) {
928811d8
KL
254 getActiveChild().onKeypress(keypress);
255 return;
256 }
257 }
258
259 if (keypress.equals(kbEsc)) {
260 getApplication().closeMenu();
261 return;
262 }
263 if (keypress.equals(kbDown)) {
264 switchWidget(true);
265 return;
266 }
267 if (keypress.equals(kbUp)) {
268 switchWidget(false);
269 return;
270 }
271 if (keypress.equals(kbRight)) {
91c9a837 272 getApplication().switchMenu(true);
928811d8
KL
273 return;
274 }
275 if (keypress.equals(kbLeft)) {
276 if (isSubMenu) {
277 getApplication().closeSubMenu();
278 } else {
279 getApplication().switchMenu(false);
280 }
281 return;
282 }
283
284 // Switch to a menuItem if it has an mnemonic
7c870d89
KL
285 if (!keypress.getKey().isFnKey()
286 && !keypress.getKey().isAlt()
287 && !keypress.getKey().isCtrl()) {
928811d8
KL
288 for (TWidget widget: getChildren()) {
289 TMenuItem item = (TMenuItem) widget;
290 if ((item.getMnemonic() != null)
291 && (Character.toLowerCase(item.getMnemonic().getShortcut())
7c870d89 292 == Character.toLowerCase(keypress.getKey().getChar()))
928811d8
KL
293 ) {
294 // Send an enter keystroke to it
295 activate(item);
296 item.handleEvent(new TKeypressEvent(kbEnter));
297 return;
298 }
299 }
300 }
301
302 // Dispatch the keypress to an active widget
303 for (TWidget widget: getChildren()) {
7c870d89 304 if (widget.isActive()) {
928811d8
KL
305 widget.handleEvent(keypress);
306 return;
307 }
308 }
309 }
310
311 /**
312 * Convenience function to add a custom menu item.
313 *
314 * @param id menu item ID. Must be greater than 1024.
315 * @param label menu item label
316 * @param key global keyboard accelerator
317 * @return the new menu item
318 */
8e688b92 319 public final TMenuItem addItem(final int id, final String label,
928811d8
KL
320 final TKeypress key) {
321
322 assert (id >= 1024);
323 return addItemInternal(id, label, key);
324 }
325
326 /**
327 * Convenience function to add a custom menu item.
328 *
329 * @param id menu item ID. Must be greater than 1024.
330 * @param label menu item label
331 * @param key global keyboard accelerator
332 * @return the new menu item
333 */
334 private TMenuItem addItemInternal(final int id, final String label,
335 final TKeypress key) {
336
337 int newY = getChildren().size() + 1;
338 assert (newY < getHeight());
339
340 TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
341 menuItem.setKey(key);
342 setHeight(getHeight() + 1);
343 if (menuItem.getWidth() + 2 > getWidth()) {
344 setWidth(menuItem.getWidth() + 2);
345 }
346 for (TWidget widget: getChildren()) {
347 widget.setWidth(getWidth() - 2);
348 }
e826b451 349 getApplication().addAccelerator(menuItem, key.toLowerCase());
928811d8
KL
350 getApplication().recomputeMenuX();
351 activate(0);
352 return menuItem;
353 }
354
355 /**
356 * Convenience function to add a menu item.
357 *
358 * @param id menu item ID. Must be greater than 1024.
359 * @param label menu item label
360 * @return the new menu item
361 */
8e688b92 362 public final TMenuItem addItem(final int id, final String label) {
928811d8
KL
363 assert (id >= 1024);
364 return addItemInternal(id, label);
365 }
366
367 /**
368 * Convenience function to add a menu item.
369 *
370 * @param id menu item ID
371 * @param label menu item label
372 * @return the new menu item
373 */
374 private TMenuItem addItemInternal(final int id, final String label) {
375 int newY = getChildren().size() + 1;
376 assert (newY < getHeight());
377
378 TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
379 setHeight(getHeight() + 1);
380 if (menuItem.getWidth() + 2 > getWidth()) {
381 setWidth(menuItem.getWidth() + 2);
382 }
383 for (TWidget widget: getChildren()) {
384 widget.setWidth(getWidth() - 2);
385 }
386 getApplication().recomputeMenuX();
387 activate(0);
388 return menuItem;
389 }
390
391 /**
392 * Convenience function to add one of the default menu items.
393 *
394 * @param id menu item ID. Must be between 0 (inclusive) and 1023
395 * (inclusive).
396 * @return the new menu item
397 */
8e688b92 398 public final TMenuItem addDefaultItem(final int id) {
928811d8
KL
399 assert (id >= 0);
400 assert (id < 1024);
401
402 String label;
403 TKeypress key = null;
404 boolean hasKey = true;
405
406 switch (id) {
407
408 case MID_EXIT:
409 label = "E&xit";
410 key = kbAltX;
411 break;
412
413 case MID_SHELL:
414 label = "O&S Shell";
415 hasKey = false;
416 break;
417
418 case MID_OPEN_FILE:
419 label = "&Open";
420 key = kbAltO;
421 break;
422
423 case MID_CUT:
424 label = "Cu&t";
425 key = kbCtrlX;
426 break;
427 case MID_COPY:
428 label = "&Copy";
429 key = kbCtrlC;
430 break;
431 case MID_PASTE:
432 label = "&Paste";
433 key = kbCtrlV;
434 break;
435 case MID_CLEAR:
436 label = "C&lear";
30bd4abd
KL
437 hasKey = false;
438 // key = kbDel;
928811d8
KL
439 break;
440
441 case MID_TILE:
442 label = "&Tile";
443 hasKey = false;
444 break;
445 case MID_CASCADE:
446 label = "C&ascade";
447 hasKey = false;
448 break;
449 case MID_CLOSE_ALL:
450 label = "Cl&ose All";
451 hasKey = false;
452 break;
453 case MID_WINDOW_MOVE:
454 label = "&Size/Move";
455 key = kbCtrlF5;
456 break;
457 case MID_WINDOW_ZOOM:
458 label = "&Zoom";
459 key = kbF5;
460 break;
461 case MID_WINDOW_NEXT:
462 label = "&Next";
463 key = kbF6;
464 break;
465 case MID_WINDOW_PREVIOUS:
466 label = "&Previous";
467 key = kbShiftF6;
468 break;
469 case MID_WINDOW_CLOSE:
470 label = "&Close";
34a42e78
KL
471 hasKey = false;
472 // key = kbCtrlW;
928811d8
KL
473 break;
474
475 default:
476 throw new IllegalArgumentException("Invalid menu ID: " + id);
477 }
478
479 if (hasKey) {
480 return addItemInternal(id, label, key);
481 }
482 return addItemInternal(id, label);
483 }
484
485 /**
486 * Convenience function to add a menu separator.
487 */
8e688b92 488 public final void addSeparator() {
928811d8
KL
489 int newY = getChildren().size() + 1;
490 assert (newY < getHeight());
491
cf9af8df
KL
492 // We just have to construct it, don't need to hang onto what it
493 // makes.
494 new TMenuSeparator(this, 1, newY);
928811d8
KL
495 setHeight(getHeight() + 1);
496 }
497
498 /**
499 * Convenience function to add a sub-menu.
500 *
501 * @param title menu title. Title must contain a keyboard shortcut,
502 * denoted by prefixing a letter with "&", e.g. "&File"
503 * @return the new sub-menu
504 */
8e688b92 505 public final TSubMenu addSubMenu(final String title) {
928811d8
KL
506 int newY = getChildren().size() + 1;
507 assert (newY < getHeight());
508
509 TSubMenu subMenu = new TSubMenu(this, title, 1, newY);
510 setHeight(getHeight() + 1);
511 if (subMenu.getWidth() + 2 > getWidth()) {
512 setWidth(subMenu.getWidth() + 2);
513 }
514 for (TWidget widget: getChildren()) {
515 widget.setWidth(getWidth() - 2);
516 }
517 getApplication().recomputeMenuX();
518 activate(0);
519 subMenu.menu.setX(getX() + getWidth() - 2);
520
521 return subMenu;
522 }
523
524}