source: mainline/uspace/lib/ui/src/menubar.c@ 4030072

ticket/834-toolchain-update topic/msim-upgrade topic/simplify-dev-export
Last change on this file since 4030072 was 1543d4c, checked in by Jiri Svoboda <jiri@…>, 21 months ago

Properly close drop-down menu on second menu-bar entry click

Note that this does not work in console due to current limitations
in libui's window emulation.

  • Property mode set to 100644
File size: 15.7 KB
Line 
1/*
2 * Copyright (c) 2023 Jiri Svoboda
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * - Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * - Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * - The name of the author may not be used to endorse or promote products
15 * derived from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29/** @addtogroup libui
30 * @{
31 */
32/**
33 * @file Menu bar
34 */
35
36#include <adt/list.h>
37#include <ctype.h>
38#include <errno.h>
39#include <gfx/color.h>
40#include <gfx/context.h>
41#include <gfx/render.h>
42#include <gfx/text.h>
43#include <io/pos_event.h>
44#include <stdlib.h>
45#include <str.h>
46#include <ui/control.h>
47#include <ui/paint.h>
48#include <ui/menubar.h>
49#include <ui/menudd.h>
50#include <ui/wdecor.h>
51#include <ui/window.h>
52#include "../private/menubar.h"
53#include "../private/resource.h"
54#include "../private/wdecor.h"
55#include "../private/window.h"
56
57enum {
58 menubar_hpad = 4,
59 menubar_vpad = 4,
60 menubar_hpad_text = 1,
61 menubar_vpad_text = 0
62};
63
64static void ui_menu_bar_ctl_destroy(void *);
65static errno_t ui_menu_bar_ctl_paint(void *);
66static ui_evclaim_t ui_menu_bar_ctl_kbd_event(void *, kbd_event_t *);
67static ui_evclaim_t ui_menu_bar_ctl_pos_event(void *, pos_event_t *);
68
69/** Menu bar control ops */
70ui_control_ops_t ui_menu_bar_ops = {
71 .destroy = ui_menu_bar_ctl_destroy,
72 .paint = ui_menu_bar_ctl_paint,
73 .kbd_event = ui_menu_bar_ctl_kbd_event,
74 .pos_event = ui_menu_bar_ctl_pos_event
75};
76
77/** Create new menu bar.
78 *
79 * @param ui UI
80 * @param window Window that will contain the menu bar
81 * @param rmbar Place to store pointer to new menu bar
82 * @return EOK on success, ENOMEM if out of memory
83 */
84errno_t ui_menu_bar_create(ui_t *ui, ui_window_t *window, ui_menu_bar_t **rmbar)
85{
86 ui_menu_bar_t *mbar;
87 errno_t rc;
88
89 mbar = calloc(1, sizeof(ui_menu_bar_t));
90 if (mbar == NULL)
91 return ENOMEM;
92
93 rc = ui_control_new(&ui_menu_bar_ops, (void *) mbar, &mbar->control);
94 if (rc != EOK) {
95 free(mbar);
96 return rc;
97 }
98
99 mbar->ui = ui;
100 mbar->window = window;
101 list_initialize(&mbar->menudds);
102
103 if (window->mbar == NULL)
104 window->mbar = mbar;
105
106 *rmbar = mbar;
107 return EOK;
108}
109
110/** Destroy menu bar
111 *
112 * @param mbar Menu bar or @c NULL
113 */
114void ui_menu_bar_destroy(ui_menu_bar_t *mbar)
115{
116 ui_menu_dd_t *mdd;
117
118 if (mbar == NULL)
119 return;
120
121 if (mbar->window->mbar == mbar)
122 mbar->window->mbar = NULL;
123
124 /* Destroy menu drop-downs */
125 mdd = ui_menu_dd_first(mbar);
126 while (mdd != NULL) {
127 ui_menu_dd_destroy(mdd);
128 mdd = ui_menu_dd_first(mbar);
129 }
130
131 ui_control_delete(mbar->control);
132 free(mbar);
133}
134
135/** Get base control from menu bar.
136 *
137 * @param mbar Menu bar
138 * @return Control
139 */
140ui_control_t *ui_menu_bar_ctl(ui_menu_bar_t *mbar)
141{
142 return mbar->control;
143}
144
145/** Set menu bar rectangle.
146 *
147 * @param mbar Menu bar
148 * @param rect New menu bar rectangle
149 */
150void ui_menu_bar_set_rect(ui_menu_bar_t *mbar, gfx_rect_t *rect)
151{
152 mbar->rect = *rect;
153}
154
155/** Paint menu bar.
156 *
157 * @param mbar Menu bar
158 * @return EOK on success or an error code
159 */
160errno_t ui_menu_bar_paint(ui_menu_bar_t *mbar)
161{
162 ui_resource_t *res;
163 ui_text_fmt_t fmt;
164 gfx_coord2_t pos;
165 gfx_coord2_t tpos;
166 gfx_rect_t rect;
167 gfx_color_t *bg_color;
168 ui_menu_dd_t *mdd;
169 const char *caption;
170 gfx_coord_t width;
171 gfx_coord_t hpad;
172 gfx_coord_t vpad;
173 errno_t rc;
174
175 res = ui_window_get_res(mbar->window);
176
177 /* Paint menu bar background */
178
179 rc = gfx_set_color(res->gc, res->wnd_face_color);
180 if (rc != EOK)
181 goto error;
182
183 rc = gfx_fill_rect(res->gc, &mbar->rect);
184 if (rc != EOK)
185 goto error;
186
187 if (res->textmode) {
188 hpad = menubar_hpad_text;
189 vpad = menubar_vpad_text;
190 } else {
191 hpad = menubar_hpad;
192 vpad = menubar_vpad;
193 }
194
195 pos = mbar->rect.p0;
196
197 ui_text_fmt_init(&fmt);
198 fmt.font = res->font;
199 fmt.halign = gfx_halign_left;
200 fmt.valign = gfx_valign_top;
201
202 mdd = ui_menu_dd_first(mbar);
203 while (mdd != NULL) {
204 caption = ui_menu_dd_caption(mdd);
205 width = ui_text_width(res->font, caption) + 2 * hpad;
206 tpos.x = pos.x + hpad;
207 tpos.y = pos.y + vpad;
208
209 rect.p0 = pos;
210 rect.p1.x = rect.p0.x + width;
211 rect.p1.y = mbar->rect.p1.y;
212
213 if (mdd == mbar->selected) {
214 fmt.color = res->wnd_sel_text_color;
215 fmt.hgl_color = res->wnd_sel_text_hgl_color;
216 bg_color = res->wnd_sel_text_bg_color;
217 } else {
218 fmt.color = res->wnd_text_color;
219 fmt.hgl_color = res->wnd_text_hgl_color;
220 bg_color = res->wnd_face_color;
221 }
222
223 rc = gfx_set_color(res->gc, bg_color);
224 if (rc != EOK)
225 goto error;
226
227 rc = gfx_fill_rect(res->gc, &rect);
228 if (rc != EOK)
229 goto error;
230
231 rc = ui_paint_text(&tpos, &fmt, caption);
232 if (rc != EOK)
233 goto error;
234
235 pos.x += width;
236 mdd = ui_menu_dd_next(mdd);
237 }
238
239 rc = gfx_update(res->gc);
240 if (rc != EOK)
241 goto error;
242
243 return EOK;
244error:
245 return rc;
246}
247
248/** Select or deselect menu from menu bar.
249 *
250 * Select @a menu. If @a menu is @c NULL or it is already selected,
251 * then select none.
252 *
253 * @param mbar Menu bar
254 * @param mdd Menu drop-down to select (or deselect if selected) or @c NULL
255 * @param openup Open menu even if not currently open
256 * @param idev_id Input device ID associated with the selecting seat
257 */
258void ui_menu_bar_select(ui_menu_bar_t *mbar, ui_menu_dd_t *mdd, bool openup,
259 sysarg_t idev_id)
260{
261 ui_menu_dd_t *old_mdd;
262 gfx_rect_t rect;
263 bool was_open;
264
265 old_mdd = mbar->selected;
266
267 mbar->selected = mdd;
268
269 /* Close previously open menu drop-down */
270 if (old_mdd != NULL && ui_menu_dd_is_open(old_mdd)) {
271 was_open = true;
272 (void) ui_menu_dd_close(old_mdd);
273 } else {
274 was_open = false;
275 }
276
277 (void) ui_menu_bar_paint(mbar);
278
279 if (mbar->selected != NULL) {
280 ui_menu_bar_entry_rect(mbar, mbar->selected, &rect);
281 if (openup || was_open) {
282 /*
283 * Open the newly selected menu drop-down if either
284 * the old menu drop-down was open or @a openup was
285 * specified.
286 */
287 (void) ui_menu_dd_open(mbar->selected, &rect, idev_id);
288 }
289
290 mbar->active = true;
291 } else {
292 mbar->active = false;
293 }
294}
295
296/** Select first drop-down.
297 *
298 * @param mbar Menu bar
299 * @param openup @c true to open drop-down if it was not open
300 * @param idev_id Input device ID
301 */
302void ui_menu_bar_select_first(ui_menu_bar_t *mbar, bool openup,
303 sysarg_t idev_id)
304{
305 ui_menu_dd_t *mdd;
306
307 mdd = ui_menu_dd_first(mbar);
308 ui_menu_bar_select(mbar, mdd, openup, idev_id);
309}
310
311/** Select last drop-down.
312 *
313 * @param mbar Menu bar
314 * @param openup @c true to open drop-down if it was not open
315 * @param idev_id Input device ID
316 */
317void ui_menu_bar_select_last(ui_menu_bar_t *mbar, bool openup,
318 sysarg_t idev_id)
319{
320 ui_menu_dd_t *mdd;
321
322 mdd = ui_menu_dd_last(mbar);
323 ui_menu_bar_select(mbar, mdd, openup, idev_id);
324}
325
326/** Select system menu.
327 *
328 * @param mbar Menu bar
329 * @param openup @c true to open drop-down if it was not open
330 * @param idev_id Input device ID
331 */
332void ui_menu_bar_select_sysmenu(ui_menu_bar_t *mbar, bool openup,
333 sysarg_t idev_id)
334{
335 ui_wdecor_sysmenu_hdl_set_active(mbar->window->wdecor, true);
336
337 if (openup)
338 ui_window_send_sysmenu(mbar->window, idev_id);
339}
340
341/** Move one entry left.
342 *
343 * If the selected menu is open, the newly selected menu will be open
344 * as well. If we are already at the first entry, we wrap around.
345 *
346 * @param mbar Menu bar
347 * @param idev_id Input device ID
348 */
349void ui_menu_bar_left(ui_menu_bar_t *mbar, sysarg_t idev_id)
350{
351 ui_menu_dd_t *nmdd;
352 bool sel_sysmenu = false;
353 bool was_open;
354
355 if (mbar->selected == NULL)
356 return;
357
358 nmdd = ui_menu_dd_prev(mbar->selected);
359 if (nmdd == NULL) {
360 if ((mbar->window->wdecor->style & ui_wds_sysmenu_hdl) != 0) {
361 sel_sysmenu = true;
362 } else {
363 nmdd = ui_menu_dd_last(mbar);
364 }
365 }
366
367 was_open = mbar->selected != NULL &&
368 ui_menu_dd_is_open(mbar->selected);
369
370 if (nmdd != mbar->selected)
371 ui_menu_bar_select(mbar, nmdd, false, idev_id);
372
373 /*
374 * Only open sysmenu *after* closing the previous menu, avoid
375 * having multiple popup windows at the same time.
376 */
377 if (sel_sysmenu)
378 ui_menu_bar_select_sysmenu(mbar, was_open, idev_id);
379}
380
381/** Move one entry right.
382 *
383 * If the selected menu is open, the newly selected menu will be open
384 * as well. If we are already at the last entry, we wrap around.
385 *
386 * @param mbar Menu bar
387 * @param idev_id Input device ID
388 */
389void ui_menu_bar_right(ui_menu_bar_t *mbar, sysarg_t idev_id)
390{
391 ui_menu_dd_t *nmdd;
392 bool sel_sysmenu = false;
393 bool was_open;
394
395 if (mbar->selected == NULL)
396 return;
397
398 nmdd = ui_menu_dd_next(mbar->selected);
399 if (nmdd == NULL) {
400 if ((mbar->window->wdecor->style & ui_wds_sysmenu_hdl) != 0) {
401 sel_sysmenu = true;
402 } else {
403 nmdd = ui_menu_dd_first(mbar);
404 }
405 }
406
407 was_open = mbar->selected != NULL &&
408 ui_menu_dd_is_open(mbar->selected);
409
410 if (nmdd != mbar->selected)
411 ui_menu_bar_select(mbar, nmdd, false, idev_id);
412
413 /*
414 * Only open sysmenu *after* closing the previous menu, avoid
415 * having multiple popup windows at the same time.
416 */
417 if (sel_sysmenu)
418 ui_menu_bar_select_sysmenu(mbar, was_open, idev_id);
419}
420
421/** Handle menu bar key press without modifiers.
422 *
423 * @param mbar Menu bar
424 * @param kbd_event Keyboard event
425 * @return @c ui_claimed iff the event is claimed
426 */
427ui_evclaim_t ui_menu_bar_key_press_unmod(ui_menu_bar_t *mbar, kbd_event_t *event)
428{
429 gfx_rect_t rect;
430
431 if (event->key == KC_F10) {
432 ui_menu_bar_activate(mbar);
433 return ui_claimed;
434 }
435
436 if (!mbar->active)
437 return ui_unclaimed;
438
439 if (event->key == KC_ESCAPE) {
440 ui_menu_bar_deactivate(mbar);
441 return ui_claimed;
442 }
443
444 if (event->key == KC_LEFT)
445 ui_menu_bar_left(mbar, event->kbd_id);
446
447 if (event->key == KC_RIGHT)
448 ui_menu_bar_right(mbar, event->kbd_id);
449
450 if (event->key == KC_ENTER || event->key == KC_DOWN) {
451 if (mbar->selected != NULL &&
452 !ui_menu_dd_is_open(mbar->selected)) {
453 ui_menu_bar_entry_rect(mbar, mbar->selected,
454 &rect);
455 ui_menu_dd_open(mbar->selected, &rect, event->kbd_id);
456 }
457
458 return ui_claimed;
459 }
460
461 if (event->c != '\0' && !ui_menu_dd_is_open(mbar->selected)) {
462 /* Check if it is an accelerator. */
463 ui_menu_bar_press_accel(mbar, event->c, event->kbd_id);
464 }
465
466 return ui_claimed;
467}
468
469/** Handle menu bar keyboard event.
470 *
471 * @param mbar Menu bar
472 * @param kbd_event Keyboard event
473 * @return @c ui_claimed iff the event is claimed
474 */
475ui_evclaim_t ui_menu_bar_kbd_event(ui_menu_bar_t *mbar, kbd_event_t *event)
476{
477 if ((event->mods & KM_ALT) != 0 &&
478 (event->mods & (KM_CTRL | KM_SHIFT)) == 0 && event->c != '\0') {
479 /* Check if it is an accelerator. */
480 ui_menu_bar_press_accel(mbar, event->c, event->kbd_id);
481 }
482
483 if (event->type == KEY_PRESS && (event->mods &
484 (KM_CTRL | KM_ALT | KM_SHIFT)) == 0) {
485 return ui_menu_bar_key_press_unmod(mbar, event);
486 }
487
488 if (mbar->active)
489 return ui_claimed;
490
491 return ui_unclaimed;
492}
493
494/** Accelerator key press.
495 *
496 * If @a c matches an accelerator key, open the respective menu.
497 *
498 * @param mbar Menu bar
499 * @param c Character
500 * @param kbd_id Keyboard ID
501 */
502void ui_menu_bar_press_accel(ui_menu_bar_t *mbar, char32_t c, sysarg_t kbd_id)
503{
504 ui_menu_dd_t *mdd;
505 char32_t maccel;
506
507 mdd = ui_menu_dd_first(mbar);
508 while (mdd != NULL) {
509 maccel = ui_menu_dd_get_accel(mdd);
510 if ((char32_t)tolower(c) == maccel) {
511 ui_menu_bar_select(mbar, mdd, true, kbd_id);
512 return;
513 }
514
515 mdd = ui_menu_dd_next(mdd);
516 }
517}
518
519/** Handle menu bar position event.
520 *
521 * @param mbar Menu bar
522 * @param pos_event Position event
523 * @return @c ui_claimed iff the event is claimed
524 */
525ui_evclaim_t ui_menu_bar_pos_event(ui_menu_bar_t *mbar, pos_event_t *event)
526{
527 ui_resource_t *res;
528 gfx_coord2_t pos;
529 gfx_rect_t rect;
530 ui_menu_dd_t *mdd;
531 const char *caption;
532 gfx_coord_t width;
533 gfx_coord_t hpad;
534 gfx_coord2_t ppos;
535 sysarg_t pos_id;
536
537 res = ui_window_get_res(mbar->window);
538
539 ppos.x = event->hpos;
540 ppos.y = event->vpos;
541
542 if (res->textmode) {
543 hpad = menubar_hpad_text;
544 } else {
545 hpad = menubar_hpad;
546 }
547
548 pos = mbar->rect.p0;
549 pos_id = event->pos_id;
550
551 mdd = ui_menu_dd_first(mbar);
552 while (mdd != NULL) {
553 caption = ui_menu_dd_caption(mdd);
554 width = ui_text_width(res->font, caption) + 2 * hpad;
555
556 rect.p0 = pos;
557 rect.p1.x = rect.p0.x + width;
558 rect.p1.y = mbar->rect.p1.y;
559
560 /* Check if press is inside menu bar entry */
561 if (event->type == POS_PRESS &&
562 gfx_pix_inside_rect(&ppos, &rect)) {
563 mbar->active = true;
564
565 /* Open the menu, if not already open. */
566 if (mdd != mbar->selected)
567 ui_menu_bar_select(mbar, mdd, true, pos_id);
568
569 return ui_claimed;
570 }
571
572 pos.x += width;
573 mdd = ui_menu_dd_next(mdd);
574 }
575
576 return ui_unclaimed;
577}
578
579/** Handle menu bar position event.
580 *
581 * @param mbar Menu bar
582 * @param mdd Menu drop-down whose entry's rectangle is to be returned
583 * @param rrect Place to store entry rectangle
584 */
585void ui_menu_bar_entry_rect(ui_menu_bar_t *mbar, ui_menu_dd_t *mdd,
586 gfx_rect_t *rrect)
587{
588 ui_resource_t *res;
589 gfx_coord2_t pos;
590 gfx_rect_t rect;
591 ui_menu_dd_t *cur;
592 const char *caption;
593 gfx_coord_t width;
594 gfx_coord_t hpad;
595
596 res = ui_window_get_res(mbar->window);
597
598 if (res->textmode) {
599 hpad = menubar_hpad_text;
600 } else {
601 hpad = menubar_hpad;
602 }
603
604 pos = mbar->rect.p0;
605
606 cur = ui_menu_dd_first(mbar);
607 while (cur != NULL) {
608 caption = ui_menu_dd_caption(cur);
609 width = ui_text_width(res->font, caption) + 2 * hpad;
610
611 rect.p0 = pos;
612 rect.p1.x = rect.p0.x + width;
613 rect.p1.y = mbar->rect.p1.y;
614
615 if (cur == mdd) {
616 *rrect = rect;
617 return;
618 }
619
620 pos.x += width;
621 cur = ui_menu_dd_next(cur);
622 }
623
624 /* We should never get here */
625 assert(false);
626}
627
628/** Activate menu bar.
629 *
630 * @param mbar Menu bar
631 */
632void ui_menu_bar_activate(ui_menu_bar_t *mbar)
633{
634 if (mbar->active)
635 return;
636
637 mbar->active = true;
638 if (mbar->selected == NULL)
639 mbar->selected = ui_menu_dd_first(mbar);
640
641 (void) ui_menu_bar_paint(mbar);
642}
643
644/** Deactivate menu bar.
645 *
646 * @param mbar Menu bar
647 */
648void ui_menu_bar_deactivate(ui_menu_bar_t *mbar)
649{
650 ui_menu_bar_select(mbar, NULL, false, 0);
651}
652
653/** Destroy menu bar control.
654 *
655 * @param arg Argument (ui_menu_bar_t *)
656 */
657void ui_menu_bar_ctl_destroy(void *arg)
658{
659 ui_menu_bar_t *mbar = (ui_menu_bar_t *) arg;
660
661 ui_menu_bar_destroy(mbar);
662}
663
664/** Paint menu bar control.
665 *
666 * @param arg Argument (ui_menu_bar_t *)
667 * @return EOK on success or an error code
668 */
669errno_t ui_menu_bar_ctl_paint(void *arg)
670{
671 ui_menu_bar_t *mbar = (ui_menu_bar_t *) arg;
672
673 return ui_menu_bar_paint(mbar);
674}
675
676/** Handle menu bar control keyboard event.
677 *
678 * @param arg Argument (ui_menu_bar_t *)
679 * @param pos_event Position event
680 * @return @c ui_claimed iff the event is claimed
681 */
682ui_evclaim_t ui_menu_bar_ctl_kbd_event(void *arg, kbd_event_t *event)
683{
684 ui_menu_bar_t *mbar = (ui_menu_bar_t *) arg;
685
686 return ui_menu_bar_kbd_event(mbar, event);
687}
688
689/** Handle menu bar control position event.
690 *
691 * @param arg Argument (ui_menu_bar_t *)
692 * @param pos_event Position event
693 * @return @c ui_claimed iff the event is claimed
694 */
695ui_evclaim_t ui_menu_bar_ctl_pos_event(void *arg, pos_event_t *event)
696{
697 ui_menu_bar_t *mbar = (ui_menu_bar_t *) arg;
698
699 return ui_menu_bar_pos_event(mbar, event);
700}
701
702/** @}
703 */
Note: See TracBrowser for help on using the repository browser.