control panel: real padding, seat grab, key hints
- GtkBox set_border_width shrinks the widget's own background, so the card was inset 30px while its contents sat flush against the card edge. Padding is css now: 12px transparent gutter outside, 20px inside. Measured on a virtual display rather than guessed. - the panel grabs the seat on map, like rofi, so it keeps keyboard and pointer focus wherever the cursor is; the grab is released on close - every control shows its key in the bottom-right: n b v d p w for the tiles, a c s t for the row beneath, l o z r q for power, m/i to mute, arrows for volume Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cn3dHfbgNYdbZvzb1b3GQt
This commit is contained in:
parent
9853553782
commit
ddf9321901
3 changed files with 109 additions and 28 deletions
|
|
@ -59,7 +59,7 @@ bspc config pointer_action2 resize_side
|
|||
bspc config pointer_action3 resize_corner
|
||||
|
||||
# Rules
|
||||
bspc rule -a Control-panel.py state=floating layer=above center=on sticky=on border=off
|
||||
bspc rule -a Control-panel.py state=floating layer=above center=on sticky=on border=off follow=on focus=on
|
||||
bspc rule -a feh state=floating
|
||||
bspc rule -a '*:sun-awt-X11-XWindowPeer' manage=off
|
||||
bspc rule -a 'vlc' state=floating center=true
|
||||
|
|
|
|||
|
|
@ -51,7 +51,20 @@ sliders are live and stepping back between views never redraws the panel:
|
|||
| Wallpaper / Particles | shuffle, toggle the particle layer |
|
||||
| Power | lock, log out, suspend, reboot, shut down |
|
||||
|
||||
Escape steps back from a subview; Escape on the main view closes it.
|
||||
Escape steps back from a subview; Escape on the main view closes it. The panel
|
||||
grabs the seat like rofi does, so it keeps focus wherever the pointer is.
|
||||
|
||||
### Panel keys
|
||||
|
||||
Each control shows its key in the bottom-right corner.
|
||||
|
||||
| | | | |
|
||||
|---|---|---|---|
|
||||
| `n` network | `b` bluetooth | `v` vpn | `d` do-not-disturb |
|
||||
| `p` particles | `w` wallpaper | `a` audio | `c` clipboard |
|
||||
| `s` screenshot | `t` toggle bar | `m` mute | `i` mute mic |
|
||||
| `l` lock | `o` log out | `z` suspend | `r` reboot |
|
||||
| `q` shut down | `←/→` volume | `Esc` back / close | |
|
||||
|
||||
## Keybindings
|
||||
|
||||
|
|
|
|||
|
|
@ -32,11 +32,17 @@ window.panel { background-color: transparent; }
|
|||
background-color: rgba(15, 13, 23, 0.88);
|
||||
border: 1px solid rgba(146, 185, 234, 0.16);
|
||||
border-radius: 18px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.title { font-weight: bold; font-size: 15px; color: #e9eefb; }
|
||||
.subtle { color: #8d85a4; font-size: 11px; }
|
||||
.section { color: #92b9ea; font-size: 10px; font-weight: bold; letter-spacing: 1px; }
|
||||
.hint {
|
||||
color: #6f6889;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tile-icon { font-size: 20px; color: #e9eefb; }
|
||||
.tile-name { font-size: 12px; color: #e9eefb; }
|
||||
.tile.active .tile-icon { color: #92b9ea; }
|
||||
|
|
@ -255,6 +261,7 @@ class Panel(Gtk.Window):
|
|||
# the window must not paint, or it paints opaque over the rgba visual
|
||||
self.set_app_paintable(True)
|
||||
|
||||
self.keymap = {}
|
||||
self.stack = Gtk.Stack()
|
||||
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
|
||||
self.stack.set_transition_duration(140)
|
||||
|
|
@ -266,14 +273,14 @@ class Panel(Gtk.Window):
|
|||
|
||||
frame = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
frame.get_style_context().add_class("frame")
|
||||
frame.set_border_width(20) # padding inside the card
|
||||
for setter in ("set_margin_top", "set_margin_bottom",
|
||||
"set_margin_start", "set_margin_end"):
|
||||
getattr(frame, setter)(10) # transparent gutter outside it
|
||||
getattr(frame, setter)(12) # transparent gutter outside the card
|
||||
frame.add(self.stack)
|
||||
self.add(frame)
|
||||
|
||||
self.connect("key-press-event", self.on_key)
|
||||
self.connect("map-event", self.on_map)
|
||||
self.connect("destroy", Gtk.main_quit)
|
||||
|
||||
self.refresh()
|
||||
|
|
@ -304,13 +311,12 @@ class Panel(Gtk.Window):
|
|||
self.t_vpn = self.tile(ICON["vpn_off"], "VPN", self.toggle_vpn)
|
||||
self.t_dnd = self.tile(ICON["dnd_off"], "Notify", self.toggle_dnd)
|
||||
self.t_par = self.tile(ICON["particles"], "Particles", self.toggle_particles)
|
||||
self.t_wall = self.tile(ICON["wall"], "Wallpaper",
|
||||
lambda *_: (spawn("sh ~/.config/scripts/set-wallpaper.sh"),
|
||||
notify("Wallpaper", "Shuffled")))
|
||||
for i, w in enumerate((self.t_net, self.t_bt, self.t_vpn)):
|
||||
grid.attach(w, i, 0, 1, 1)
|
||||
for i, w in enumerate((self.t_dnd, self.t_par, self.t_wall)):
|
||||
grid.attach(w, i, 1, 1, 1)
|
||||
self.t_wall = self.tile(ICON["wall"], "Wallpaper", lambda *_: self.shuffle_wall())
|
||||
tiles = ((self.t_net, "n"), (self.t_bt, "b"), (self.t_vpn, "v"),
|
||||
(self.t_dnd, "d"), (self.t_par, "p"), (self.t_wall, "w"))
|
||||
for i, (w, k) in enumerate(tiles):
|
||||
grid.attach(self.hint(w, k), i % 3, i // 3, 1, 1)
|
||||
self.keymap[k] = (lambda _w=None, btn=w: btn.emit("clicked"))
|
||||
box.add(grid)
|
||||
|
||||
# sliders
|
||||
|
|
@ -326,41 +332,60 @@ class Panel(Gtk.Window):
|
|||
|
||||
# power row
|
||||
prow = Gtk.Box(spacing=8, homogeneous=True)
|
||||
for icon, tip, act, danger in (
|
||||
(ICON["lock"], "Lock", self.do_lock, False),
|
||||
(ICON["logout"], "Log out", lambda *_: spawn("bspc quit"), False),
|
||||
(ICON["suspend"], "Suspend", lambda *_: (self.do_lock(), spawn("systemctl suspend")), False),
|
||||
(ICON["reboot"], "Reboot", lambda *_: spawn("systemctl reboot"), True),
|
||||
(ICON["power"], "Shut down", lambda *_: spawn("systemctl poweroff"), True),
|
||||
for icon, tip, act, danger, key in (
|
||||
(ICON["lock"], "Lock", self.do_lock, False, "l"),
|
||||
(ICON["logout"], "Log out", lambda *_: spawn("bspc quit"), False, "o"),
|
||||
(ICON["suspend"], "Suspend",
|
||||
lambda *_: (self.do_lock(), spawn("systemctl suspend")), False, "z"),
|
||||
(ICON["reboot"], "Reboot", lambda *_: spawn("systemctl reboot"), True, "r"),
|
||||
(ICON["power"], "Shut down", lambda *_: spawn("systemctl poweroff"), True, "q"),
|
||||
):
|
||||
b = Gtk.Button(label=icon)
|
||||
b.set_tooltip_text(tip)
|
||||
b.set_tooltip_text(f"{tip} ({key})")
|
||||
b.get_style_context().add_class("pw")
|
||||
if danger:
|
||||
b.get_style_context().add_class("danger")
|
||||
b.connect("clicked", act)
|
||||
prow.add(b)
|
||||
self.keymap[key] = act
|
||||
prow.add(self.hint(b, key))
|
||||
box.add(prow)
|
||||
|
||||
extra = Gtk.Box(spacing=8, homogeneous=True)
|
||||
for icon, tip, cmd in (
|
||||
(ICON["vol"], "Audio devices & app volumes", "@audio"),
|
||||
for icon, tip, cmd, key in (
|
||||
(ICON["vol"], "Audio devices & app volumes", "@audio", "a"),
|
||||
(ICON["clip"], "Clipboard history",
|
||||
"sh ~/.config/scripts/rofi-menus/clipboard-manager.sh"),
|
||||
(ICON["shot"], "Screenshot", "flameshot gui"),
|
||||
(ICON["bar"], "Toggle bar", "sh ~/.config/scripts/toggle-bar.sh"),
|
||||
"sh ~/.config/scripts/rofi-menus/clipboard-manager.sh", "c"),
|
||||
(ICON["shot"], "Screenshot", "flameshot gui", "s"),
|
||||
(ICON["bar"], "Toggle bar", "sh ~/.config/scripts/toggle-bar.sh", "t"),
|
||||
):
|
||||
b = Gtk.Button(label=icon)
|
||||
b.set_tooltip_text(tip)
|
||||
b.set_tooltip_text(f"{tip} ({key})")
|
||||
b.get_style_context().add_class("pw")
|
||||
if cmd == "@audio":
|
||||
b.connect("clicked", lambda *_: self.go("audio"))
|
||||
act = lambda *_: self.go("audio")
|
||||
else:
|
||||
b.connect("clicked", lambda _w, c=cmd: (spawn(c), self.close()))
|
||||
extra.add(b)
|
||||
act = lambda _w=None, c=cmd: (spawn(c), self.close())
|
||||
b.connect("clicked", act)
|
||||
self.keymap[key] = act
|
||||
extra.add(self.hint(b, key))
|
||||
box.add(extra)
|
||||
return box
|
||||
|
||||
def hint(self, widget, letter):
|
||||
"""Wrap a widget so a small key hint sits in its bottom-right."""
|
||||
ov = Gtk.Overlay()
|
||||
ov.add(widget)
|
||||
lab = Gtk.Label(label=letter)
|
||||
lab.get_style_context().add_class("hint")
|
||||
lab.set_halign(Gtk.Align.END)
|
||||
lab.set_valign(Gtk.Align.END)
|
||||
lab.set_margin_end(7)
|
||||
lab.set_margin_bottom(5)
|
||||
ov.add_overlay(lab)
|
||||
ov.set_overlay_pass_through(lab, True)
|
||||
ov.btn = widget
|
||||
return ov
|
||||
|
||||
def tile(self, icon, label, cb):
|
||||
b = Gtk.Button()
|
||||
b.get_style_context().add_class("tile")
|
||||
|
|
@ -541,6 +566,26 @@ class Panel(Gtk.Window):
|
|||
self.audio_box.add(card)
|
||||
self.audio_box.show_all()
|
||||
|
||||
def on_map(self, *_):
|
||||
"""Own the seat the way rofi does: keyboard and pointer both, so the
|
||||
panel keeps focus wherever the cursor is or whatever gets clicked."""
|
||||
self.present()
|
||||
seat = Gdk.Display.get_default().get_default_seat()
|
||||
gdkwin = self.get_window()
|
||||
if seat and gdkwin:
|
||||
seat.grab(gdkwin, Gdk.SeatCapabilities.ALL, True,
|
||||
None, None, None, None)
|
||||
return False
|
||||
|
||||
def release_grab(self):
|
||||
seat = Gdk.Display.get_default().get_default_seat()
|
||||
if seat:
|
||||
seat.ungrab()
|
||||
|
||||
def close(self, *_):
|
||||
self.release_grab()
|
||||
super().close()
|
||||
|
||||
# -------------------------------------------------------------- actions
|
||||
def go(self, name):
|
||||
if name != "main":
|
||||
|
|
@ -554,6 +599,25 @@ class Panel(Gtk.Window):
|
|||
else:
|
||||
self.close()
|
||||
return True
|
||||
if self.stack.get_visible_child_name() != "main":
|
||||
return False
|
||||
name = Gdk.keyval_name(ev.keyval) or ""
|
||||
if name in ("m", "M"):
|
||||
self.toggle_mute()
|
||||
return True
|
||||
if name in ("i", "I"):
|
||||
self.toggle_micmute()
|
||||
return True
|
||||
if name in ("Left", "Right", "Up", "Down"):
|
||||
step = -5 if name in ("Left", "Down") else 5
|
||||
spawn(f"wpctl set-volume @DEFAULT_AUDIO_SINK@ "
|
||||
f"{abs(step)}%{'-' if step < 0 else '+'} -l 1.3")
|
||||
GLib.timeout_add(180, lambda: (self.refresh(), False)[1])
|
||||
return True
|
||||
act = self.keymap.get(name.lower())
|
||||
if act:
|
||||
act()
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_vol(self, scale):
|
||||
|
|
@ -584,6 +648,10 @@ class Panel(Gtk.Window):
|
|||
spawn("particles || ~/.local/bin/particles")
|
||||
GLib.timeout_add(700, lambda: (self.refresh(), False)[1])
|
||||
|
||||
def shuffle_wall(self):
|
||||
spawn("sh ~/.config/scripts/set-wallpaper.sh")
|
||||
notify("Wallpaper", "Shuffled")
|
||||
|
||||
def toggle_vpn(self, *_):
|
||||
_, _, up = vpn_state()
|
||||
if up:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue