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