diff --git a/desktopenvs/hyprlua/astal-menu/style/style.css b/desktopenvs/hyprlua/astal-menu/style/style.css index 2223922..a088a3c 100644 --- a/desktopenvs/hyprlua/astal-menu/style/style.css +++ b/desktopenvs/hyprlua/astal-menu/style/style.css @@ -182,6 +182,7 @@ button:checked.quad-action { background: @accent; color: @bg; border-color: @acc /* pop-open panel: layout controls + per-window rows */ .task-panel { padding: 4px 2px; } +.layout-page { padding: 6px 4px 2px 4px; } .task-row { padding: 2px 4px; border-radius: 10px; min-height: 30px; } .task-row:hover { background: alpha(@violet, 0.12); } .task-name { background: transparent; border: none; padding: 2px 6px; min-height: 26px; } diff --git a/desktopenvs/hyprlua/astal-menu/ui/taskbar.py b/desktopenvs/hyprlua/astal-menu/ui/taskbar.py index 881a8dc..d01f04b 100644 --- a/desktopenvs/hyprlua/astal-menu/ui/taskbar.py +++ b/desktopenvs/hyprlua/astal-menu/ui/taskbar.py @@ -166,27 +166,24 @@ class Taskbar(Gtk.Box): def _rebuild_panel(self) -> None: self._clear(self._panel) - # layout controls for the active workspace - ctrl = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + # tabbed layout selector for the active workspace: a tab per layout, and a + # per-layout options page underneath that switches with the tab. Selecting a + # tab applies that layout to the currently focused workspace. ws = self._active_ws - ctrl.append(Gtk.Label(label=f"Workspace {ws if ws is not None else '?'}", xalign=0.0)) - names = [ly["label"] for ly in self._layouts] - self._layout_dd = Gtk.DropDown.new_from_strings(names or ["…"]) - self._layout_dd.set_tooltip_text("Layout") - self._layout_dd.connect("notify::selected", self._on_layout_pick) - ctrl.append(self._layout_dd) - self._dir_dd = Gtk.DropDown.new_from_strings(["—"]) - self._dir_dd.set_tooltip_text("Direction") - self._dir_dd.connect("notify::selected", self._on_dir_pick) - ctrl.append(self._dir_dd) - self._fit_btn = Gtk.ToggleButton(label="Centered") - self._fit_btn.add_css_class("quad-action") - self._fit_btn.set_tooltip_text( - "Centered follow: keep the focused column centred so the prev/next columns " - "stay on-screen and tappable (scrolling focus_fit_method)") - self._fit_btn.connect("toggled", self._on_fit_toggle) - ctrl.append(self._fit_btn) - self._panel.append(ctrl) + hdr = Gtk.Label(label=f"Workspace {ws if ws is not None else '?'} layout", xalign=0.0) + hdr.add_css_class("section-title") + self._panel.append(hdr) + + self._dir_dd = None + self._fit_sw = None + self._layout_stack = Gtk.Stack() + for ly in self._layouts: + self._layout_stack.add_titled(self._layout_page(ly), ly["name"], ly["label"]) + switcher = Gtk.StackSwitcher(stack=self._layout_stack) + switcher.add_css_class("net-switcher") # reuse the tab pill styling + self._panel.append(switcher) + self._panel.append(self._layout_stack) + self._layout_stack.connect("notify::visible-child-name", self._on_tab_switch) self._sync_layout_controls() self._panel.append(Gtk.Separator()) @@ -231,79 +228,97 @@ class Taskbar(Gtk.Box): row.append(pull) return row - # -- layout control sync + handlers ------------------------------------ + # -- per-layout options page ------------------------------------------- + def _layout_page(self, ly: dict) -> Gtk.Widget: + page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + page.add_css_class("layout-page") + if ly.get("dirs"): + row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + row.append(Gtk.Label(label="direction", xalign=0.0, hexpand=True)) + dd = Gtk.DropDown.new_from_strings(ly["dirs"]) + dd.connect("notify::selected", self._on_opt_change) + row.append(dd) + page.append(row) + self._dir_dd = dd # only scrolling declares dirs + if ly.get("fit_method"): + row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + row.append(Gtk.Label(label="center-focused", xalign=0.0, hexpand=True)) + sw = Gtk.Switch(valign=Gtk.Align.CENTER) + sw.set_tooltip_text("Keep the focused column centred so the prev/next " + "columns stay on-screen and tappable") + sw.connect("state-set", self._on_fit_toggle) + row.append(sw) + page.append(row) + self._fit_sw = sw + if not ly.get("dirs") and not ly.get("fit_method"): + page.append(Gtk.Label(label="No adjustable options", xalign=0.0, + css_classes=["net-ip"])) + return page + + # -- sync + handlers --------------------------------------------------- def _sync_layout_controls(self) -> None: - # reflect the current general:layout / scrolling:direction / focus_fit in the UI + # reflect the current per-ws layout / direction / focus_fit in the tabs+options self._syncing = True - run_json(["hyprctl", "getoption", "general:layout", "-j"], self._apply_layout_sel) + run_json(["hyprctl", "getoption", "general:layout", "-j"], self._apply_tab_sel) run_json(["hyprctl", "getoption", "scrolling:direction", "-j"], self._apply_dir_sel) run_json(["hyprctl", "getoption", "scrolling:focus_fit_method", "-j"], self._apply_fit_sel) - def _apply_fit_sel(self, ok, data) -> None: - val = data.get("int") if ok and isinstance(data, dict) else 0 - self._syncing = True - self._fit_btn.set_active(val == 1) - self._syncing = False - - def _apply_layout_sel(self, ok, data) -> None: + def _apply_tab_sel(self, ok, data) -> None: cur = data.get("str") if ok and isinstance(data, dict) else None - # prefer the per-workspace layout recorded by hypr/layouts (layouts.set), so - # the dropdown reflects THIS workspace, not just the global default. + # prefer the per-workspace layout recorded by hypr/layouts (layouts.set) try: state = json.loads(_LAYOUTS_STATE.read_text()) cur = state.get(str(self._active_ws), cur) except (FileNotFoundError, json.JSONDecodeError): pass - idx = next((i for i, ly in enumerate(self._layouts) if ly["name"] == cur), 0) - self._layout_dd.set_selected(idx) - self._refresh_dir_dd(idx) + if cur and any(ly["name"] == cur for ly in self._layouts): + self._layout_stack.set_visible_child_name(cur) self._syncing = False - def _refresh_dir_dd(self, layout_idx: int) -> None: - ly = self._layouts[layout_idx] if layout_idx < len(self._layouts) else {} - dirs = ly.get("dirs") or [] - self._dir_dd.set_model(Gtk.StringList.new(dirs or ["—"])) - self._dir_dd.set_sensitive(bool(dirs)) - self._fit_btn.set_visible(bool(ly.get("fit_method"))) - def _apply_dir_sel(self, ok, data) -> None: + if self._dir_dd is None: + return cur = data.get("str") if ok and isinstance(data, dict) else None model = self._dir_dd.get_model() - if model is not None: - for i in range(model.get_n_items()): - if model.get_string(i) == cur: - self._dir_dd.set_selected(i) - break + for i in range(model.get_n_items() if model else 0): + if model.get_string(i) == cur: + self._syncing = True + self._dir_dd.set_selected(i) + self._syncing = False + break - def _selected_layout(self) -> dict | None: - i = self._layout_dd.get_selected() - return self._layouts[i] if 0 <= i < len(self._layouts) else None + def _apply_fit_sel(self, ok, data) -> None: + if self._fit_sw is None: + return + val = data.get("int") if ok and isinstance(data, dict) else 0 + self._syncing = True + self._fit_sw.set_active(val == 1) + self._syncing = False - def _on_layout_pick(self, *_a) -> None: - if getattr(self, "_syncing", False): - return - ly = self._selected_layout() - if ly is None or self._active_ws is None: - return - self._refresh_dir_dd(self._layout_dd.get_selected()) - dm = self._dir_dd.get_model() - d = dm.get_string(self._dir_dd.get_selected()) if (dm and ly.get("dirs")) else "" - _eval(f'layouts.set("{self._active_ws}", "{ly["name"]}", "{d}")') + def _cur_dir(self) -> str: + if self._dir_dd is None: + return "" + m = self._dir_dd.get_model() + return m.get_string(self._dir_dd.get_selected()) if m else "" - def _on_fit_toggle(self, btn) -> None: - if getattr(self, "_syncing", False): + def _on_tab_switch(self, *_a) -> None: + if getattr(self, "_syncing", False) or self._active_ws is None: return - _eval(f"layouts.set_fit({1 if btn.get_active() else 0})") + name = self._layout_stack.get_visible_child_name() + if name: + _eval(f'layouts.set("{self._active_ws}", "{name}", "{self._cur_dir()}")') - def _on_dir_pick(self, *_a) -> None: - if getattr(self, "_syncing", False): + def _on_opt_change(self, *_a) -> None: + if getattr(self, "_syncing", False) or self._active_ws is None: return - ly = self._selected_layout() - dm = self._dir_dd.get_model() - if ly is None or not ly.get("dirs") or dm is None or self._active_ws is None: - return - d = dm.get_string(self._dir_dd.get_selected()) - _eval(f'layouts.set("{self._active_ws}", "{ly["name"]}", "{d}")') + name = self._layout_stack.get_visible_child_name() + if name: + _eval(f'layouts.set("{self._active_ws}", "{name}", "{self._cur_dir()}")') + + def _on_fit_toggle(self, _sw, state) -> bool: + if not getattr(self, "_syncing", False): + _eval(f"layouts.set_fit({1 if state else 0})") + return False # -- window actions ---------------------------------------------------- def _focus(self, w) -> None: