Major refactor to move away from glade file

This commit is contained in:
2025-11-29 23:16:01 -06:00
parent 08587c16b8
commit d2d266adda
83 changed files with 3091 additions and 1434 deletions

View File

@@ -0,0 +1,3 @@
"""
Widgets Package
"""

View File

@@ -0,0 +1,129 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from xdg.DesktopEntry import DesktopEntry
# Application imports
from libs.desktop_parsing.app_finder import find_apps
class CategoryListWidget(Gtk.ButtonBox):
def __init__(self):
super(CategoryListWidget, self).__init__()
self.ctx = self.get_style_context()
self.active_category: str = 'Accessories'
self.category_dict: {} = {
"Accessories": [],
"Multimedia": [],
"Graphics": [],
"Game": [],
"Office": [],
"Development": [],
"Internet": [],
"Settings": [],
"System": [],
"Wine": [],
"Other": []
}
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
self.fill_menu_objects()
self.show_all()
def _setup_styling(self):
self.set_orientation(Gtk.Orientation.VERTICAL)
self.ctx.add_class("category-list-widget")
def _setup_signals(self):
event_system.subscribe("refresh-active-category", self.refresh_active_category)
def _subscribe_to_events(self):
...
def _load_widgets(self):
for category in self.category_dict.keys():
button = Gtk.Button(label = category)
button.connect("clicked", self.set_active_category)
button.show()
self.add(button)
def set_active_category(self, button):
self.active_category = button.get_label()
event_system.emit(
"load-active-category",
(self.category_dict[ self.active_category ],)
)
def fill_menu_objects(self, apps: [] = []):
if not apps:
apps = find_apps()
for app in apps:
fPath = app.get_filename()
xdgObj = DesktopEntry( fPath )
title = xdgObj.getName()
groups = xdgObj.getCategories()
comment = xdgObj.getComment()
icon = xdgObj.getIcon()
mainExec = xdgObj.getExec()
tryExec = xdgObj.getTryExec()
group = ""
if "Accessories" in groups or "Utility" in groups:
group = "Accessories"
elif "Multimedia" in groups or "Video" in groups or "Audio" in groups:
group = "Multimedia"
elif "Development" in groups:
group = "Development"
elif "Game" in groups:
group = "Game"
elif "Internet" in groups or "Network" in groups:
group = "Internet"
elif "Graphics" in groups:
group = "Graphics"
elif "Office" in groups:
group = "Office"
elif "System" in groups:
group = "System"
elif "Settings" in groups:
group = "Settings"
elif "Wine" in groups:
group = "Wine"
else:
group = "Other"
self.category_dict[group].append(
{
"title": title,
"groups": groups,
"comment": comment,
"exec": mainExec,
"tryExec": tryExec,
"fileName": fPath.split("/")[-1],
"filePath": fPath,
"icon": icon
}
)
def refresh_active_category(self):
event_system.emit(
"load-active-category",
(self.category_dict[ self.active_category ],)
)

View File

