Files
NexusOS/bin/panel/nexus_menu_base.py

242 lines
6.2 KiB
Python
Executable File

"""Shared base for NexusOS popup menus — override_redirect window, no WM placement."""
# PyGObject (gi.repository) is dynamically generated and its API is Optional-heavy
# (e.g. Gdk.Display.get_default() is typed Display|None). These are fine at runtime,
# so silence the type-checker noise for this GTK desktop script.
# pyright: reportMissingModuleSource=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportAttributeAccessIssue=false
import gi, fcntl, os, sys, signal, subprocess
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk, Gdk, GdkPixbuf, GLib
NEXUS_CSS = b"""
* { font-family: sans-serif; }
window {
background-color: #2a1d33;
color: #f2f2f2;
border: 1px solid #4d3461;
border-radius: 6px;
}
notebook {
background-color: #2a1d33;
}
notebook > header {
background-color: #1e1526;
border-bottom: 1px solid #4d3461;
border-radius: 6px 6px 0 0;
}
notebook > header > tabs > tab {
color: #a8a8a8;
padding: 6px 16px;
border: none;
background-color: transparent;
border-bottom: 2px solid transparent;
}
notebook > header > tabs > tab:checked {
color: #8cc63f;
border-bottom: 2px solid #8cc63f;
}
notebook > header > tabs > tab:hover:not(:checked) {
color: #f2f2f2;
background-color: rgba(140,198,63,0.10);
}
notebook stack {
background-color: #2a1d33;
}
treeview {
background-color: #1e1526;
color: #f2f2f2;
}
treeview:selected {
background-color: #88008f;
color: #ffffff;
}
treeview:hover {
background-color: rgba(140,198,63,0.14);
}
treeview header button {
background-color: #1e1526;
color: #a8a8a8;
border: none;
border-bottom: 1px solid #4d3461;
box-shadow: none;
}
label { color: #f2f2f2; }
button {
background-color: #2e3236;
color: #f2f2f2;
border: 1px solid #3a3d41;
border-radius: 4px;
padding: 5px 14px;
box-shadow: none;
text-shadow: none;
-gtk-icon-shadow: none;
}
button:hover {
background-color: rgba(140,198,63,0.18);
border-color: #8cc63f;
color: #8cc63f;
}
button:active {
background-color: #8cc63f;
color: #0a0a00;
}
scale trough {
background-color: #1e1526;
border: 1px solid #4d3461;
border-radius: 4px;
min-height: 6px;
}
scale trough highlight {
background-color: #8cc63f;
border-radius: 4px;
}
scale slider {
background-color: #8cc63f;
border: none;
border-radius: 50%;
min-width: 16px;
min-height: 16px;
}
separator {
background-color: #4d3461;
min-height: 1px;
}
scrolledwindow { background-color: #1e1526; }
.section-header {
color: #a8a8a8;
font-size: 0.8em;
padding: 4px 8px 2px 8px;
}
.action-row {
padding: 6px 12px;
border-radius: 4px;
}
.action-row:hover {
background-color: rgba(140,198,63,0.14);
color: #8cc63f;
}
.dim { color: #a8a8a8; }
"""
def apply_css():
provider = Gtk.CssProvider()
provider.load_from_data(NEXUS_CSS)
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
def make_popup_window(width, height):
"""Create an override_redirect popup window — bypasses WM placement entirely."""
win = Gtk.Window(type=Gtk.WindowType.POPUP)
win.set_type_hint(Gdk.WindowTypeHint.POPUP_MENU)
win.set_decorated(False)
win.set_skip_taskbar_hint(True)
win.set_skip_pager_hint(True)
win.set_keep_above(True)
win.set_default_size(width, height)
win.set_resizable(False)
win.connect('key-press-event', lambda w, e: w.destroy() if e.keyval == Gdk.KEY_Escape else None)
def on_button_press(w, event):
wx, wy = w.get_position()
ww = w.get_allocated_width()
wh = w.get_allocated_height()
rx, ry = int(event.x_root), int(event.y_root)
if rx < wx or rx >= wx + ww or ry < wy or ry >= wy + wh:
w.destroy()
return False
win.connect('button-press-event', on_button_press)
def on_map(w, _event):
def do_grab():
gdk_win = w.get_window()
if gdk_win:
seat = Gdk.Display.get_default().get_default_seat()
seat.grab(gdk_win, Gdk.SeatCapabilities.ALL, True, None, None, None)
return False # don't repeat
GLib.timeout_add(150, do_grab)
return False
win.connect('map-event', on_map)
def on_destroy(w):
Gdk.Display.get_default().get_default_seat().ungrab()
win.connect('destroy', on_destroy)
return win
def get_mouse_position():
display = Gdk.Display.get_default()
seat = display.get_default_seat()
_, x, y = seat.get_pointer().get_position()
return x, y
def get_panel_bottom():
"""Return y-coordinate just below the top panel via _NET_WORKAREA."""
try:
out = subprocess.check_output(
['xprop', '-root', '_NET_WORKAREA'],
text=True, stderr=subprocess.DEVNULL
)
nums = [int(n.strip()) for n in out.split('=')[1].split(',')]
return nums[1] # workarea y = where usable area begins (= panel height)
except Exception:
return 40
def position_window(win, width, height):
"""Position popup just below the panel, horizontally centered on click."""
mx, _ = get_mouse_position()
screen = Gdk.Screen.get_default()
sw, sh = screen.get_width(), screen.get_height()
panel_bottom = get_panel_bottom()
x = mx - width // 2
y = panel_bottom + 2
x = max(4, min(x, sw - width - 4))
if height > 0:
y = min(y, sh - height - 4)
win.move(x, y)
def single_instance(lockfile, pidfile):
"""flock-based single instance. Returns lock_fd on success, exits on collision."""
fd = open(lockfile, 'w')
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
try:
with open(pidfile) as f:
os.kill(int(f.read().strip()), signal.SIGTERM)
except Exception:
pass
sys.exit(0)
with open(pidfile, 'w') as f:
f.write(str(os.getpid()))
return fd
def load_icon(path, size=16):
try:
return GdkPixbuf.Pixbuf.new_from_file_at_size(path, size, size)
except Exception:
return None