feat(src/window/screencapture): implement screen capture window with toolbar, tools, and options menu
Added a new window class that integrates with the capture service, provides a toolbar of tool buttons for region, screen, window, and recording modes, includes an options button to toggle settings, and implements save directory selection, delay, cursor, thumbnail, and microphone controls. The implementation follows the existing widget architecture and updates related modules to expose the new functionality.
This commit is contained in:
4 files changed
+647
No files matched your search
@@ -0,0 +1,3 @@
|
||||
from window.screencapture.main import ScreenCaptureWindow
|
||||
|
||||
__all__ = ["ScreenCaptureWindow"]
|
||||
@@ -0,0 +1,333 @@
|
||||
from fabric.utils import Gdk, GLib
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
|
||||
from services.screencapture import screen_capture_service
|
||||
from window.screencapture.options_menu import OptionsMenu
|
||||
from window.screencapture.tool_button import ToolButton, ToolGroup
|
||||
|
||||
|
||||
class ScreenCaptureWindow(Window):
|
||||
_TOOLS = [
|
||||
("region.svg", "Screenshot Region", "screenshot-region"),
|
||||
("monitor.svg", "Screenshot Screen", "screenshot-screen"),
|
||||
("window.svg", "Screenshot Window", "screenshot-window"),
|
||||
("region-video.svg", "Record Region", "record-region"),
|
||||
("monitor-video.svg", "Record Screen", "record-screen"),
|
||||
("window-video.svg", "Record Window", "record-window"),
|
||||
]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="sc-window",
|
||||
title="modus-screencapture",
|
||||
anchor="bottom",
|
||||
layer="top",
|
||||
keyboard_mode="on-demand",
|
||||
margin="0px 0px 120px 0px",
|
||||
visible=False,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self._tool_group = ToolGroup()
|
||||
self._options_menu: OptionsMenu | None = None
|
||||
self._delay_timeout_id: int | None = None
|
||||
|
||||
close_lbl = Label(label="✕")
|
||||
close_btn = Button(name="sc-close-btn", child=close_lbl)
|
||||
close_btn.connect("clicked", lambda *_: self.hide())
|
||||
|
||||
tool_boxes = []
|
||||
for i, (icon, tip, tid) in enumerate(self._TOOLS):
|
||||
if i == 3:
|
||||
tool_boxes.append(Box(name="sc-separator"))
|
||||
btn = ToolButton(
|
||||
icon=icon, tooltip=tip, tool_id=tid, group=self._tool_group
|
||||
)
|
||||
self._tool_group.add(btn)
|
||||
tool_boxes.append(btn)
|
||||
|
||||
self._tool_bar = Box(
|
||||
name="sc-tool-bar",
|
||||
orientation="h",
|
||||
spacing=2,
|
||||
children=tool_boxes,
|
||||
)
|
||||
|
||||
# Options button — opens a Gtk.Menu
|
||||
self._options_btn = Button(name="sc-options-btn")
|
||||
opt_inner = Box(
|
||||
orientation="h",
|
||||
spacing=2,
|
||||
children=[
|
||||
Label(name="sc-options-label", label="Options"),
|
||||
Label(name="sc-options-arrow", label="⌄"),
|
||||
],
|
||||
)
|
||||
self._options_btn.add(opt_inner)
|
||||
self._options_btn.connect("clicked", self._toggle_options)
|
||||
|
||||
# Action button
|
||||
self._action_btn = Button(name="sc-action-btn", label="Capture")
|
||||
self._action_btn.connect("clicked", self._on_action)
|
||||
|
||||
# Assemble toolbar
|
||||
toolbar = Box(
|
||||
name="sc-toolbar",
|
||||
orientation="h",
|
||||
spacing=0,
|
||||
children=[
|
||||
close_btn,
|
||||
self._tool_bar,
|
||||
self._options_btn,
|
||||
self._action_btn,
|
||||
],
|
||||
)
|
||||
|
||||
# Wrap in a transparent container to prevent solid square background issues
|
||||
container = Box(
|
||||
name="sc-container",
|
||||
orientation="v",
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
expand=True,
|
||||
children=[toolbar],
|
||||
)
|
||||
|
||||
self.add(container)
|
||||
|
||||
self._tool_group.select_id("screenshot-screen")
|
||||
self._tool_group.on_change(self._on_tool_changed)
|
||||
|
||||
screen_capture_service.connect("recording-started", self._on_recording_started)
|
||||
screen_capture_service.connect("recording-stopped", self._on_recording_stopped)
|
||||
|
||||
self.connect("key-press-event", self._on_key_press)
|
||||
|
||||
def do_focus(self, direction):
|
||||
"""
|
||||
Disable GTK's built-in Tab/Shift+Tab focus traversal so those keys
|
||||
reach our key-press-event handler instead of moving widget focus.
|
||||
"""
|
||||
return False
|
||||
|
||||
def _on_key_press(self, _, event: Gdk.EventKey) -> bool:
|
||||
kv = event.keyval
|
||||
mods = event.state
|
||||
|
||||
# Escape — close window
|
||||
if kv == Gdk.KEY_Escape:
|
||||
self.hide()
|
||||
return True
|
||||
|
||||
# Enter / Space — trigger action
|
||||
if kv in (Gdk.KEY_Return, Gdk.KEY_KP_Enter, Gdk.KEY_space):
|
||||
self._on_action()
|
||||
return True
|
||||
|
||||
# Left / h — previous tool (clamp)
|
||||
if kv in (Gdk.KEY_Left, Gdk.KEY_h):
|
||||
idx = self._tool_group.current_index()
|
||||
self._tool_group.select_index(max(0, idx - 1))
|
||||
return True
|
||||
|
||||
# Right / l — next tool (clamp)
|
||||
if kv in (Gdk.KEY_Right, Gdk.KEY_l):
|
||||
idx = self._tool_group.current_index()
|
||||
self._tool_group.select_index(min(len(self._TOOLS) - 1, idx + 1))
|
||||
return True
|
||||
|
||||
# Shift+Tab — previous tool (wrap)
|
||||
if kv in (Gdk.KEY_Tab, Gdk.KEY_ISO_Left_Tab) and (
|
||||
mods & Gdk.ModifierType.SHIFT_MASK
|
||||
):
|
||||
idx = self._tool_group.current_index()
|
||||
self._tool_group.select_index((idx - 1) % len(self._TOOLS))
|
||||
return True
|
||||
|
||||
# Tab — next tool (wrap)
|
||||
if kv == Gdk.KEY_Tab:
|
||||
idx = self._tool_group.current_index()
|
||||
self._tool_group.select_index((idx + 1) % len(self._TOOLS))
|
||||
return True
|
||||
|
||||
# 1–6 — jump directly to tool
|
||||
if Gdk.KEY_1 <= kv <= Gdk.KEY_6:
|
||||
self._tool_group.select_index(kv - Gdk.KEY_1)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _toggle_options(self, *_):
|
||||
if self._options_menu is None:
|
||||
self._options_menu = OptionsMenu()
|
||||
self._options_menu.popup_below(self._options_btn)
|
||||
|
||||
def _on_tool_changed(self, tool_id: str):
|
||||
is_recording_tool = tool_id.startswith("record-")
|
||||
is_active = screen_capture_service.is_recording
|
||||
|
||||
ctx = self._action_btn.get_style_context()
|
||||
if is_active:
|
||||
self._action_btn.set_label("Stop")
|
||||
ctx.add_class("recording")
|
||||
elif is_recording_tool:
|
||||
self._action_btn.set_label("Record")
|
||||
ctx.remove_class("recording")
|
||||
else:
|
||||
self._action_btn.set_label("Capture")
|
||||
ctx.remove_class("recording")
|
||||
|
||||
def _on_action(self, *_):
|
||||
# Stop an active recording
|
||||
if screen_capture_service.is_recording:
|
||||
screen_capture_service.stop_recording()
|
||||
return
|
||||
|
||||
delay = 0
|
||||
if self._options_menu:
|
||||
delay = self._options_menu.get_timer_delay()
|
||||
|
||||
self.hide()
|
||||
|
||||
if delay > 0:
|
||||
self._delay_timeout_id = GLib.timeout_add_seconds(delay, self._do_action)
|
||||
else:
|
||||
GLib.timeout_add(80, self._do_action)
|
||||
|
||||
def _do_action(self):
|
||||
tool = self._tool_group.selected_id
|
||||
self._delay_timeout_id = None
|
||||
|
||||
# Gather options once
|
||||
use_audio = (
|
||||
self._options_menu.get_mic_enabled() if self._options_menu else False
|
||||
)
|
||||
show_cursor = (
|
||||
self._options_menu.get_show_cursor() if self._options_menu else False
|
||||
)
|
||||
|
||||
if tool == "screenshot-screen":
|
||||
self._do_screenshot("active")
|
||||
elif tool == "screenshot-window":
|
||||
self._do_screenshot("window") # hyprshot -m window
|
||||
elif tool == "screenshot-region":
|
||||
self._do_screenshot("region")
|
||||
elif tool == "record-screen":
|
||||
screen_capture_service.record(
|
||||
"active", use_audio=use_audio, show_cursor=show_cursor
|
||||
)
|
||||
elif tool == "record-window":
|
||||
screen_capture_service.record(
|
||||
"selection", use_audio=use_audio, show_cursor=show_cursor
|
||||
)
|
||||
elif tool == "record-region":
|
||||
screen_capture_service.record(
|
||||
"selection", use_audio=use_audio, show_cursor=show_cursor
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def _do_screenshot(self, target: str):
|
||||
"""
|
||||
Take a screenshot.
|
||||
- On success: service emits screenshot_taken(path) → we stay hidden.
|
||||
- On cancel: service emits screenshot_taken(None) → we re-show.
|
||||
- If screenshot() itself fails immediately → we re-show.
|
||||
The handler is always disconnected after one fire to avoid leaks.
|
||||
"""
|
||||
# Gather options
|
||||
output_dir = None
|
||||
show_cursor = False
|
||||
if self._options_menu:
|
||||
output_dir = self._options_menu.get_save_dir()
|
||||
show_cursor = self._options_menu.get_show_cursor()
|
||||
|
||||
_handler_id = [None]
|
||||
|
||||
def _on_taken(svc, path, *_):
|
||||
# Disconnect immediately — one-shot
|
||||
if _handler_id[0] is not None:
|
||||
try:
|
||||
svc.disconnect(_handler_id[0])
|
||||
except Exception:
|
||||
pass
|
||||
_handler_id[0] = None
|
||||
# Re-show only on cancel (path is None)
|
||||
if not path:
|
||||
GLib.timeout_add(120, self.show)
|
||||
|
||||
_handler_id[0] = screen_capture_service.connect("screenshot-taken", _on_taken)
|
||||
|
||||
ok = screen_capture_service.screenshot(
|
||||
target,
|
||||
output_dir=output_dir,
|
||||
show_cursor=show_cursor,
|
||||
)
|
||||
if not ok:
|
||||
# screenshot() returned False — clean up and re-show immediately
|
||||
if _handler_id[0] is not None:
|
||||
try:
|
||||
screen_capture_service.disconnect(_handler_id[0])
|
||||
except Exception:
|
||||
pass
|
||||
_handler_id[0] = None
|
||||
GLib.timeout_add(120, self.show)
|
||||
|
||||
def _on_recording_started(self, *_):
|
||||
self._action_btn.set_label("Stop")
|
||||
self._action_btn.get_style_context().add_class("recording")
|
||||
self.show()
|
||||
|
||||
def _on_recording_stopped(self, *_):
|
||||
tool = self._tool_group.selected_id or ""
|
||||
ctx = self._action_btn.get_style_context()
|
||||
self._action_btn.set_label(
|
||||
"Record" if tool.startswith("record-") else "Capture"
|
||||
)
|
||||
ctx.remove_class("recording")
|
||||
|
||||
def toggle(self, ss: str = None, sr: str = None):
|
||||
"""
|
||||
Toggle the window, or trigger a direct action via CLI.
|
||||
:param ss: screenshot mode (e.g. 'region', 'fullscreen', 'selectwindow')
|
||||
:param sr: record mode (e.g. 'region', 'fullscreen', 'selectwindow')
|
||||
"""
|
||||
if ss:
|
||||
target_map = {
|
||||
"region": "region",
|
||||
"fullscreen": "active",
|
||||
"selectwindow": "window",
|
||||
}
|
||||
self._do_screenshot(target_map.get(ss, ss))
|
||||
return
|
||||
|
||||
if sr:
|
||||
if screen_capture_service.is_recording:
|
||||
screen_capture_service.stop_recording()
|
||||
return
|
||||
|
||||
target_map = {
|
||||
"region": "selection",
|
||||
"fullscreen": "active",
|
||||
"selectwindow": "selection",
|
||||
}
|
||||
use_audio = (
|
||||
self._options_menu.get_mic_enabled() if self._options_menu else False
|
||||
)
|
||||
show_cursor = (
|
||||
self._options_menu.get_show_cursor() if self._options_menu else False
|
||||
)
|
||||
screen_capture_service.record(
|
||||
target_map.get(sr, sr),
|
||||
use_audio=use_audio,
|
||||
show_cursor=show_cursor,
|
||||
)
|
||||
return
|
||||
|
||||
if self.get_visible():
|
||||
self.hide()
|
||||
else:
|
||||
self.show()
|
||||
@@ -0,0 +1,237 @@
|
||||
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()
|
||||
@@ -0,0 +1,74 @@
|
||||
from fabric.widgets.button import Button
|
||||
from utils.utils import svg_file
|
||||
|
||||
|
||||
class ToolButton(Button):
|
||||
"""A toggle button representing one capture/record mode."""
|
||||
|
||||
def __init__(self, icon: str, tooltip: str, tool_id: str, group, **kwargs):
|
||||
self.tool_id = tool_id
|
||||
self._group = group
|
||||
|
||||
img = svg_file(f"screencapture/{icon}", size=20)
|
||||
|
||||
super().__init__(
|
||||
name="sc-tool-btn",
|
||||
tooltip_text=tooltip,
|
||||
child=img,
|
||||
**kwargs,
|
||||
)
|
||||
self.connect("clicked", self._on_clicked)
|
||||
|
||||
def _on_clicked(self, *_):
|
||||
self._group.select(self)
|
||||
|
||||
def set_active(self, active: bool):
|
||||
ctx = self.get_style_context()
|
||||
if active:
|
||||
ctx.add_class("active")
|
||||
else:
|
||||
ctx.remove_class("active")
|
||||
|
||||
|
||||
class ToolGroup:
|
||||
"""Manages exclusive selection among ToolButtons."""
|
||||
|
||||
def __init__(self):
|
||||
self._buttons: list[ToolButton] = []
|
||||
self._selected: ToolButton | None = None
|
||||
self._callbacks: list = []
|
||||
|
||||
def add(self, btn: ToolButton):
|
||||
self._buttons.append(btn)
|
||||
|
||||
def select(self, btn: ToolButton):
|
||||
if self._selected is btn:
|
||||
return
|
||||
if self._selected:
|
||||
self._selected.set_active(False)
|
||||
self._selected = btn
|
||||
btn.set_active(True)
|
||||
for cb in self._callbacks:
|
||||
cb(btn.tool_id)
|
||||
|
||||
def select_index(self, idx: int):
|
||||
if 0 <= idx < len(self._buttons):
|
||||
self.select(self._buttons[idx])
|
||||
|
||||
def select_id(self, tool_id: str):
|
||||
for btn in self._buttons:
|
||||
if btn.tool_id == tool_id:
|
||||
self.select(btn)
|
||||
return
|
||||
|
||||
def current_index(self) -> int:
|
||||
if self._selected and self._selected in self._buttons:
|
||||
return self._buttons.index(self._selected)
|
||||
return 0
|
||||
|
||||
def on_change(self, cb):
|
||||
self._callbacks.append(cb)
|
||||
|
||||
@property
|
||||
def selected_id(self) -> str | None:
|
||||
return self._selected.tool_id if self._selected else None
|
||||
Reference in new issue
Block a user