Files
Modus/src/window/screencapture/options_menu.py
T
sankalp d8696b9198 fix: add destroy() and signal cleanup across widgets, fix settings window + menu regressions
P0 leak fixes: context/action menus destroyed on deactivate, config
callbacks unregistered in settings widgets, recorder and server signal
IDs stored and disconnected, bluetooth client closed and unsubscribed,
rfkill timer removed, seek timers cancelled, dropdown and options menus
freed, app name cache LRU-bounded.

Fix desktop 'Edit Widgets' and settings menu regressions by deferring
destroy out of Gtk signal handlers via GLib.idle_add.

Fix settings entry fields by connecting the activate signal explicitly
(Enter saves the value).
2026-08-01 19:44:39 +05:45

243 lines
8.1 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 destroy(self):
"""Destroy the persistent Gtk.Menu and its children."""
self._menu.destroy()
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()