diff --git a/src/core/containers/base_container.py b/src/core/containers/base_container.py index 79ab27e..d725c88 100644 --- a/src/core/containers/base_container.py +++ b/src/core/containers/base_container.py @@ -8,6 +8,7 @@ from gi.repository import Gtk # Application imports from .header_container import HeaderContainer from .body_container import BodyContainer +from .footer_container import FooterContainer @@ -39,6 +40,7 @@ class BaseContainer(Gtk.Box): def _load_widgets(self): self.add(HeaderContainer()) self.add(BodyContainer()) + self.add(FooterContainer()) def _update_transparency(self): self.ctx.add_class(f"mw_transparency_{settings.theming.transparency}") diff --git a/src/core/containers/center_container.py b/src/core/containers/center_container.py index ae6858f..2fa8887 100644 --- a/src/core/containers/center_container.py +++ b/src/core/containers/center_container.py @@ -26,6 +26,10 @@ class CenterContainer(Gtk.Box): def _setup_styling(self): self.set_orientation(Gtk.Orientation.VERTICAL) + + self.set_hexpand(True) + self.set_vexpand(True) + ctx = self.get_style_context() ctx.add_class("center-container") diff --git a/src/core/containers/footer_container.py b/src/core/containers/footer_container.py new file mode 100644 index 0000000..4e21cea --- /dev/null +++ b/src/core/containers/footer_container.py @@ -0,0 +1,41 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports + + + +class FooterContainer(Gtk.Box): + def __init__(self): + super(FooterContainer, self).__init__() + + self.ctx = self.get_style_context() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + self.show() + + + def _setup_styling(self): + self.set_orientation(Gtk.Orientation.HORIZONTAL) + + self.set_hexpand(True) + + self.ctx.add_class("footer-container") + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + ... + + + def _load_widgets(self): + ... diff --git a/src/core/containers/header_container.py b/src/core/containers/header_container.py index 6463b1d..7da0a75 100644 --- a/src/core/containers/header_container.py +++ b/src/core/containers/header_container.py @@ -22,11 +22,14 @@ class HeaderContainer(Gtk.Box): self._subscribe_to_events() self._load_widgets() - self.show_all() + self.show() def _setup_styling(self): self.set_orientation(Gtk.Orientation.HORIZONTAL) + + self.set_hexpand(True) + self.ctx.add_class("header-container") def _setup_signals(self): diff --git a/src/core/containers/left_container.py b/src/core/containers/left_container.py index 4b4e564..45e6a37 100644 --- a/src/core/containers/left_container.py +++ b/src/core/containers/left_container.py @@ -23,6 +23,9 @@ class LeftContainer(Gtk.Box): def _setup_styling(self): self.set_orientation(Gtk.Orientation.VERTICAL) + + self.set_vexpand(True) + ctx = self.get_style_context() ctx.add_class("left-container") diff --git a/src/core/containers/right_container.py b/src/core/containers/right_container.py index a8c0138..6e760a7 100644 --- a/src/core/containers/right_container.py +++ b/src/core/containers/right_container.py @@ -24,6 +24,9 @@ class RightContainer(Gtk.Box): def _setup_styling(self): self.set_orientation(Gtk.Orientation.VERTICAL) + + self.set_vexpand(True) + ctx = self.get_style_context() ctx.add_class("right-container") diff --git a/src/core/controllers/base_controller_data.py b/src/core/controllers/base_controller_data.py index 966be21..8b85498 100644 --- a/src/core/controllers/base_controller_data.py +++ b/src/core/controllers/base_controller_data.py @@ -104,4 +104,4 @@ class BaseControllerData: proc = subprocess.Popen(command, stdin = subprocess.PIPE) proc.stdin.write(data.encode(encoding)) proc.stdin.close() - retcode = proc.wait() \ No newline at end of file + retcode = proc.wait() diff --git a/src/core/widgets/clock_widget.py b/src/core/widgets/clock_widget.py new file mode 100644 index 0000000..bda1842 --- /dev/null +++ b/src/core/widgets/clock_widget.py @@ -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() + + + + + diff --git a/src/core/widgets/pager_widget.py b/src/core/widgets/pager_widget.py new file mode 100644 index 0000000..9736ee5 --- /dev/null +++ b/src/core/widgets/pager_widget.py @@ -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() diff --git a/src/core/widgets/task_list_widget.py b/src/core/widgets/task_list_widget.py new file mode 100644 index 0000000..244a092 --- /dev/null +++ b/src/core/widgets/task_list_widget.py @@ -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) diff --git a/src/core/window.py b/src/core/window.py index 684d5a2..8199215 100644 --- a/src/core/window.py +++ b/src/core/window.py @@ -10,6 +10,11 @@ from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GLib +try: + from gi.repository import GdkX11 +except ImportError: + logger.debug("Could not import X11 gir module...") + # Application imports from libs.status_icon import StatusIcon from core.controllers.base_controller import BaseController @@ -31,6 +36,9 @@ class Window(Gtk.ApplicationWindow): self._status_icon = None self._controller = None + self.guake_key = settings_manager.get_guake_key() + self.hidefunc = None + self._setup_styling() self._setup_signals() self._subscribe_to_events() @@ -38,6 +46,7 @@ class Window(Gtk.ApplicationWindow): self._set_window_data() self._set_size_constraints() + self._setup_window_toggle_event() self.show() @@ -45,6 +54,9 @@ class Window(Gtk.ApplicationWindow): def _setup_styling(self): self.set_title(f"{APP_NAME}") self.set_icon_from_file( settings_manager.get_window_icon() ) + self.set_decorated(True) + self.set_skip_pager_hint(False) + self.set_skip_taskbar_hint(False) self.set_gravity(5) # 5 = CENTER self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS @@ -74,6 +86,15 @@ class Window(Gtk.ApplicationWindow): self.add( self._controller.get_base_container() ) + def _display_manager(self): + """ Try to detect which display manager we are running under... """ + + import os + if os.environ.get('WAYLAND_DISPLAY'): + return 'WAYLAND' + + return 'X11' + def _set_size_constraints(self): _window_x = settings.config.main_window_x _window_y = settings.config.main_window_y @@ -118,6 +139,48 @@ class Window(Gtk.ApplicationWindow): def _load_interactive_debug(self): self.set_interactive_debugging(True) + def _setup_window_toggle_event(self) -> None: + hidebound = None + if not self.guake_key or not self._display_manager() == 'X11': + return + + try: + import gi + gi.require_version('Keybinder', '3.0') + from gi.repository import Keybinder + + Keybinder.init() + Keybinder.set_use_cooked_accelerators(False) + except (ImportError, ValueError) as e: + logger.warning(e) + logger.warning('Unable to load Keybinder module. This means the hide_window shortcut will be unavailable') + + return + + # Attempt to grab a global hotkey for hiding the window. + # If we fail, we'll never hide the window, iconifying instead. + try: + hidebound = Keybinder.bind(self.guake_key, self._on_toggle_window, self) + except (KeyError, NameError) as e: + logger.warning(e) + + if not hidebound: + logger.debug('Unable to bind hide_window key, another instance/window has it.') + self.hidefunc = self.iconify + else: + self.hidefunc = self.hide + + def _on_toggle_window(self, data, window): + """Handle a request to hide/show the window""" + if not window.get_property('visible'): + window.show() + # Note: Needed to properly grab widget focus when set_skip_taskbar_hint set to True + window.present() + # NOTE: Need here to enforce sticky after hide and reshow. + window.stick() + else: + self.hidefunc() + def start(self): Gtk.main() diff --git a/src/libs/ipc_server.py b/src/libs/ipc_server.py index ba412d2..eacde83 100644 --- a/src/libs/ipc_server.py +++ b/src/libs/ipc_server.py @@ -16,7 +16,7 @@ class IPCServer(Singleton): """ Create a listener so that other {APP_NAME} instances send requests back to existing instance. """ def __init__(self, ipc_address: str = '127.0.0.1', conn_type: str = "socket"): self.is_ipc_alive = False - self._ipc_port = 4848 + self._ipc_port = 0 # Use 0 to let Listener chose port self._ipc_address = ipc_address self._conn_type = conn_type self._ipc_authkey = b'' + bytes(f'{APP_NAME}-ipc', 'utf-8') diff --git a/src/libs/settings/manager.py b/src/libs/settings/manager.py index 5b77c38..e4e1c87 100644 --- a/src/libs/settings/manager.py +++ b/src/libs/settings/manager.py @@ -91,7 +91,9 @@ class SettingsManager(StartCheckMixin, Singleton): try: with open(self._KEY_BINDINGS_FILE) as file: - bindings = json.load(file)["keybindings"] + bindings = json.load(file)["keybindings"] + self._guake_key = bindings["guake_key"] + keybindings.configure(bindings) except Exception as e: print( f"Settings Manager: {self._KEY_BINDINGS_FILE}\n\t\t{repr(e)}" ) @@ -155,6 +157,7 @@ class SettingsManager(StartCheckMixin, Singleton): def get_window_icon(self) -> str: return self._WINDOW_ICON def get_home_path(self) -> str: return self._USER_HOME def get_starting_files(self) -> list: return self._starting_files + def get_guake_key(self) -> tuple: return self._guake_key def get_starting_args(self): return self.args, self.unknownargs diff --git a/user_config/usr/share/app_name/key-bindings.json b/user_config/usr/share/app_name/key-bindings.json index 8eb8187..9e47bfb 100644 --- a/user_config/usr/share/app_name/key-bindings.json +++ b/user_config/usr/share/app_name/key-bindings.json @@ -1,6 +1,7 @@ { "keybindings": { "help" : "F1", + "guake_key" : "", "rename-files" : ["F2", "e"], "open-terminal" : "F4", "refresh-tab" : ["F5", "r"], diff --git a/user_config/usr/share/app_name/stylesheet.css b/user_config/usr/share/app_name/stylesheet.css index ecac4cd..7d3c240 100644 --- a/user_config/usr/share/app_name/stylesheet.css +++ b/user_config/usr/share/app_name/stylesheet.css @@ -6,6 +6,7 @@ popover > box, .body-container, .center-container, .header-container, +.footer-container, .left-containerm, .right-container { background: rgba(0, 0, 0, 0.0);