@@ -0,0 +1,125 @@
# Python imports
import os
import subprocess
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Gtk
from gi.repository import Gio
from gi.repository import GdkPixbuf
# Application imports
class CategoryWidget(Gtk.Grid):
def __init__(self):
super(CategoryWidget, self).__init__()
self.ctx = self.get_style_context()
self.column_count = 4
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
self.show()
def _setup_styling(self):
self.set_hexpand(True)
self.set_vexpand(True)
self.set_row_spacing(10)
self.set_column_spacing(10)
self.set_row_homogeneous(True)
self.set_column_homogeneous(True)
self.set_orientation(Gtk.Orientation.HORIZONTAL)
self.ctx.add_class("category-widget")
def _setup_signals(self):
event_system.subscribe("load-active-category", self.load_active_category)
def _subscribe_to_events(self):
...
def _load_widgets(self):
event_system.emit("refresh-active-category")
def load_active_category(self, app_list: [] = []):
self.clear_children()
row = 0
col = 0
icon_theme = Gtk.IconTheme.get_default()
for app in app_list:
button = self.generate_app_button(icon_theme, app)
self.attach(button, col, row, 1, 1)
col += 1
if col == self.column_count:
col = 0
row += 1
def generate_app_button(self, icon_theme, app: {} = {}):
if not app: return
title = app["title"]
exec_str = app[
"exec" if not app["exec"] in ("", None) else "tryExec"
]
button = Gtk.Button(label = title)
button.sig_id = button.connect("clicked", self._do_exec, exec_str)
icon_pth = app["icon"]
icon = self.get_icon_from_path(icon_pth) \
if os.path.exists(icon_pth) \
else \
self.get_icon_from_gio(icon_theme, icon_pth)
button.set_image(icon)
button.set_always_show_image(True)
button.show_all()
return button
def get_icon_from_path(self, path: str):
pixbuf = GdkPixbuf.PixbufAnimation.new_from_file(path) \
.get_static_image() \
.scale_simple(32, 32, \
GdkPixbuf.InterpType.BILINEAR)
return Gtk.Image.new_from_pixbuf(pixbuf)
def get_icon_from_gio(self, icon_theme, icon_name: str):
gio_icon = Gio.Icon.new_for_string(icon_name)
pixbuf = None
# Note: https://docs.gtk.org/gtk3/enum.IconSize.html
for i in [6, 5, 3, 4, 2, 1]:
icon_info = Gtk.IconTheme.lookup_by_gicon(icon_theme, gio_icon, i, Gtk.IconLookupFlags.FORCE_REGULAR)
if not icon_info: continue
pixbuf = icon_info.load_icon().scale_simple(32, 32, 2) # 2 = BILINEAR and is best by default
break
return Gtk.Image.new_from_pixbuf( pixbuf )
def _do_exec(self, widget, _command):
command = _command.split("%")[0]
subprocess.Popen(command.split(), start_new_session=True, stdout=None, stderr=None)
def clear_children(self, app_list: [] = []):
children = self.get_children()
for child in children:
child.disconnect(child.sig_id)
self.remove(child)
child.run_dispose()

View File

@@ -0,0 +1,110 @@
# Python imports
from datetime import datetime
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GObject
# Application imports
class CalendarWidget(Gtk.Popover):
def __init__(self):
super(CalendarWidget, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
...
def _setup_signals(self):
...
def _subscribe_to_events(self):
...
def _load_widgets(self):
self.body = Gtk.Calendar()
self.body.show()
self.add(self.body)
class ClockWidget(Gtk.EventBox):
def __init__(self):
super(ClockWidget, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
self.show_all()
def _setup_styling(self):
self.set_size_request(180, -1)
ctx = self.get_style_context()
ctx.add_class("clock-widget")
def _setup_signals(self):
self.connect("button_release_event", self._toggle_cal_popover)
def _subscribe_to_events(self):
...
def _load_widgets(self):
self.calendar = CalendarWidget()
self.label = Gtk.Label()
self.calendar.set_relative_to(self)
self.label.set_justify(Gtk.Justification.CENTER)
self.label.set_margin_top(15)
self.label.set_margin_bottom(15)
self.label.set_margin_left(15)
self.label.set_margin_right(15)
self._update_face()
self.add(self.label)
GObject.timeout_add(59000, self._update_face)
def _update_face(self):
dt_now = datetime.now()
hours_mins_sec = dt_now.strftime("%I:%M %p")
month_day_year = dt_now.strftime("%m/%d/%Y")
time_str = hours_mins_sec + "\n" + month_day_year
self.label.set_label(time_str)
def _toggle_cal_popover(self, widget, eve):
if (self.calendar.get_visible() == True):
self.calendar.popdown()
return
now = datetime.now()
timeStr = now.strftime("%m/%d/%Y")
parts = timeStr.split("/")
month = int(parts[0]) - 1
day = int(parts[1])
year = int(parts[2])
self.calendar.body.select_day(day)
self.calendar.body.select_month(month, year)
self.calendar.popup()

View File

@@ -0,0 +1,3 @@
"""
Widgets.Controls Package
"""

View File

@@ -0,0 +1,83 @@
# Python imports
from contextlib import suppress
import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gio
# Application imports
class OpenFilesButton(Gtk.Button):
"""docstring for OpenFilesButton."""
def __init__(self):
super(OpenFilesButton, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
self.set_label("Open File(s)...")
self.set_image( Gtk.Image.new_from_icon_name("gtk-open", 4) )
self.set_always_show_image(True)
self.set_image_position(1) # Left - 0, Right = 1
self.set_hexpand(False)
def _setup_signals(self):
self.connect("button-release-event", self._open_files)
def _subscribe_to_events(self):
event_system.subscribe("open_files", self._open_files)
def _load_widgets(self):
...
def _open_files(self, widget = None, eve = None, gfile = None):
start_dir = None
_gfiles = []
if gfile and gfile.query_exists():
start_dir = gfile.get_parent()
chooser = Gtk.FileChooserDialog("Open File(s)...", None,
Gtk.FileChooserAction.OPEN,
(
Gtk.STOCK_CANCEL,
Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN,
Gtk.ResponseType.OK
)
)
chooser.set_select_multiple(True)
with suppress(Exception):
folder = widget.get_current_file().get_parent() if not start_dir else start_dir
chooser.set_current_folder( folder.get_path() )
response = chooser.run()
if not response == Gtk.ResponseType.OK:
chooser.destroy()
return _gfiles
filenames = chooser.get_filenames()
if not filenames:
chooser.destroy()
return _gfiles
for file in filenames:
path = file if os.path.isabs(file) else os.path.abspath(file)
_gfiles.append( Gio.File.new_for_path(path) )
chooser.destroy()
logger.debug(_gfiles)
return _gfiles

View File

@@ -0,0 +1,72 @@
# Python imports
import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gio
# Application imports
class SaveAsButton(Gtk.Button):
def __init__(self):
super(SaveAsButton, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
self.set_label("Save As")
self.set_image( Gtk.Image.new_from_icon_name("gtk-save-as", 4) )
self.set_always_show_image(True)
self.set_image_position(1) # Left - 0, Right = 1
self.set_hexpand(False)
def _setup_signals(self):
self.connect("released", self._save_as)
def _subscribe_to_events(self):
event_system.subscribe("save-as", self._save_as)
def _load_widgets(self):
...
def _save_as(self, widget = None, eve = None, gfile = None):
start_dir = None
_gfile = None
chooser = Gtk.FileChooserDialog("Save File As...", None,
Gtk.FileChooserAction.SAVE,
(
Gtk.STOCK_CANCEL,
Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE_AS,
Gtk.ResponseType.OK
)
)
# chooser.set_select_multiple(False)
response = chooser.run()
if not response == Gtk.ResponseType.OK:
chooser.destroy()
return _gfile
file = chooser.get_filename()
if not file:
chooser.destroy()
return _gfile
path = file if os.path.isabs(file) else os.path.abspath(file)
_gfile = Gio.File.new_for_path(path)
chooser.destroy()
logger.debug(f"File To Save As: {_gfile}")
return _gfile

View File

@@ -0,0 +1,48 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class TransparencyScale(Gtk.Scale):
def __init__(self):
super(TransparencyScale, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
self.show_all()
def _setup_styling(self):
self.set_digits(0)
self.set_value_pos(Gtk.PositionType.RIGHT)
self.add_mark(50.0, Gtk.PositionType.TOP, "50%")
self.set_hexpand(True)
def _setup_signals(self):
self.connect("value-changed", self._update_transparency)
def _subscribe_to_events(self):
...
def _load_widgets(self):
adjust = self.get_adjustment()
adjust.set_lower(0)
adjust.set_upper(100)
adjust.set_value(settings.theming.transparency)
adjust.set_step_increment(1.0)
def _update_transparency(self, range):
event_system.emit("remove-transparency")
tp = int(range.get_value())
settings.theming.transparency = tp
event_system.emit("update-transparency")

View File

@@ -0,0 +1,35 @@
# Python imports
# Lib imports
import gi
gi.require_version('Wnck', '3.0')
from gi.repository import Wnck
# Application imports
class PagerWidget:
def __init__(self):
super(PagerWidget, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
...
def _setup_signals(self):
...
def _subscribe_to_events(self):
...
def _load_widgets(self):
...
def get_widget(self):
return Wnck.Pager.new()

View File

@@ -0,0 +1,62 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from ..widgets.category_widget import CategoryWidget
from ..widgets.vte_widget import VteWidget
class TabWidget(Gtk.Notebook):
def __init__(self):
super(TabWidget, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
self.show()
def _setup_styling(self):
self.set_hexpand(True)
self.set_vexpand(True)
ctx = self.get_style_context()
ctx.add_class("tab-widget")
def _setup_signals(self):
...
def _subscribe_to_events(self):
event_system.subscribe("focus-apps", self.focus_apps)
event_system.subscribe("focus-terminal", self.focus_terminal)
def _load_widgets(self):
scroll_view = Gtk.ScrolledWindow()
viewport = Gtk.Viewport()
viewport.add(CategoryWidget())
scroll_view.add(viewport)
scroll_view.show_all()
self.insert_page(scroll_view, Gtk.Label(label = "Apps"), 0)
self.insert_page(VteWidget(), Gtk.Label(label = "Terminal"), 1)
self.set_current_page(0)
def focus_apps(self):
widget = self.get_nth_page(0).get_children()[0].get_children()[0]
self.set_current_page(0)
widget.grab_focus()
def focus_terminal(self):
widget = self.get_nth_page(1)
self.set_current_page(1)
widget.grab_focus()

View File

@@ -0,0 +1,54 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Wnck', '3.0')
from gi.repository import Gtk
from gi.repository import Wnck
# Application imports
class TaskListWidget(Gtk.ScrolledWindow):
def __init__(self):
super(TaskListWidget, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
self.show_all()
def _setup_styling(self):
self.set_hexpand(False)
self.set_size_request(180, -1)
def _setup_signals(self):
...
def _subscribe_to_events(self):
...
def _load_widgets(self):
viewport = Gtk.Viewport()
task_list = Wnck.Tasklist.new()
vbox = Gtk.Box()
vbox.set_orientation(Gtk.Orientation.VERTICAL)
task_list.set_scroll_enabled(False)
task_list.set_button_relief(2) # 0 = normal relief, 2 = no relief
task_list.set_grouping(1) # 0 = mever group, 1 auto group, 2 = always group
task_list.set_vexpand(True)
task_list.set_include_all_workspaces(False)
task_list.set_orientation(1) # 0 = horizontal, 1 = vertical
vbox.add(task_list)
viewport.add(vbox)
self.add(viewport)

View File

@@ -0,0 +1,125 @@
# Python imports
import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Vte', '2.91')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import Vte
# Application imports
from libs.dto.event import Event
class VteWidgetException(Exception):
...
class VteWidget(Vte.Terminal):
"""
https://stackoverflow.com/questions/60454326/how-to-implement-a-linux-terminal-in-a-pygtk-app-like-vscode-and-pycharm-has
"""
def __init__(self):
super(VteWidget, self).__init__()
self.cd_cmd_prefix = ("cd".encode(), "cd ".encode())
self.dont_process = False
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
self._do_session_spawn()
self.show()
def _setup_styling(self):
ctx = self.get_style_context()
ctx.add_class("vte-widget")
self.set_clear_background(False)
self.set_enable_sixel(True)
self.set_cursor_shape( Vte.CursorShape.IBEAM )
def _setup_signals(self):
self.connect("commit", self._commit)
def _subscribe_to_events(self):
event_system.subscribe("update_term_path", self.update_term_path)
def _load_widgets(self):
...
def _do_session_spawn(self):
self.spawn_sync(
Vte.PtyFlags.DEFAULT,
settings_manager.get_home_path(),
["/bin/bash"],
[],
GLib.SpawnFlags.DEFAULT,
None, None,
)
# Note: '-->:' is used as a delimiter to split on to get command actual.
# !!! DO NOT REMOVE UNLESS CODE UPDATED ACCORDINGLY !!!
startup_cmds = [
"env -i /bin/bash --noprofile --norc\n",
"export TERM='xterm-256color'\n",
"export LC_ALL=C\n",
"export XDG_RUNTIME_DIR='/run/user/1000'\n",
"export DISPLAY=:0\n",
f"export XAUTHORITY='{settings_manager.get_home_path()}/.Xauthority'\n",
f"\nexport HOME='{settings_manager.get_home_path()}'\n",
"export PS1='\\h@\\u \\W -->: '\n",
"clear\n"
]
for i in startup_cmds:
self.run_command(i)
def _commit(self, terminal, text, size):
if self.dont_process:
self.dont_process = False
return
if not text.encode() == "\r".encode(): return
text, attributes = self.get_text()
lines = text.strip().splitlines()
command_ran = None
try:
command_ran = lines[-1].split("-->:")[1].strip()
except VteWidgetException as e:
logger.debug(e)
return
if not command_ran[0:3].encode() in self.cd_cmd_prefix:
return
target_path = command_ran.split( command_ran[0:3] )[1]
if target_path in (".", "./"): return
if not target_path:
target_path = settings_manager.get_home_path()
event = Event("pty_path_updated", "", target_path)
event_system.emit("handle_bridge_event", (event,))
def update_term_path(self, fpath: str):
self.dont_process = True
cmds = [f"cd '{fpath}'\n", "clear\n"]
for cmd in cmds:
self.run_command(cmd)
def run_command(self, cmd: str):
self.feed_child_binary(bytes(cmd, 'utf8'))