chore(initial):
This commit is contained in:
42 files changed
+21627
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,102 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import GLib, Gtk
|
||||
from loguru import logger
|
||||
|
||||
import config.data as data
|
||||
|
||||
ICON_CACHE_FILE = data.CACHE_DIR + "/icons.json"
|
||||
if not os.path.exists(data.CACHE_DIR):
|
||||
os.makedirs(data.CACHE_DIR)
|
||||
|
||||
|
||||
class IconResolver:
|
||||
def __init__(self, default_applicaiton_icon: str = "application-x-executable-symbolic"):
|
||||
if os.path.exists(ICON_CACHE_FILE):
|
||||
with open(ICON_CACHE_FILE) as f:
|
||||
try:
|
||||
self._icon_dict = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
logger.info("[ICONS] Cache file does not exist or is corrupted")
|
||||
self._icon_dict = {}
|
||||
else:
|
||||
self._icon_dict = {}
|
||||
|
||||
self.default_applicaiton_icon = default_applicaiton_icon
|
||||
|
||||
def get_icon_name(self, app_id: str):
|
||||
if app_id in self._icon_dict:
|
||||
return self._icon_dict[app_id]
|
||||
new_icon = self._compositor_find_icon(app_id)
|
||||
logger.info(
|
||||
f"[ICONS] found new icon: '{new_icon}' for app id: '{app_id}', storing..."
|
||||
)
|
||||
self._store_new_icon(app_id, new_icon)
|
||||
return new_icon
|
||||
|
||||
def get_icon_pixbuf(self, app_id: str, size: int = 16):
|
||||
icon_theme = Gtk.IconTheme.get_default()
|
||||
icon_name = self.get_icon_name(app_id)
|
||||
try:
|
||||
# Try to load the resolved icon.
|
||||
return icon_theme.load_icon(icon_name, size, Gtk.IconLookupFlags.FORCE_SIZE)
|
||||
except GLib.Error as primary_error:
|
||||
logger.warning(
|
||||
f"Warning: Icon '{icon_name}' not found in theme. Error: {primary_error}"
|
||||
)
|
||||
try:
|
||||
# Fallback to the default application icon.
|
||||
return icon_theme.load_icon(
|
||||
self.default_applicaiton_icon, size, Gtk.IconLookupFlags.FORCE_SIZE
|
||||
)
|
||||
except GLib.Error as fallback_error:
|
||||
logger.error(
|
||||
f"Error: Fallback icon '{self.default_applicaiton_icon}' also not found. Error: {fallback_error}"
|
||||
)
|
||||
return None
|
||||
|
||||
def _store_new_icon(self, app_id: str, icon: str):
|
||||
self._icon_dict[app_id] = icon
|
||||
with open(ICON_CACHE_FILE, "w") as f:
|
||||
json.dump(self._icon_dict, f)
|
||||
|
||||
def _get_icon_from_desktop_file(self, desktop_file_path: str):
|
||||
# Retrieve the icon specified in the [Desktop Entry] section.
|
||||
with open(desktop_file_path) as f:
|
||||
for line in f.readlines():
|
||||
if "Icon=" in line:
|
||||
return "".join(line[5:].split())
|
||||
return self.default_applicaiton_icon
|
||||
|
||||
def _get_desktop_file(self, app_id: str) -> str | None:
|
||||
data_dirs = GLib.get_system_data_dirs()
|
||||
for data_dir in data_dirs:
|
||||
data_dir = os.path.join(data_dir, "applications")
|
||||
if os.path.exists(data_dir):
|
||||
files = os.listdir(data_dir)
|
||||
matching = [s for s in files if "".join(app_id.lower().split()) in s.lower()]
|
||||
if matching:
|
||||
return os.path.join(data_dir, matching[0])
|
||||
for word in list(filter(None, re.split(r"-|\.|_|\s", app_id))):
|
||||
matching = [s for s in files if word.lower() in s.lower()]
|
||||
if matching:
|
||||
return os.path.join(data_dir, matching[0])
|
||||
return None
|
||||
|
||||
def _compositor_find_icon(self, app_id: str):
|
||||
icon_theme = Gtk.IconTheme.get_default()
|
||||
if icon_theme.has_icon(app_id):
|
||||
return app_id
|
||||
if icon_theme.has_icon(app_id + "-desktop"):
|
||||
return app_id + "-desktop"
|
||||
desktop_file = self._get_desktop_file(app_id)
|
||||
return (
|
||||
self._get_icon_from_desktop_file(desktop_file)
|
||||
if desktop_file
|
||||
else self.default_applicaiton_icon
|
||||
)
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
# Parameters
|
||||
font_family: str = "tabler-icons"
|
||||
font_weight: str = "normal"
|
||||
|
||||
span: str = f"<span font-family='{font_family}' font-weight='{font_weight}'>"
|
||||
|
||||
# Panels
|
||||
apps: str = ""
|
||||
dashboard: str = ""
|
||||
chat: str = ""
|
||||
wallpapers: str = ""
|
||||
windows: str = ""
|
||||
|
||||
# Bar
|
||||
colorpicker: str = ""
|
||||
media: str = ""
|
||||
|
||||
# Circles
|
||||
temp: str = ""
|
||||
disk: str = ""
|
||||
battery: str = ""
|
||||
memory: str = "流"
|
||||
cpu: str = ""
|
||||
swap: str = ""
|
||||
|
||||
# Toolbox
|
||||
toolbox: str = ""
|
||||
ssfull: str = ""
|
||||
ssregion: str = ""
|
||||
screenshots: str = ""
|
||||
screenrecord: str = ""
|
||||
recordings: str = ""
|
||||
ocr: str = "ﳃ"
|
||||
gamemode: str = ""
|
||||
gamemode_off: str = ""
|
||||
close: str = ""
|
||||
bell: str = ""
|
||||
bell_off: str = ""
|
||||
notifications_on: str = ""
|
||||
notifications_off: str = ""
|
||||
notifications_clear: str = ""
|
||||
notifications: str = ""
|
||||
|
||||
# Wallpapers
|
||||
add: str = ""
|
||||
sort: str = ""
|
||||
circle: str = ""
|
||||
check_box: str = ""
|
||||
box: str = ""
|
||||
scan: str = ""
|
||||
scan_stop: str = ""
|
||||
record: str = ""
|
||||
update: str = "見"
|
||||
updated: str = ""
|
||||
|
||||
# Chevrons
|
||||
chevron_up: str = ""
|
||||
chevron_down: str = ""
|
||||
chevron_left: str = ""
|
||||
chevron_right: str = ""
|
||||
|
||||
# Power
|
||||
lock: str = ""
|
||||
suspend: str = ""
|
||||
logout: str = ""
|
||||
reboot: str = ""
|
||||
shutdown: str = ""
|
||||
|
||||
# Power Manager
|
||||
power_saving: str = ""
|
||||
power_balanced: str = "勺"
|
||||
power_performance: str = ""
|
||||
charging: str = ""
|
||||
discharging: str = ""
|
||||
alert: str = ""
|
||||
bat_charging: str = ""
|
||||
bat_discharging: str = ""
|
||||
bat_low: str = "="
|
||||
bat_full: str = ""
|
||||
|
||||
|
||||
moderate_signal: str = ""
|
||||
good_signal: str = ""
|
||||
no_signal: str = ""
|
||||
weak_signal: str = ""
|
||||
# Applets
|
||||
lan: str = ""
|
||||
wifi_0: str = ""
|
||||
wifi_1: str = ""
|
||||
wifi_2: str = ""
|
||||
wifi_3: str = ""
|
||||
wifi: str = ""
|
||||
world: str = ""
|
||||
world_off: str = ""
|
||||
bluetooth: str = ""
|
||||
night: str = ""
|
||||
coffee: str = ""
|
||||
dnd: str = ""
|
||||
darkmode: str = "﹖"
|
||||
|
||||
|
||||
wifi_off: str = ""
|
||||
bluetooth_off: str = ""
|
||||
night_off: str = ""
|
||||
dnd_off: str = ""
|
||||
|
||||
|
||||
# Bluetooth
|
||||
bluetooth_connected: str = ""
|
||||
bluetooth_disconnected: str = ""
|
||||
|
||||
|
||||
# Player
|
||||
pause: str = ""
|
||||
play: str = ""
|
||||
stop: str = ""
|
||||
skip_back: str = ""
|
||||
skip_forward: str = ""
|
||||
prev: str = ""
|
||||
next: str = ""
|
||||
shuffle: str = ""
|
||||
repeat: str = ""
|
||||
music: str = ""
|
||||
rewind_backward_5: str = "謹"
|
||||
rewind_forward_5: str = "難"
|
||||
|
||||
# Volume
|
||||
vol_off: str = ""
|
||||
vol_mute: str = ""
|
||||
vol_medium: str = ""
|
||||
vol_high: str = ""
|
||||
|
||||
|
||||
mic: str = ""
|
||||
mic_muted: str = ""
|
||||
|
||||
|
||||
# Overview
|
||||
circle_plus: str = ""
|
||||
|
||||
# Pins
|
||||
copy_plus: str = "ﶮ"
|
||||
|
||||
# Clipboard Manager
|
||||
clipboard: str = ""
|
||||
clip_text: str = ""
|
||||
|
||||
# Confirm
|
||||
accept: str = ""
|
||||
cancel: str = ""
|
||||
trash: str = ""
|
||||
clear: str = ""
|
||||
|
||||
# Config
|
||||
config: str = ""
|
||||
|
||||
# Icons
|
||||
firefox: str = ""
|
||||
chromium: str = ""
|
||||
spotify: str = "ﺆ"
|
||||
disc: str = "𐀾"
|
||||
disc_off: str = ""
|
||||
|
||||
# Misc
|
||||
dot: str = ""
|
||||
palette: str = ""
|
||||
cloud_off: str = ""
|
||||
loader: str = ""
|
||||
radar: str = ""
|
||||
emoji: str = ""
|
||||
keyboard: str = ""
|
||||
terminal: str = ""
|
||||
|
||||
|
||||
exceptions: list[str] = ["font_family", "font_weight", "span"]
|
||||
|
||||
|
||||
def apply_span() -> None:
|
||||
global_dict = globals()
|
||||
for key in global_dict:
|
||||
if key not in exceptions and not key.startswith("__"):
|
||||
global_dict[key] = f"{span}{global_dict[key]}</span>"
|
||||
|
||||
|
||||
apply_span()
|
||||
@@ -0,0 +1,141 @@
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
import config.data as data
|
||||
|
||||
|
||||
def get_current_workspace():
|
||||
"""
|
||||
Get the current workspace ID using hyprctl.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["hyprctl", "activeworkspace"], capture_output=True, text=True
|
||||
)
|
||||
# Assume the output similar to: "ID <number>"
|
||||
# Extracting the number from the output
|
||||
parts = result.stdout.split()
|
||||
for i, part in enumerate(parts):
|
||||
if part == "ID" and i + 1 < len(parts):
|
||||
return int(parts[i + 1])
|
||||
except Exception as e:
|
||||
print(f"Error getting current workspace: {e}")
|
||||
return -1
|
||||
|
||||
|
||||
def get_screen_dimensions():
|
||||
"""
|
||||
Get screen dimensions from hyprctl.
|
||||
|
||||
Returns:
|
||||
tuple: (width, height) of the monitor containing the current workspace
|
||||
"""
|
||||
try:
|
||||
# Get current workspace
|
||||
workspace_id = get_current_workspace()
|
||||
|
||||
# Get monitor information
|
||||
result = subprocess.run(
|
||||
["hyprctl", "-j", "monitors"], capture_output=True, text=True
|
||||
)
|
||||
monitors = json.loads(result.stdout)
|
||||
|
||||
# Find the monitor containing our workspace
|
||||
for monitor in monitors:
|
||||
if monitor.get("activeWorkspace", {}).get("id") == workspace_id:
|
||||
return monitor.get("width", data.CURRENT_WIDTH), monitor.get(
|
||||
"height", data.CURRENT_HEIGHT
|
||||
)
|
||||
|
||||
# Fallback to first monitor
|
||||
if monitors:
|
||||
return monitors[0].get("width", data.CURRENT_WIDTH), monitors[0].get(
|
||||
"height", data.CURRENT_HEIGHT
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error getting screen dimensions: {e}")
|
||||
|
||||
# Default fallback values
|
||||
return data.CURRENT_WIDTH, data.CURRENT_HEIGHT
|
||||
|
||||
|
||||
def check_occlusion(occlusion_region, workspace=None):
|
||||
"""
|
||||
Check if a region is occupied by any window on a given workspace.
|
||||
|
||||
Parameters:
|
||||
occlusion_region: Can be one of:
|
||||
- tuple (side, size): where side is "top", "bottom", "left", or "right"
|
||||
and size is the pixel width of the region
|
||||
- tuple (x, y, width, height): The full region coordinates (legacy format)
|
||||
workspace (int, optional): The workspace ID to check. If None, the current workspace is used.
|
||||
|
||||
Returns:
|
||||
bool: True if any window overlaps with the occlusion region, False otherwise.
|
||||
"""
|
||||
if workspace is None:
|
||||
workspace = get_current_workspace()
|
||||
|
||||
# Handle simplified side-based format
|
||||
if isinstance(occlusion_region, tuple) and len(occlusion_region) == 2:
|
||||
side, size = occlusion_region
|
||||
if isinstance(side, str):
|
||||
# Convert side-based format to coordinates
|
||||
screen_width, screen_height = get_screen_dimensions()
|
||||
|
||||
if side.lower() == "bottom":
|
||||
occlusion_region = (0, screen_height - size, screen_width, size)
|
||||
elif side.lower() == "top":
|
||||
occlusion_region = (0, 0, screen_width, size)
|
||||
elif side.lower() == "left":
|
||||
occlusion_region = (0, 0, size, screen_height)
|
||||
elif side.lower() == "right":
|
||||
occlusion_region = (screen_width - size, 0, size, screen_height)
|
||||
|
||||
# Ensure occlusion_region is in the correct format (x, y, width, height)
|
||||
if not isinstance(occlusion_region, tuple) or len(occlusion_region) != 4:
|
||||
print(f"Invalid occlusion region format: {occlusion_region}")
|
||||
return False
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["hyprctl", "-j", "clients"], capture_output=True, text=True
|
||||
)
|
||||
clients = json.loads(result.stdout)
|
||||
except Exception as e:
|
||||
print(f"Error retrieving client windows: {e}")
|
||||
return False
|
||||
|
||||
occ_x, occ_y, occ_width, occ_height = occlusion_region
|
||||
occ_x2 = occ_x + occ_width
|
||||
occ_y2 = occ_y + occ_height
|
||||
|
||||
for client in clients:
|
||||
# Check if client is mapped
|
||||
if not client.get("mapped", False):
|
||||
continue
|
||||
|
||||
# Ensure client has proper workspace information and matches the workspace
|
||||
client_workspace = client.get("workspace", {})
|
||||
if client_workspace.get("id") != workspace:
|
||||
continue
|
||||
|
||||
# Ensure client has position and size info
|
||||
position = client.get("at")
|
||||
size = client.get("size")
|
||||
if not position or not size:
|
||||
continue
|
||||
|
||||
x, y = position
|
||||
width, height = size
|
||||
win_x1, win_y1 = x, y
|
||||
win_x2, win_y2 = x + width, y + height
|
||||
|
||||
# Check for intersection between the window and occlusion region
|
||||
if not (
|
||||
win_x2 <= occ_x or win_x1 >= occ_x2 or win_y2 <= occ_y or win_y1 >= occ_y2
|
||||
):
|
||||
return True # Occlusion region is occupied
|
||||
|
||||
return False # No window overlaps the occlusion region
|
||||
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
from typing import ClassVar
|
||||
|
||||
import gi
|
||||
from fabric.hyprland.service import HyprlandEvent
|
||||
from fabric.hyprland.widgets import get_hyprland_connection
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.wayland import WaylandWindow
|
||||
from fabric.widgets.widget import Widget
|
||||
from gi.repository import Gdk, GLib, GObject, GtkLayerShell
|
||||
from loguru import logger
|
||||
|
||||
gi.require_versions(
|
||||
{"Gtk": "3.0", "Gdk": "3.0", "GtkLayerShell": "0.1", "GObject": "2.0"}
|
||||
)
|
||||
|
||||
|
||||
class PopoverManager:
|
||||
"""Singleton manager to handle shared resources for popovers."""
|
||||
|
||||
_instance = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
# Shared overlay window for all popovers
|
||||
self.overlay = WaylandWindow(
|
||||
name="popover-overlay",
|
||||
style_classes="popover-overlay",
|
||||
title="fabric-shell-popover-overlay",
|
||||
anchor="left top right bottom",
|
||||
margin="-50px 0px 0px 0px",
|
||||
exclusivity="auto",
|
||||
layer="overlay",
|
||||
type="top-level",
|
||||
visible=False,
|
||||
all_visible=False,
|
||||
style="background-color: rgba(0,0,0,0.0);",
|
||||
)
|
||||
|
||||
# Add empty box so GTK doesn't complain
|
||||
self.overlay.add(Box())
|
||||
|
||||
# Keep track of active popovers
|
||||
self.active_popover = None
|
||||
self.available_windows = []
|
||||
|
||||
# Close popover when clicking overlay
|
||||
self.overlay.connect("button-press-event", self._on_overlay_clicked)
|
||||
self._hyprland_connection = get_hyprland_connection()
|
||||
self._hyprland_connection.connect(
|
||||
"event::focusedmonv2", self._on_monitor_change
|
||||
)
|
||||
|
||||
def _on_monitor_change(self, _, event: HyprlandEvent):
|
||||
if self.active_popover:
|
||||
self.active_popover.hide_popover()
|
||||
return True
|
||||
|
||||
def _on_overlay_clicked(self, widget, event):
|
||||
if self.active_popover:
|
||||
self.active_popover.hide_popover()
|
||||
return True
|
||||
|
||||
def get_popover_window(self):
|
||||
"""Get an available popover window or create a new one."""
|
||||
if self.available_windows:
|
||||
return self.available_windows.pop()
|
||||
|
||||
window = WaylandWindow(
|
||||
type="popup",
|
||||
layer="overlay",
|
||||
name="popover-window",
|
||||
anchor="left top",
|
||||
visible=False,
|
||||
all_visible=False,
|
||||
)
|
||||
GtkLayerShell.set_keyboard_interactivity(window, True)
|
||||
window.set_keep_above(True)
|
||||
return window
|
||||
|
||||
def return_popover_window(self, window):
|
||||
"""Return a popover window to the pool."""
|
||||
# Remove any children
|
||||
for child in window.get_children():
|
||||
window.remove(child)
|
||||
|
||||
window.hide()
|
||||
# Only keep a reasonable number of windows in the pool
|
||||
if len(self.available_windows) < 5:
|
||||
self.available_windows.append(window)
|
||||
else:
|
||||
# Let the window be garbage collected
|
||||
window.destroy()
|
||||
|
||||
def activate_popover(self, popover):
|
||||
"""Set the active popover and show overlay."""
|
||||
if self.active_popover and self.active_popover != popover:
|
||||
self.active_popover.hide_popover()
|
||||
|
||||
self.active_popover = popover
|
||||
self.overlay.show()
|
||||
|
||||
|
||||
@GObject.Signal(
|
||||
flags=GObject.SignalFlags.RUN_LAST, return_type=GObject.TYPE_NONE, arg_types=()
|
||||
)
|
||||
def popover_opened(widget: Widget): ...
|
||||
|
||||
|
||||
@GObject.Signal(
|
||||
flags=GObject.SignalFlags.RUN_LAST, return_type=GObject.TYPE_NONE, arg_types=()
|
||||
)
|
||||
def popover_closed(widget: Widget): ...
|
||||
|
||||
|
||||
@GObject.type_register
|
||||
class Popover(Widget):
|
||||
"""Memory-efficient popover implementation."""
|
||||
|
||||
__gsignals__: ClassVar = {
|
||||
"popover-opened": (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()),
|
||||
"popover-closed": (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
point_to,
|
||||
content_factory=None,
|
||||
content=None,
|
||||
):
|
||||
super().__init__()
|
||||
"""
|
||||
Initialize a popover.
|
||||
|
||||
Args:
|
||||
content_factory: Function that returns content widget when called
|
||||
point_to: Widget to position the popover next to
|
||||
"""
|
||||
self._content_factory = content_factory
|
||||
self._point_to = point_to
|
||||
self._content_window = None
|
||||
self._content = content
|
||||
self._visible = False
|
||||
self._destroy_timeout = None
|
||||
# Use weak reference to avoid circular reference issues
|
||||
self._manager = PopoverManager.get_instance()
|
||||
|
||||
def set_content_factory(self, content_factory):
|
||||
"""Set the content factory for the popover."""
|
||||
self._content_factory = content_factory
|
||||
|
||||
def set_content(self, content):
|
||||
"""Set the content for the popover."""
|
||||
self._content = content
|
||||
|
||||
def _on_key_press(self, widget, event):
|
||||
if event.keyval == Gdk.KEY_Escape and self._manager.active_popover:
|
||||
self._manager.active_popover.hide_popover()
|
||||
|
||||
def open(self):
|
||||
if self._destroy_timeout is not None:
|
||||
GLib.source_remove(self._destroy_timeout)
|
||||
self._destroy_timeout = None
|
||||
|
||||
if not self._content_window:
|
||||
try:
|
||||
self._create_popover()
|
||||
except Exception as e:
|
||||
logger.error(f"Could not create popover! Error: {e}")
|
||||
else:
|
||||
self._manager.activate_popover(self)
|
||||
self._content_window.show()
|
||||
self._content_window.steal_input()
|
||||
self._visible = True
|
||||
|
||||
self.emit("popover-opened")
|
||||
|
||||
def _calculate_margins(self):
|
||||
widget_allocation = self._point_to.get_allocation()
|
||||
popover_size = self._content_window.get_size()
|
||||
|
||||
display = Gdk.Display.get_default()
|
||||
screen = display.get_default()
|
||||
monitor_at_window = screen.get_monitor_at_window(self._point_to.get_window())
|
||||
monitor_geometry = monitor_at_window.get_geometry()
|
||||
|
||||
x = (
|
||||
widget_allocation.x
|
||||
+ (widget_allocation.width / 2)
|
||||
- (popover_size.width / 2)
|
||||
)
|
||||
# Position below the button instead of above
|
||||
y = widget_allocation.y + widget_allocation.height + 5 # 5px gap below button
|
||||
|
||||
# Ensure popover stays within monitor bounds
|
||||
if x <= 0:
|
||||
x = widget_allocation.x
|
||||
elif x + popover_size.width >= monitor_geometry.width:
|
||||
x = widget_allocation.x - popover_size.width + widget_allocation.width
|
||||
|
||||
# If popover would go below monitor, position it above the button
|
||||
if y + popover_size.height > monitor_geometry.y + monitor_geometry.height:
|
||||
y = widget_allocation.y - popover_size.height - 5 # 5px gap above button
|
||||
|
||||
return [y, 0, 0, x]
|
||||
|
||||
def set_position(self, position: tuple[int, int, int, int] | None = None):
|
||||
if position is None:
|
||||
self._content_window.set_margin(self._calculate_margins())
|
||||
return False
|
||||
|
||||
self._content_window.set_margin(position)
|
||||
return False
|
||||
|
||||
def _on_content_ready(self, widget, event):
|
||||
self.set_position()
|
||||
|
||||
def _create_popover(self):
|
||||
if self._content is None and self._content_factory is not None:
|
||||
self._content = self._content_factory()
|
||||
|
||||
# Get a window from the pool
|
||||
self._content_window = self._manager.get_popover_window()
|
||||
|
||||
# This is a hack to fix wrong positioning for widgets that are not rendered
|
||||
# immediately (e.g., Gtk.Calendar())
|
||||
self._content.connect("draw", self._on_content_ready)
|
||||
|
||||
# Add content to window
|
||||
self._content_window.add(
|
||||
Box(style_classes="popover-content", children=self._content)
|
||||
)
|
||||
|
||||
self._content_window.connect("focus-out-event", self._on_popover_focus_out)
|
||||
|
||||
self._content_window.connect("key-press-event", self._on_key_press)
|
||||
self._manager.activate_popover(self)
|
||||
self._content_window.show()
|
||||
self._content_window.steal_input()
|
||||
self._visible = True
|
||||
|
||||
def _on_popover_focus_out(self, widget, event):
|
||||
# This helps with keyboard focus issues
|
||||
GLib.timeout_add(100, self.hide_popover)
|
||||
return False
|
||||
|
||||
def hide_popover(self):
|
||||
if not self._visible or not self._content_window:
|
||||
return False
|
||||
|
||||
self._content_window.hide()
|
||||
self._manager.overlay.hide()
|
||||
self._visible = False
|
||||
|
||||
if not self._destroy_timeout:
|
||||
self._destroy_timeout = GLib.timeout_add(1000 * 5, self._destroy_popover)
|
||||
|
||||
self.emit("popover-closed")
|
||||
|
||||
return False
|
||||
|
||||
def _destroy_popover(self):
|
||||
"""Return resources to the pool and clear references."""
|
||||
self._destroy_timeout = None
|
||||
self._visible = False
|
||||
|
||||
if self._content_window:
|
||||
# Return window to the pool
|
||||
self._manager.return_popover_window(self._content_window)
|
||||
self._content_window = None
|
||||
|
||||
# Allow content to be garbage collected if no longer needed
|
||||
self._content = None
|
||||
|
||||
return False
|
||||
Reference in new issue
Block a user