Merge branch 'master' of https://github.com/klamonte/jexer
[fanfix.git] / src / jexer / menu / TMenu.java
CommitLineData
928811d8
KL
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 */
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() {
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;
928811d8
KL
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;
928811d8
KL
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;
928811d8
KL
228
229 // See if we should activate a different menu item
230 for (TWidget widget: getChildren()) {
231 if ((mouse.getMouse1())
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 if (getActiveChild() != null) {
252 if (getActiveChild() instanceof TSubMenu) {
253 getActiveChild().onKeypress(keypress);
254 return;
255 }
256 }
257
258 if (keypress.equals(kbEsc)) {
259 getApplication().closeMenu();
260 return;
261 }
262 if (keypress.equals(kbDown)) {
263 switchWidget(true);
264 return;
265 }
266 if (keypress.equals(kbUp)) {
267 switchWidget(false);
268 return;
269 }
270 if (keypress.equals(kbRight)) {
271 if (!isSubMenu) {
272 getApplication().switchMenu(true);
273 }
274 return;
275 }
276 if (keypress.equals(kbLeft)) {
277 if (isSubMenu) {
278 getApplication().closeSubMenu();
279 } else {
280 getApplication().switchMenu(false);
281 }
282 return;
283 }
284
285 // Switch to a menuItem if it has an mnemonic
286 if (!keypress.getKey().getIsKey()
287 && !keypress.getKey().getAlt()
288 && !keypress.getKey().getCtrl()) {
289 for (TWidget widget: getChildren()) {
290 TMenuItem item = (TMenuItem) widget;
291 if ((item.getMnemonic() != null)
292 && (Character.toLowerCase(item.getMnemonic().getShortcut())
293 == Character.toLowerCase(keypress.getKey().getCh()))
294 ) {
295 // Send an enter keystroke to it
296 activate(item);
297 item.handleEvent(new TKeypressEvent(kbEnter));
298 return;
299 }
300 }
301 }
302
303 // Dispatch the keypress to an active widget
304 for (TWidget widget: getChildren()) {
305 if (widget.getActive()) {
928811d8
KL
306 widget.handleEvent(keypress);
307 return;
308 }
309 }
310 }
311
312 /**
313 * Convenience function to add a custom menu item.
314 *
315 * @param id menu item ID. Must be greater than 1024.
316 * @param label menu item label
317 * @param key global keyboard accelerator
318 * @return the new menu item
319 */
8e688b92 320 public final TMenuItem addItem(final int id, final String label,
928811d8
KL
321 final TKeypress key) {
322
323 assert (id >= 1024);
324 return addItemInternal(id, label, key);
325 }
326
327 /**
328 * Convenience function to add a custom menu item.
329 *
330 * @param id menu item ID. Must be greater than 1024.
331 * @param label menu item label
332 * @param key global keyboard accelerator
333 * @return the new menu item
334 */
335 private TMenuItem addItemInternal(final int id, final String label,
336 final TKeypress key) {
337
338 int newY = getChildren().size() + 1;
339 assert (newY < getHeight());
340
341 TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
342 menuItem.setKey(key);
343 setHeight(getHeight() + 1);
344 if (menuItem.getWidth() + 2 > getWidth()) {
345 setWidth(menuItem.getWidth() + 2);
346 }
347 for (TWidget widget: getChildren()) {
348 widget.setWidth(getWidth() - 2);
349 }
e826b451 350 getApplication().addAccelerator(menuItem, key.toLowerCase());
928811d8
KL
351 getApplication().recomputeMenuX();
352 activate(0);
353 return menuItem;
354 }
355
356 /**
357 * Convenience function to add a menu item.
358 *
359 * @param id menu item ID. Must be greater than 1024.
360 * @param label menu item label
361 * @return the new menu item
362 */
8e688b92 363 public final TMenuItem addItem(final int id, final String label) {
928811d8
KL
364 assert (id >= 1024);
365 return addItemInternal(id, label);
366 }
367
368 /**
369 * Convenience function to add a menu item.
370 *
371 * @param id menu item ID
372 * @param label menu item label
373 * @return the new menu item
374 */
375 private TMenuItem addItemInternal(final int id, final String label) {
376 int newY = getChildren().size() + 1;
377 assert (newY < getHeight());
378
379 TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
380 setHeight(getHeight() + 1);
381 if (menuItem.getWidth() + 2 > getWidth()) {
382 setWidth(menuItem.getWidth() + 2);
383 }
384 for (TWidget widget: getChildren()) {
385 widget.setWidth(getWidth() - 2);
386 }
387 getApplication().recomputeMenuX();
388 activate(0);
389 return menuItem;
390 }
391
392 /**
393 * Convenience function to add one of the default menu items.
394 *
395 * @param id menu item ID. Must be between 0 (inclusive) and 1023
396 * (inclusive).
397 * @return the new menu item
398 */
8e688b92 399 public final TMenuItem addDefaultItem(final int id) {
928811d8
KL
400 assert (id >= 0);
401 assert (id < 1024);
402
403 String label;
404 TKeypress key = null;
405 boolean hasKey = true;
406
407 switch (id) {
408
409 case MID_EXIT:
410 label = "E&xit";
411 key = kbAltX;
412 break;
413
414 case MID_SHELL:
415 label = "O&S Shell";
416 hasKey = false;
417 break;
418
419 case MID_OPEN_FILE:
420 label = "&Open";
421 key = kbAltO;
422 break;
423
424 case MID_CUT:
425 label = "Cu&t";
426 key = kbCtrlX;
427 break;
428 case MID_COPY:
429 label = "&Copy";
430 key = kbCtrlC;
431 break;
432 case MID_PASTE:
433 label = "&Paste";
434 key = kbCtrlV;
435 break;
436 case MID_CLEAR:
437 label = "C&lear";
30bd4abd
KL
438 hasKey = false;
439 // key = kbDel;
928811d8
KL
440 break;
441
442 case MID_TILE:
443 label = "&Tile";
444 hasKey = false;
445 break;
446 case MID_CASCADE:
447 label = "C&ascade";
448 hasKey = false;
449 break;
450 case MID_CLOSE_ALL:
451 label = "Cl&ose All";
452 hasKey = false;
453 break;
454 case MID_WINDOW_MOVE:
455 label = "&Size/Move";
456 key = kbCtrlF5;
457 break;
458 case MID_WINDOW_ZOOM:
459 label = "&Zoom";
460 key = kbF5;
461 break;
462 case MID_WINDOW_NEXT:
463 label = "&Next";
464 key = kbF6;
465 break;
466 case MID_WINDOW_PREVIOUS:
467 label = "&Previous";
468 key = kbShiftF6;
469 break;
470 case MID_WINDOW_CLOSE:
471 label = "&Close";
34a42e78
KL
472 hasKey = false;
473 // key = kbCtrlW;
928811d8
KL
474 break;
475
476 default:
477 throw new IllegalArgumentException("Invalid menu ID: " + id);
478 }
479
480 if (hasKey) {
481 return addItemInternal(id, label, key);
482 }
483 return addItemInternal(id, label);
484 }
485
486 /**
487 * Convenience function to add a menu separator.
488 */
8e688b92 489 public final void addSeparator() {
928811d8
KL
490 int newY = getChildren().size() + 1;
491 assert (newY < getHeight());
492
493 TMenuItem menuItem = new TMenuSeparator(this, 1, newY);
494 setHeight(getHeight() + 1);
495 }
496
497 /**
498 * Convenience function to add a sub-menu.
499 *
500 * @param title menu title. Title must contain a keyboard shortcut,
501 * denoted by prefixing a letter with "&", e.g. "&File"
502 * @return the new sub-menu
503 */
8e688b92 504 public final TSubMenu addSubMenu(final String title) {
928811d8
KL
505 int newY = getChildren().size() + 1;
506 assert (newY < getHeight());
507
508 TSubMenu subMenu = new TSubMenu(this, title, 1, newY);
509 setHeight(getHeight() + 1);
510 if (subMenu.getWidth() + 2 > getWidth()) {
511 setWidth(subMenu.getWidth() + 2);
512 }
513 for (TWidget widget: getChildren()) {
514 widget.setWidth(getWidth() - 2);
515 }
516 getApplication().recomputeMenuX();
517 activate(0);
518 subMenu.menu.setX(getX() + getWidth() - 2);
519
520 return subMenu;
521 }
522
523}