feat(astal-menu): tabbed workspace-layout selector
Replace the layout dropdown with a tab bar (one tab per layout — Dwindle · Master · Monocle · Scrolling, from the manifest) over a Gtk.Stack of per-layout options that switches with the tab. Selecting a tab applies that layout to the currently focused workspace; the scrolling page carries its options — a "direction" dropdown and a "center-focused" switch (focus_fit_method). The active tab reflects the workspace's real (per-ws) layout via the layouts state file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bPmain
parent
fa097da436
commit
4196b84ad3
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue