#19 expose home/end for TField
[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 *
a2018e99 6 * Copyright (C) 2017 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
55d2b2c2
KL
91 // Help menu
92 public static final int MID_HELP_CONTENTS = 40;
93 public static final int MID_HELP_INDEX = 41;
94 public static final int MID_HELP_SEARCH = 42;
95 public static final int MID_HELP_PREVIOUS = 43;
96 public static final int MID_HELP_HELP = 44;
97 public static final int MID_HELP_ACTIVE_FILE = 45;
98 public static final int MID_ABOUT = 46;
99
928811d8
KL
100 /**
101 * Public constructor.
102 *
103 * @param parent parent application
104 * @param x column relative to parent
105 * @param y row relative to parent
106 * @param label mnemonic menu title. Label must contain a keyboard
107 * shortcut (mnemonic), denoted by prefixing a letter with "&",
108 * e.g. "&File"
109 */
110 public TMenu(final TApplication parent, final int x, final int y,
111 final String label) {
112
113 super(parent, label, x, y, parent.getScreen().getWidth(),
114 parent.getScreen().getHeight());
115
928811d8
KL
116 // Setup the menu shortcut
117 mnemonic = new MnemonicString(label);
118 setTitle(mnemonic.getRawLabel());
119 assert (mnemonic.getShortcutIdx() >= 0);
120
121 // Recompute width and height to reflect an empty menu
122 setWidth(getTitle().length() + 4);
123 setHeight(2);
124
125 setActive(false);
126 }
127
128 /**
129 * Draw a top-level menu with title and menu items.
130 */
131 @Override
132 public void draw() {
928811d8
KL
133 CellAttributes background = getTheme().getColor("tmenu");
134
7c870d89 135 assert (isAbsoluteActive());
928811d8
KL
136
137 // Fill in the interior background
138 for (int i = 0; i < getHeight(); i++) {
139 hLineXY(0, i, getWidth(), ' ', background);
140 }
141
142 // Draw the box
143 char cTopLeft;
144 char cTopRight;
145 char cBottomLeft;
146 char cBottomRight;
147 char cHSide;
148
149 cTopLeft = GraphicsChars.ULCORNER;
150 cTopRight = GraphicsChars.URCORNER;
151 cBottomLeft = GraphicsChars.LLCORNER;
152 cBottomRight = GraphicsChars.LRCORNER;
153 cHSide = GraphicsChars.SINGLE_BAR;
154
155 // Place the corner characters
156 putCharXY(1, 0, cTopLeft, background);
157 putCharXY(getWidth() - 2, 0, cTopRight, background);
158 putCharXY(1, getHeight() - 1, cBottomLeft, background);
159 putCharXY(getWidth() - 2, getHeight() - 1, cBottomRight, background);
160
161 // Draw the box lines
162 hLineXY(1 + 1, 0, getWidth() - 4, cHSide, background);
163 hLineXY(1 + 1, getHeight() - 1, getWidth() - 4, cHSide, background);
164
165 // Draw a shadow
166 getScreen().drawBoxShadow(0, 0, getWidth(), getHeight());
167 }
168
169 /**
170 * Handle mouse button presses.
171 *
172 * @param mouse mouse button event
173 */
174 @Override
175 public void onMouseDown(final TMouseEvent mouse) {
176 this.mouse = mouse;
928811d8
KL
177
178 // Pass to children
179 for (TWidget widget: getChildren()) {
180 if (widget.mouseWouldHit(mouse)) {
181 // Dispatch to this child, also activate it
182 activate(widget);
183
184 // Set x and y relative to the child's coordinates
185 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
186 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
187 widget.handleEvent(mouse);
188 return;
189 }
190 }
191 }
192
193 /**
194 * Handle mouse button releases.
195 *
196 * @param mouse mouse button release event
197 */
198 @Override
199 public void onMouseUp(final TMouseEvent mouse) {
200 this.mouse = mouse;
928811d8
KL
201
202 // Pass to children
203 for (TWidget widget: getChildren()) {
204 if (widget.mouseWouldHit(mouse)) {
205 // Dispatch to this child, also activate it
206 activate(widget);
207
208 // Set x and y relative to the child's coordinates
209 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
210 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
211 widget.handleEvent(mouse);
212 return;
213 }
214 }
215 }
216
217 /**
218 * Handle mouse movements.
219 *
220 * @param mouse mouse motion event
221 */
222 @Override
223 public void onMouseMotion(final TMouseEvent mouse) {
224 this.mouse = mouse;
928811d8
KL
225
226 // See if we should activate a different menu item
227 for (TWidget widget: getChildren()) {
7c870d89 228 if ((mouse.isMouse1())
928811d8
KL
229 && (widget.mouseWouldHit(mouse))
230 ) {
231 // Activate this menu item
232 activate(widget);
233 if (widget instanceof TSubMenu) {
234 ((TSubMenu) widget).dispatch();
235 }
236 return;
237 }
238 }
239 }
240
241 /**
242 * Handle keystrokes.
243 *
244 * @param keypress keystroke event
245 */
246 @Override
247 public void onKeypress(final TKeypressEvent keypress) {
91c9a837
KL
248
249 /*
250 System.err.printf("keypress: %s active child: %s\n", keypress,
251 getActiveChild());
252 */
253
254 if (getActiveChild() != this) {
255 if ((getActiveChild() instanceof TSubMenu)
256 || (getActiveChild() instanceof TMenu)
257 ) {
928811d8
KL
258 getActiveChild().onKeypress(keypress);
259 return;
260 }
261 }
262
263 if (keypress.equals(kbEsc)) {
264 getApplication().closeMenu();
265 return;
266 }
267 if (keypress.equals(kbDown)) {
268 switchWidget(true);
269 return;
270 }
271 if (keypress.equals(kbUp)) {
272 switchWidget(false);
273 return;
274 }
275 if (keypress.equals(kbRight)) {
91c9a837 276 getApplication().switchMenu(true);
928811d8
KL
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
7c870d89
KL
289 if (!keypress.getKey().isFnKey()
290 && !keypress.getKey().isAlt()
291 && !keypress.getKey().isCtrl()) {
928811d8
KL
292 for (TWidget widget: getChildren()) {
293 TMenuItem item = (TMenuItem) widget;
294 if ((item.getMnemonic() != null)
295 && (Character.toLowerCase(item.getMnemonic().getShortcut())
7c870d89 296 == Character.toLowerCase(keypress.getKey().getChar()))
928811d8
KL
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()) {
7c870d89 308 if (widget.isActive()) {
928811d8
KL
309 widget.handleEvent(keypress);
310 return;
311 }
312 }
313 }
314
315 /**
efb7af1f 316 * Convenience function to add a menu item.
928811d8
KL
317 *
318 * @param id menu item ID. Must be greater than 1024.
319 * @param label menu item label
928811d8
KL
320 * @return the new menu item
321 */
329fd62e 322 public TMenuItem addItem(final int id, final String label) {
928811d8 323 assert (id >= 1024);
efb7af1f 324 return addItemInternal(id, label, null);
928811d8
KL
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 */
329fd62e 335 public TMenuItem addItem(final int id, final String label,
928811d8
KL
336 final TKeypress key) {
337
efb7af1f
KL
338 assert (id >= 1024);
339 return addItemInternal(id, label, key);
928811d8
KL
340 }
341
342 /**
efb7af1f 343 * Convenience function to add a custom menu item.
928811d8
KL
344 *
345 * @param id menu item ID. Must be greater than 1024.
346 * @param label menu item label
efb7af1f 347 * @param key global keyboard accelerator
928811d8
KL
348 * @return the new menu item
349 */
efb7af1f
KL
350 private TMenuItem addItemInternal(final int id, final String label,
351 final TKeypress key) {
928811d8 352
928811d8
KL
353 int newY = getChildren().size() + 1;
354 assert (newY < getHeight());
355
356 TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
efb7af1f 357 menuItem.setKey(key);
928811d8
KL
358 setHeight(getHeight() + 1);
359 if (menuItem.getWidth() + 2 > getWidth()) {
360 setWidth(menuItem.getWidth() + 2);
361 }
362 for (TWidget widget: getChildren()) {
363 widget.setWidth(getWidth() - 2);
364 }
efb7af1f 365 getApplication().addMenuItem(menuItem);
928811d8
KL
366 getApplication().recomputeMenuX();
367 activate(0);
368 return menuItem;
369 }
370
371 /**
372 * Convenience function to add one of the default menu items.
373 *
374 * @param id menu item ID. Must be between 0 (inclusive) and 1023
375 * (inclusive).
376 * @return the new menu item
377 */
329fd62e 378 public TMenuItem addDefaultItem(final int id) {
928811d8
KL
379 assert (id >= 0);
380 assert (id < 1024);
381
382 String label;
383 TKeypress key = null;
928811d8
KL
384
385 switch (id) {
386
387 case MID_EXIT:
388 label = "E&xit";
389 key = kbAltX;
390 break;
391
392 case MID_SHELL:
393 label = "O&S Shell";
928811d8
KL
394 break;
395
396 case MID_OPEN_FILE:
397 label = "&Open";
398 key = kbAltO;
399 break;
400
401 case MID_CUT:
402 label = "Cu&t";
403 key = kbCtrlX;
404 break;
405 case MID_COPY:
406 label = "&Copy";
407 key = kbCtrlC;
408 break;
409 case MID_PASTE:
410 label = "&Paste";
411 key = kbCtrlV;
412 break;
413 case MID_CLEAR:
414 label = "C&lear";
30bd4abd 415 // key = kbDel;
928811d8
KL
416 break;
417
418 case MID_TILE:
419 label = "&Tile";
928811d8
KL
420 break;
421 case MID_CASCADE:
422 label = "C&ascade";
928811d8
KL
423 break;
424 case MID_CLOSE_ALL:
425 label = "Cl&ose All";
928811d8
KL
426 break;
427 case MID_WINDOW_MOVE:
428 label = "&Size/Move";
429 key = kbCtrlF5;
430 break;
431 case MID_WINDOW_ZOOM:
432 label = "&Zoom";
433 key = kbF5;
434 break;
435 case MID_WINDOW_NEXT:
436 label = "&Next";
437 key = kbF6;
438 break;
439 case MID_WINDOW_PREVIOUS:
440 label = "&Previous";
441 key = kbShiftF6;
442 break;
443 case MID_WINDOW_CLOSE:
444 label = "&Close";
5dfd1c11 445 key = kbCtrlW;
928811d8
KL
446 break;
447
55d2b2c2
KL
448 case MID_HELP_CONTENTS:
449 label = "&Contents";
450 break;
451 case MID_HELP_INDEX:
452 label = "&Index";
453 key = kbShiftF1;
454 break;
455 case MID_HELP_SEARCH:
456 label = "&Topic search";
457 key = kbCtrlF1;
458 break;
459 case MID_HELP_PREVIOUS:
460 label = "&Previous topic";
461 key = kbAltF1;
462 break;
463 case MID_HELP_HELP:
464 label = "&Help on help";
465 break;
466 case MID_HELP_ACTIVE_FILE:
467 label = "Active &file...";
468 break;
469 case MID_ABOUT:
470 label = "&About...";
471 break;
472
928811d8
KL
473 default:
474 throw new IllegalArgumentException("Invalid menu ID: " + id);
475 }
476
efb7af1f 477 return addItemInternal(id, label, key);
928811d8
KL
478 }
479
480 /**
481 * Convenience function to add a menu separator.
482 */
329fd62e 483 public void addSeparator() {
928811d8
KL
484 int newY = getChildren().size() + 1;
485 assert (newY < getHeight());
486
cf9af8df
KL
487 // We just have to construct it, don't need to hang onto what it
488 // makes.
489 new TMenuSeparator(this, 1, newY);
928811d8
KL
490 setHeight(getHeight() + 1);
491 }
492
493 /**
494 * Convenience function to add a sub-menu.
495 *
496 * @param title menu title. Title must contain a keyboard shortcut,
497 * denoted by prefixing a letter with "&", e.g. "&File"
498 * @return the new sub-menu
499 */
329fd62e 500 public TSubMenu addSubMenu(final String title) {
928811d8
KL
501 int newY = getChildren().size() + 1;
502 assert (newY < getHeight());
503
504 TSubMenu subMenu = new TSubMenu(this, title, 1, newY);
505 setHeight(getHeight() + 1);
506 if (subMenu.getWidth() + 2 > getWidth()) {
507 setWidth(subMenu.getWidth() + 2);
508 }
509 for (TWidget widget: getChildren()) {
510 widget.setWidth(getWidth() - 2);
511 }
512 getApplication().recomputeMenuX();
513 activate(0);
514 subMenu.menu.setX(getX() + getWidth() - 2);
515
516 return subMenu;
517 }
518
519}