- Configure ruff: isort, pyupgrade, bugbear, flake8 rules - Auto-fix import sorting, redundant parens, exception chaining - Remove from __future__ import annotations (Python 3.13+) - Convert debug print() to logger calls - Fix typo applicaiton → application in icon_resolver - Remove bare returns in animator setters - Move TYPE_CHECKING imports to runtime where used at runtime
239 lines
7.9 KiB
Python
239 lines
7.9 KiB
Python
from fabric.utils import Gdk, Gtk
|
|
|
|
from shared.widgets.smooth_switch import SmoothSwitch
|
|
|
|
|
|
class SwitchMenuItem(Gtk.MenuItem):
|
|
"""
|
|
A Gtk.MenuItem that contains a label + SmoothSwitch side by side.
|
|
Clicking the row toggles the switch.
|
|
"""
|
|
|
|
def __init__(self, label: str, active: bool = True):
|
|
super().__init__()
|
|
self.set_name("sc-menu-item")
|
|
|
|
self._switch = SmoothSwitch(
|
|
active=active,
|
|
width=34,
|
|
height=18,
|
|
style_classes=["sc-switch"],
|
|
v_align="center",
|
|
)
|
|
|
|
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
|
|
row.set_hexpand(True)
|
|
|
|
lbl = Gtk.Label(label=label)
|
|
lbl.set_halign(Gtk.Align.START)
|
|
lbl.set_hexpand(True)
|
|
row.pack_start(lbl, True, True, 0)
|
|
row.pack_end(self._switch, False, False, 0)
|
|
row.show_all()
|
|
|
|
self.add(row)
|
|
self.connect("activate", self._on_activate)
|
|
|
|
def _on_activate(self, *_):
|
|
# Toggle the switch when the menu item is clicked
|
|
self._switch._on_click(self._switch, type("e", (), {"button": 1})())
|
|
|
|
def get_active(self) -> bool:
|
|
return self._switch.get_active()
|
|
|
|
def set_active(self, value: bool):
|
|
self._switch.set_active(value)
|
|
|
|
|
|
class OptionsMenu:
|
|
"""
|
|
Gtk.Menu that pops up below the Options button.
|
|
Contains: Save to, Timer, Microphone, and toggle options.
|
|
State is preserved across opens.
|
|
"""
|
|
|
|
_TIMER_LABELS = ["None", "5 seconds", "10 seconds"]
|
|
_MIC_LABELS = ["None", "Built-in Microphone"]
|
|
|
|
def __init__(self):
|
|
self._timer_idx = 0
|
|
self._mic_idx = 0
|
|
self._menu = Gtk.Menu()
|
|
self._menu.set_name("sc-menu")
|
|
|
|
self._add_section("Save to")
|
|
self._save_path = "~/Pictures/Screenshots"
|
|
save_item = Gtk.MenuItem()
|
|
save_item.set_name("sc-menu-item")
|
|
save_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
|
self._save_lbl = Gtk.Label(label=self._save_path)
|
|
self._save_lbl.set_halign(Gtk.Align.START)
|
|
self._save_lbl.set_hexpand(True)
|
|
browse_lbl = Gtk.Label(label="Other…")
|
|
browse_lbl.get_style_context().add_class("sc-menu-hint")
|
|
save_box.pack_start(self._save_lbl, True, True, 0)
|
|
save_box.pack_end(browse_lbl, False, False, 0)
|
|
save_box.show_all()
|
|
save_item.add(save_box)
|
|
save_item.connect("activate", self._choose_save_dir)
|
|
self._menu.append(save_item)
|
|
|
|
self._add_separator()
|
|
|
|
self._timer_hint_lbl = Gtk.Label(label=self._TIMER_LABELS[0])
|
|
self._timer_hint_lbl.get_style_context().add_class("sc-menu-hint")
|
|
timer_parent = self._make_submenu_item("Timer", self._timer_hint_lbl)
|
|
timer_sub = Gtk.Menu()
|
|
timer_sub.set_name("sc-menu")
|
|
self._timer_items: list[Gtk.CheckMenuItem] = []
|
|
for i, lbl in enumerate(self._TIMER_LABELS):
|
|
item = Gtk.CheckMenuItem(label=lbl)
|
|
item.set_name("sc-menu-item")
|
|
item.set_draw_as_radio(True)
|
|
item.set_active(i == 0)
|
|
item.connect("activate", self._on_timer_select, i)
|
|
timer_sub.append(item)
|
|
self._timer_items.append(item)
|
|
timer_sub.show_all()
|
|
timer_parent.set_submenu(timer_sub)
|
|
self._menu.append(timer_parent)
|
|
|
|
self._add_separator()
|
|
|
|
self._mic_hint_lbl = Gtk.Label(label=self._MIC_LABELS[0])
|
|
self._mic_hint_lbl.get_style_context().add_class("sc-menu-hint")
|
|
mic_parent = self._make_submenu_item("Microphone", self._mic_hint_lbl)
|
|
mic_sub = Gtk.Menu()
|
|
mic_sub.set_name("sc-menu")
|
|
self._mic_items: list[Gtk.CheckMenuItem] = []
|
|
for i, lbl in enumerate(self._MIC_LABELS):
|
|
item = Gtk.CheckMenuItem(label=lbl)
|
|
item.set_name("sc-menu-item")
|
|
item.set_draw_as_radio(True)
|
|
item.set_active(i == 0)
|
|
item.connect("activate", self._on_mic_select, i)
|
|
mic_sub.append(item)
|
|
self._mic_items.append(item)
|
|
mic_sub.show_all()
|
|
mic_parent.set_submenu(mic_sub)
|
|
self._menu.append(mic_parent)
|
|
|
|
self._add_separator()
|
|
|
|
self._add_section("Options")
|
|
self._floating_thumb = SwitchMenuItem("Show Floating Thumbnail", active=True)
|
|
self._remember_sel = SwitchMenuItem("Remember Last Selection", active=True)
|
|
self._show_mouse = SwitchMenuItem("Show Mouse Pointer", active=True)
|
|
self._menu.append(self._floating_thumb)
|
|
self._menu.append(self._remember_sel)
|
|
self._menu.append(self._show_mouse)
|
|
|
|
self._menu.show_all()
|
|
|
|
def _make_submenu_item(self, label: str, hint_lbl: Gtk.Label) -> Gtk.MenuItem:
|
|
"""
|
|
Create a menu item that has a label on the left and a dim hint on the right.
|
|
The hint shows the currently selected sub-option.
|
|
"""
|
|
item = Gtk.MenuItem()
|
|
item.set_name("sc-menu-item")
|
|
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
|
|
row.set_hexpand(True)
|
|
|
|
title = Gtk.Label(label=label)
|
|
title.set_halign(Gtk.Align.START)
|
|
title.set_hexpand(True)
|
|
|
|
hint_lbl.set_halign(Gtk.Align.END)
|
|
hint_lbl.show()
|
|
|
|
row.pack_start(title, True, True, 0)
|
|
row.pack_end(hint_lbl, False, False, 0)
|
|
row.show_all()
|
|
item.add(row)
|
|
return item
|
|
|
|
def _add_section(self, text: str):
|
|
item = Gtk.MenuItem()
|
|
item.set_name("sc-menu-item")
|
|
item.set_sensitive(False)
|
|
lbl = Gtk.Label(label=text.upper())
|
|
lbl.set_name("sc-menu-section")
|
|
lbl.set_halign(Gtk.Align.START)
|
|
lbl.show()
|
|
item.add(lbl)
|
|
self._menu.append(item)
|
|
|
|
def _add_separator(self):
|
|
sep = Gtk.SeparatorMenuItem()
|
|
sep.set_name("sc-menu-sep")
|
|
self._menu.append(sep)
|
|
|
|
def _on_timer_select(self, item, idx: int):
|
|
if not item.get_active():
|
|
return
|
|
self._timer_idx = idx
|
|
self._timer_hint_lbl.set_label(self._TIMER_LABELS[idx])
|
|
for i, t in enumerate(self._timer_items):
|
|
if i != idx:
|
|
t.set_active(False)
|
|
|
|
def _on_mic_select(self, item, idx: int):
|
|
if not item.get_active():
|
|
return
|
|
self._mic_idx = idx
|
|
self._mic_hint_lbl.set_label(self._MIC_LABELS[idx])
|
|
for i, m in enumerate(self._mic_items):
|
|
if i != idx:
|
|
m.set_active(False)
|
|
|
|
def _choose_save_dir(self, *_):
|
|
dialog = Gtk.FileChooserDialog(
|
|
title="Choose Save Location",
|
|
action=Gtk.FileChooserAction.SELECT_FOLDER,
|
|
)
|
|
dialog.add_buttons(
|
|
"_Cancel",
|
|
Gtk.ResponseType.CANCEL,
|
|
"_Select",
|
|
Gtk.ResponseType.ACCEPT,
|
|
)
|
|
resp = dialog.run()
|
|
if resp == Gtk.ResponseType.ACCEPT:
|
|
self._save_path = dialog.get_filename()
|
|
self._save_lbl.set_label(self._save_path)
|
|
dialog.destroy()
|
|
|
|
def popup_below(self, widget: Gtk.Widget):
|
|
"""Pop the menu up, positioned below the given widget."""
|
|
self._menu.popup_at_widget(
|
|
widget,
|
|
Gdk.Gravity.SOUTH_WEST,
|
|
Gdk.Gravity.NORTH_WEST,
|
|
None,
|
|
)
|
|
|
|
def get_timer_delay(self) -> int:
|
|
return [0, 5, 10][self._timer_idx]
|
|
|
|
def get_mic_enabled(self) -> bool:
|
|
"""True when a microphone source (not None) is selected."""
|
|
return self._mic_idx != 0
|
|
|
|
def get_save_dir(self) -> str | None:
|
|
"""
|
|
Returns the custom save directory if the user changed it,
|
|
or None to use the service default.
|
|
"""
|
|
default = "~/Pictures/Screenshots"
|
|
return self._save_path if self._save_path != default else None
|
|
|
|
def get_show_cursor(self) -> bool:
|
|
return self._show_mouse.get_active()
|
|
|
|
def get_show_thumbnail(self) -> bool:
|
|
return self._floating_thumb.get_active()
|
|
|
|
def get_remember_selection(self) -> bool:
|
|
return self._remember_sel.get_active()
|