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