diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a9ccbb06..b7329c7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,7 @@ on: - 'include/**' - 'protocols/**' - 'clients/**' + - 't/**' - 'scripts/**' - '.github/workflows/**' diff --git a/NEWS.md b/NEWS.md index 1ae97c51..e7d8b383 100644 --- a/NEWS.md +++ b/NEWS.md @@ -115,6 +115,51 @@ differently [#3099]. There is a pending fix [wlroots-5159]. [unreleased-commits] +### Added + +- With the window-switcher custom field state specifiers 's' and 'S', show 's' + for shaded window @domo141 [#2895] +- Support `xdg-dialog` protocol to enable better handling of modal dialogs @xi + [#3134] +- labnag: add --keyboard-focus option @tokyo4j [#3120] +- Allow window switcher to temporarily unshade windows using config option + `` @Amodio @Consolatis [#3124] +- For the 'classic' style window-switcher, add the following theme options: + - `osd.window-switcher.style-classic.item.active.border.color` + - `osd.window-switcher.style-classic.item.active.bg.color` + @tokyo4j [#3118] + +### Fixed + +- Don't remove newlines when parsing config, menu and XBM because doing so can + cause parser error in some unusual situations like the one shown below. + @tokyo4j [#3148] + +``` + +``` + +### Changed + +- If XML documents (like rc.xml and menu.xml) have an XML declaration (typically + ``), this XML declaration must be the first thing in the + document. In previous versions, line breaks (`\n`) were allowed before due to + the way the files were parsed, but this is approach caused other issues like + [#3145] and is contrary to XML syntax. [#3148] [#3153] +- With the window-switcher custom field state specifiers 's' and 'S', change the + display order from M|m|F to m|s|M|F; and increase the size from three + characters wide to four. @domo141 [#2895] +- Call labnag with on-demand keyboard interactivity by default @tokyo4j [#3120] +- Temporarily unshade windows when switching windows. Restore old behaviour with + `` @Amodio @Consolatis [#3124] +- In the classic style window-switcher, the default color of the selected window + item has been changed to inherit the border color but with 15% opacity + @tokyo4j [#3118] + ## 0.9.2 - 2025-10-10 [0.9.2-commits] @@ -2804,6 +2849,7 @@ Compile with wlroots 0.12.0 and wayland-server >=1.16 [#2886]: https://github.com/labwc/labwc/pull/2886 [#2887]: https://github.com/labwc/labwc/pull/2887 [#2891]: https://github.com/labwc/labwc/pull/2891 +[#2895]: https://github.com/labwc/labwc/pull/2895 [#2909]: https://github.com/labwc/labwc/pull/2909 [#2910]: https://github.com/labwc/labwc/pull/2910 [#2914]: https://github.com/labwc/labwc/pull/2914 @@ -2840,4 +2886,11 @@ Compile with wlroots 0.12.0 and wayland-server >=1.16 [#3081]: https://github.com/labwc/labwc/pull/3081 [#3097]: https://github.com/labwc/labwc/pull/3097 [#3099]: https://github.com/labwc/labwc/pull/3099 +[#3118]: https://github.com/labwc/labwc/pull/3118 +[#3120]: https://github.com/labwc/labwc/pull/3120 +[#3124]: https://github.com/labwc/labwc/pull/3124 [#3126]: https://github.com/labwc/labwc/pull/3126 +[#3134]: https://github.com/labwc/labwc/pull/3134 +[#3145]: https://github.com/labwc/labwc/pull/3145 +[#3148]: https://github.com/labwc/labwc/pull/3148 +[#3153]: https://github.com/labwc/labwc/pull/3153 diff --git a/clients/labnag.c b/clients/labnag.c index 23e0be1f..31ac4e9d 100644 --- a/clients/labnag.c +++ b/clients/labnag.c @@ -19,6 +19,7 @@ #ifdef __FreeBSD__ #include /* For signalfd() */ #endif +#include #include #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include "action-prompt-codes.h" #include "pool-buffer.h" #include "cursor-shape-v1-client-protocol.h" @@ -38,6 +40,7 @@ struct conf { char *output; uint32_t anchors; int32_t layer; /* enum zwlr_layer_shell_v1_layer or -1 if unset */ + enum zwlr_layer_surface_v1_keyboard_interactivity keyboard_focus; /* Colors */ uint32_t button_text; @@ -69,11 +72,18 @@ struct pointer { int y; }; +struct keyboard { + struct wl_keyboard *keyboard; + struct xkb_keymap *keymap; + struct xkb_state *state; +}; + struct seat { struct wl_seat *wl_seat; uint32_t wl_name; struct nag *nag; struct pointer pointer; + struct keyboard keyboard; struct wl_list link; /* nag.seats */ }; @@ -130,6 +140,7 @@ struct nag { struct conf *conf; char *message; struct wl_list buttons; + int selected_button; struct pollfd pollfds[NR_FDS]; struct { @@ -409,7 +420,8 @@ render_detailed(cairo_t *cairo, struct nag *nag, uint32_t y) } static uint32_t -render_button(cairo_t *cairo, struct nag *nag, struct button *button, int *x) +render_button(cairo_t *cairo, struct nag *nag, struct button *button, + bool selected, int *x) { int text_width, text_height; get_text_size(cairo, nag->conf->font_description, &text_width, @@ -439,6 +451,14 @@ render_button(cairo_t *cairo, struct nag *nag, struct button *button, int *x) button->width, button->height); cairo_fill(cairo); + if (selected) { + cairo_set_source_u32(cairo, nag->conf->button_border); + cairo_set_line_width(cairo, 1); + cairo_rectangle(cairo, button->x + 1.5, button->y + 1.5, + button->width - 3, button->height - 3); + cairo_stroke(cairo); + } + cairo_set_source_u32(cairo, nag->conf->button_text); cairo_move_to(cairo, button->x + padding, button->y + padding); render_text(cairo, nag->conf->font_description, 1, true, @@ -464,11 +484,13 @@ render_to_cairo(cairo_t *cairo, struct nag *nag) int x = nag->width - nag->conf->button_margin_right; x -= nag->conf->button_gap_close; + int idx = 0; struct button *button; wl_list_for_each(button, &nag->buttons, link) { - h = render_button(cairo, nag, button, &x); + h = render_button(cairo, nag, button, idx == nag->selected_button, &x); max_height = h > max_height ? h : max_height; x -= nag->conf->button_gap; + idx++; } if (nag->details.visible) { @@ -555,6 +577,15 @@ seat_destroy(struct seat *seat) if (seat->pointer.pointer) { wl_pointer_destroy(seat->pointer.pointer); } + if (seat->keyboard.keyboard) { + wl_keyboard_destroy(seat->keyboard.keyboard); + } + if (seat->keyboard.keymap) { + xkb_keymap_unref(seat->keyboard.keymap); + } + if (seat->keyboard.state) { + xkb_state_unref(seat->keyboard.state); + } wl_seat_destroy(seat->wl_seat); wl_list_remove(&seat->link); free(seat); @@ -939,12 +970,170 @@ static const struct wl_pointer_listener pointer_listener = { .axis_discrete = wl_pointer_axis_discrete, }; +static void +wl_keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, + uint32_t format, int32_t fd, uint32_t size) +{ + struct seat *seat = data; + + if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { + wlr_log(WLR_ERROR, "unreconizable keymap format: %d", format); + close(fd); + return; + } + + char *map_shm = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + if (map_shm == MAP_FAILED) { + wlr_log_errno(WLR_ERROR, "mmap()"); + close(fd); + return; + } + + if (seat->keyboard.keymap) { + xkb_keymap_unref(seat->keyboard.keymap); + seat->keyboard.keymap = NULL; + } + if (seat->keyboard.state) { + xkb_state_unref(seat->keyboard.state); + seat->keyboard.state = NULL; + } + struct xkb_context *xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + seat->keyboard.keymap = xkb_keymap_new_from_string(xkb, map_shm, + XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); + if (seat->keyboard.keymap) { + seat->keyboard.state = xkb_state_new(seat->keyboard.keymap); + } else { + wlr_log(WLR_ERROR, "failed to compile keymap"); + } + xkb_context_unref(xkb); + + munmap(map_shm, size); + close(fd); +} + +static void +wl_keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, + struct wl_surface *surface, struct wl_array *keys) +{ +} + +static void +wl_keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, + struct wl_surface *surface) +{ +} + +static void +wl_keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, + uint32_t time, uint32_t key, uint32_t state) +{ + struct seat *seat = data; + struct nag *nag = seat->nag; + + if (!seat->keyboard.keymap || !seat->keyboard.state) { + wlr_log(WLR_ERROR, "keymap/state unavailable"); + return; + } + + if (state != WL_KEYBOARD_KEY_STATE_PRESSED) { + return; + } + + key += 8; + const xkb_keysym_t *syms; + if (!xkb_keymap_key_get_syms_by_level(seat->keyboard.keymap, + key, 0, 0, &syms)) { + wlr_log(WLR_ERROR, "failed to translate key: %d", key); + return; + } + xkb_mod_mask_t mods = xkb_state_serialize_mods(seat->keyboard.state, + XKB_STATE_MODS_EFFECTIVE); + xkb_mod_index_t shift_idx = xkb_keymap_mod_get_index( + seat->keyboard.keymap, XKB_MOD_NAME_SHIFT); + bool shift = shift_idx != XKB_MOD_INVALID && (mods & (1 << shift_idx)); + + int nr_buttons = wl_list_length(&nag->buttons); + + switch (syms[0]) { + case XKB_KEY_Left: + case XKB_KEY_Right: + case XKB_KEY_Tab: { + if (nr_buttons <= 0) { + break; + } + int direction; + if (syms[0] == XKB_KEY_Left || (syms[0] == XKB_KEY_Tab && shift)) { + direction = 1; + } else { + direction = -1; + } + nag->selected_button += nr_buttons + direction; + nag->selected_button %= nr_buttons; + render_frame(nag); + close_pollfd(&nag->pollfds[FD_TIMER]); + break; + } + case XKB_KEY_Escape: + exit_status = LAB_EXIT_CANCELLED; + nag->run_display = false; + break; + case XKB_KEY_Return: + case XKB_KEY_KP_Enter: { + int idx = 0; + struct button *button; + wl_list_for_each(button, &nag->buttons, link) { + if (idx == nag->selected_button) { + button_execute(nag, button); + close_pollfd(&nag->pollfds[FD_TIMER]); + exit_status = idx; + break; + } + idx++; + } + break; + } + } +} + +static void +wl_keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, + uint32_t mods_locked, uint32_t group) +{ + struct seat *seat = data; + + if (!seat->keyboard.state) { + wlr_log(WLR_ERROR, "xkb state unavailable"); + return; + } + + xkb_state_update_mask(seat->keyboard.state, mods_depressed, + mods_latched, mods_locked, 0, 0, group); +} + +static void +wl_keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard, + int32_t rate, int32_t delay) +{ +} + +static const struct wl_keyboard_listener keyboard_listener = { + .keymap = wl_keyboard_keymap, + .enter = wl_keyboard_enter, + .leave = wl_keyboard_leave, + .key = wl_keyboard_key, + .modifiers = wl_keyboard_modifiers, + .repeat_info = wl_keyboard_repeat_info, +}; + static void seat_handle_capabilities(void *data, struct wl_seat *wl_seat, enum wl_seat_capability caps) { struct seat *seat = data; bool cap_pointer = caps & WL_SEAT_CAPABILITY_POINTER; + bool cap_keyboard = caps & WL_SEAT_CAPABILITY_KEYBOARD; + if (cap_pointer && !seat->pointer.pointer) { seat->pointer.pointer = wl_seat_get_pointer(wl_seat); wl_pointer_add_listener(seat->pointer.pointer, @@ -953,6 +1142,15 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, wl_pointer_destroy(seat->pointer.pointer); seat->pointer.pointer = NULL; } + + if (cap_keyboard && !seat->keyboard.keyboard) { + seat->keyboard.keyboard = wl_seat_get_keyboard(wl_seat); + wl_keyboard_add_listener(seat->keyboard.keyboard, + &keyboard_listener, seat); + } else if (!cap_keyboard && seat->keyboard.keyboard) { + wl_keyboard_destroy(seat->keyboard.keyboard); + seat->keyboard.keyboard = NULL; + } } static void @@ -1075,7 +1273,7 @@ handle_global(void *data, struct wl_registry *registry, uint32_t name, } } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { nag->layer_shell = wl_registry_bind( - registry, name, &zwlr_layer_shell_v1_interface, 1); + registry, name, &zwlr_layer_shell_v1_interface, 4); } else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) { nag->cursor_shape_manager = wl_registry_bind( registry, name, &wp_cursor_shape_manager_v1_interface, 1); @@ -1170,6 +1368,8 @@ nag_setup(struct nag *nag) &layer_surface_listener, nag); zwlr_layer_surface_v1_set_anchor(nag->layer_surface, nag->conf->anchors); + zwlr_layer_surface_v1_set_keyboard_interactivity(nag->layer_surface, + nag->conf->keyboard_focus); wl_registry_destroy(registry); @@ -1233,7 +1433,7 @@ nag_run(struct nag *nag) wl_display_cancel_read(nag->display); } if (nag->pollfds[FD_TIMER].revents & POLLIN) { - exit_status = LAB_EXIT_TIMEOUT; + exit_status = LAB_EXIT_CANCELLED; break; } if (nag->pollfds[FD_SIGNAL].revents & POLLIN) { @@ -1250,13 +1450,7 @@ conf_init(struct conf *conf) | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; - conf->button_background = 0x333333FF; - conf->details_background = 0x333333FF; - conf->background = 0x323232FF; - conf->text = 0xFFFFFFFF; - conf->button_text = 0xFFFFFFFF; - conf->button_border = 0x222222FF; - conf->border_bottom = 0x444444FF; + conf->keyboard_focus = ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE; conf->bar_border_thickness = 2; conf->message_padding = 8; conf->details_border_thickness = 3; @@ -1364,6 +1558,7 @@ nag_parse_options(int argc, char **argv, struct nag *nag, {"debug", no_argument, NULL, 'd'}, {"edge", required_argument, NULL, 'e'}, {"layer", required_argument, NULL, 'y'}, + {"keyboard-focus", required_argument, NULL, 'k'}, {"font", required_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"detailed-message", no_argument, NULL, 'l'}, @@ -1402,6 +1597,8 @@ nag_parse_options(int argc, char **argv, struct nag *nag, " -e, --edge top|bottom Set the edge to use.\n" " -y, --layer overlay|top|bottom|background\n" " Set the layer to use.\n" + " -k, --keyboard-focus none|exclusive|on-demand|\n" + " Set the policy for keyboard focus.\n" " -f, --font Set the font to use.\n" " -h, --help Show help message and quit.\n" " -l, --detailed-message Read a detailed message from stdin.\n" @@ -1433,7 +1630,7 @@ nag_parse_options(int argc, char **argv, struct nag *nag, optind = 1; while (1) { - int c = getopt_long(argc, argv, "B:Z:c:de:y:f:hlL:m:o:s:t:vx", opts, NULL); + int c = getopt_long(argc, argv, "B:Z:c:de:y:k:f:hlL:m:o:s:t:vx", opts, NULL); if (c == -1) { break; } @@ -1487,6 +1684,23 @@ nag_parse_options(int argc, char **argv, struct nag *nag, return LAB_EXIT_FAILURE; } break; + case 'k': + if (strcmp(optarg, "none") == 0) { + conf->keyboard_focus = + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE; + } else if (strcmp(optarg, "exclusive") == 0) { + conf->keyboard_focus = + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE; + } else if (strcmp(optarg, "on-demand") == 0) { + conf->keyboard_focus = + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND; + } else { + fprintf(stderr, "Invalid keyboard focus: %s\n" + "Usage: --keyboard-focus none|exclusive|on-demand\n", + optarg); + return LAB_EXIT_FAILURE; + } + break; case 'f': /* Font */ pango_font_description_free(conf->font_description); conf->font_description = pango_font_description_from_string(optarg); @@ -1629,6 +1843,14 @@ main(int argc, char **argv) wl_list_insert(nag.buttons.prev, &nag.details.button_details->link); } + int nr_buttons = wl_list_length(&nag.buttons); + if (conf.keyboard_focus && nr_buttons > 0) { + /* select the leftmost button */ + nag.selected_button = nr_buttons - 1; + } else { + nag.selected_button = -1; + } + wlr_log(WLR_DEBUG, "Output: %s", nag.conf->output); wlr_log(WLR_DEBUG, "Anchors: %lu", (unsigned long)nag.conf->anchors); wlr_log(WLR_DEBUG, "Message: %s", nag.message); diff --git a/clients/meson.build b/clients/meson.build index 54b92db8..467bc035 100644 --- a/clients/meson.build +++ b/clients/meson.build @@ -49,6 +49,7 @@ executable( wlroots, server_protos, epoll_dep, + xkbcommon, ], include_directories: [labwc_inc], install: true, diff --git a/docs/labnag.1.scd b/docs/labnag.1.scd index c8aa4d09..a2a36c10 100644 --- a/docs/labnag.1.scd +++ b/docs/labnag.1.scd @@ -31,6 +31,9 @@ _labnag_ [options...] *-y, --layer* overlay|top|bottom|background Set the layer to use. +*-k, --keyboard-focus none|exclusive|on-demand* + Set the policy for keyboard focus. + *-f, --font* Set the font to use. diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index 21ac2629..00817898 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -285,6 +285,8 @@ this is for compatibility with Openbox. --button-text-color '%t' \\ --border-bottom-size 1 \\ --button-border-size 3 \\ + --keyboard-focus on-demand \\ + --layer overlay \\ --timeout 0 ``` @@ -346,7 +348,7 @@ this is for compatibility with Openbox. ``` -** +** *show* [yes|no] Draw the OnScreenDisplay when switching between windows. Default is yes. @@ -364,6 +366,9 @@ this is for compatibility with Openbox. they are on. Default no (that is only windows on the current workspace are shown). + *unshade* [yes|no] Temporarily unshade windows when switching between + them and permanently unshade on the final selection. Default is yes. + ** Define window switcher fields when using **. @@ -397,9 +402,9 @@ this is for compatibility with Openbox. fields are: - 'B' - shell type, values [xwayland|xdg-shell] - 'b' - shell type (short form), values [X|W] - - 'S' - state of window, values [M|m|F] (3 spaces allocated) - (maximized, minimized, fullscreen) - - 's' - state of window (short form), values [M|m|F] (1 space) + - 'S' - state of window, values [m|s|M|F] (4 spaces allocated) + (minimized, shaded, maximized, fullscreen) + - 's' - state of window (short form), values [m|s|M|F] (1 space) - 'I' - wm-class/app-id - 'i' - wm-class/app-id trimmed, remove "org." if available - 'n' - desktop entry/file application name, falls back to diff --git a/docs/labwc-theme.5.scd b/docs/labwc-theme.5.scd index fad8a77e..8097615b 100644 --- a/docs/labwc-theme.5.scd +++ b/docs/labwc-theme.5.scd @@ -327,6 +327,14 @@ all are supported. Border width of the selection box in the window switcher in pixels. Default is 2. +*osd.window-switcher.style-classic.item.active.border.color* + Border color around the selected window switcher item. + Default is *osd.label.text.color* with 50% opacity. + +*osd.window-switcher.style-classic.item.active.bg.color* + Background color of the selected window switcher item. + Default is *osd.label.text.color* with 15% opacity. + *osd.window-switcher.style-classic.item.icon.size* Size of the icon in window switcher, in pixels. If not set, the font size derived from diff --git a/docs/menu.xml b/docs/menu.xml index b9dda361..4b4d5dda 100644 --- a/docs/menu.xml +++ b/docs/menu.xml @@ -1,5 +1,3 @@ - - diff --git a/docs/rc.xml b/docs/rc.xml index edcca868..ec9a2f6f 100644 --- a/docs/rc.xml +++ b/docs/rc.xml @@ -1,5 +1,3 @@ - -