Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Preview komorebi workspace #115

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ bars:
# ^ Specifies if the workspace and monitor index values should be zero-indexed. Accepts: boolean
# hide_empty_workspaces: False
# ^ Specifies if empty workspaces should be shown in the list of komorebi workspaces. Accepts: boolean
# preview_workspace: False
# ^ Specifies mouse over behavior on komorebi workspaces. If true, you can see workspace by mouse over and get back by leaving the button area. Accepts: boolean


# komorebi_active_layout:
# type: "komorebi.active_layout.ActiveLayoutWidget"
Expand Down Expand Up @@ -420,6 +423,7 @@ widgets:
label_default_name: "{index}"
label_zero_index: false
hide_empty_workspaces: false
preview_workspace: false

komorebi_active_layout:
type: "komorebi.active_layout.ActiveLayoutWidget"
Expand Down
3 changes: 3 additions & 0 deletions src/core/event_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ def __contains__(cls, item):
class Event(Enum, metaclass=MetaEvent):
pass

class WorkspaceButtonEvent(Event):
HoverEnter = "HoverEnter"
HoverLeave = "HoverLeave"

class KomorebiEvent(Event):
KomorebiConnect = "KomorebiConnect"
Expand Down
37 changes: 32 additions & 5 deletions src/core/utils/komorebi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ class KomorebiClient:
def __init__(
self,
komorebic_path: str = "komorebic.exe",
timeout_secs: int = 0.5
timeout_secs: float = 0.5
):
super().__init__()
self._timeout_secs = timeout_secs
self._komorebic_path = komorebic_path
self._previous_poll_offline = False
self._previous_mouse_follows_focus = False

def query_state(self) -> Optional[dict]:
with suppress(json.JSONDecodeError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
Expand Down Expand Up @@ -81,8 +82,31 @@ def get_workspace_by_window_hwnd(self, workspaces: list[Optional[dict]], window_
if managed_window['hwnd'] == window_hwnd:
return add_index(workspace, i)

def activate_workspace(self, ws_idx: int) -> None:
subprocess.Popen([self._komorebic_path, "focus-workspace", str(ws_idx)], shell=True)
def get_mouse_follows_focus(self, state: dict) -> bool:
return state['mouse_follows_focus']

def activate_workspace(self, ws_idx: int, wait: bool = False) -> None:
p = subprocess.Popen([self._komorebic_path, "focus-workspace", str(ws_idx)], shell=True)

if wait:
p.wait()

def hide_preview(self, ws_idx: int, stay_on_workspace: bool = False) -> None:
if self._previous_mouse_follows_focus:
self._previous_mouse_follows_focus = False
self.toggle("mouse-follows-focus", True)

if not stay_on_workspace:
self.activate_workspace(ws_idx, True)

def preview_workspace(self, ws_idx: int, state: dict) -> None:
is_mff_active = self.get_mouse_follows_focus(state)

if is_mff_active and not self._previous_mouse_follows_focus:
self._previous_mouse_follows_focus = True
self.toggle("mouse-follows-focus", True)

self.activate_workspace(ws_idx, True)

def next_workspace(self) -> None:
try:
Expand Down Expand Up @@ -119,9 +143,12 @@ def flip_layout(self) -> None:
except subprocess.SubprocessError:
pass

def toggle(self, toggle_type: str):
def toggle(self, toggle_type: str, wait: bool = False) -> None:
try:
subprocess.Popen([self._komorebic_path, f"toggle-{toggle_type}"], shell=True)
p = subprocess.Popen([self._komorebic_path, f"toggle-{toggle_type}"], shell=True)

if wait:
p.wait()
except subprocess.SubprocessError:
logging.exception(f"Failed to toggle {toggle_type} for currently active workspace")

Expand Down
9 changes: 7 additions & 2 deletions src/core/validation/widgets/komorebi/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
'label_workspace_btn': '{index}',
'label_default_name': '',
'label_zero_index': False,
'hide_empty_workspaces': False
'hide_empty_workspaces': False,
'preview_workspace': False
}

VALIDATION_SCHEMA = {
Expand All @@ -26,5 +27,9 @@
'hide_empty_workspaces': {
'type': 'boolean',
'default': DEFAULTS['hide_empty_workspaces']
}
},
'preview_workspace': {
'type': 'boolean',
'default': DEFAULTS['preview_workspace']
},
}
93 changes: 88 additions & 5 deletions src/core/widgets/komorebi/workspaces.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import logging
from PyQt6.QtWidgets import QPushButton, QWidget, QHBoxLayout, QLabel
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtCore import QObject, pyqtSignal, QTimer, pyqtSlot
from typing import Literal
from contextlib import suppress
from core.utils.win32.utilities import get_monitor_hwnd
from core.event_service import EventService
from core.event_enums import KomorebiEvent
from core.event_enums import KomorebiEvent, WorkspaceButtonEvent
from core.widgets.base import BaseWidget
from core.utils.komorebi.client import KomorebiClient
from core.validation.widgets.komorebi.workspaces import VALIDATION_SCHEMA
Expand All @@ -24,16 +24,26 @@

class WorkspaceButton(QPushButton):

def __init__(self, workspace_index: int, label: str = None):
def __init__(self, workspace_index: int, label: str | None = None, preview_workspace: bool = False):
super().__init__()
self._preview_workspace = preview_workspace
self.komorebic = KomorebiClient()

self.hover_enter_debounce = QTimer()
self.hover_enter_debounce.setInterval(500)
self.hover_enter_debounce.setSingleShot(True)
self.hover_enter_debounce.timeout.connect(self.sendHoverEnter)

self.event_service = EventService()
self.workspace_index = workspace_index
self.status = WORKSPACE_STATUS_EMPTY
self.setProperty("class", "ws-btn")
self.setText(label if label else str(workspace_index + 1))
self.clicked.connect(self.activate_workspace)
self.hide()

self.isPressed = False

def update_and_redraw(self, status: WorkspaceStatus):
self.status = status
self.setProperty("class", f"ws-btn {status.lower()}")
Expand All @@ -42,14 +52,54 @@ def update_and_redraw(self, status: WorkspaceStatus):
def activate_workspace(self):
try:
self.komorebic.activate_workspace(self.workspace_index)
self.isPressed = True
except Exception:
logging.exception(f"Failed to focus workspace at index {self.workspace_index}")

def sendHoverEnter(self):
self.event_service.emit_event(
WorkspaceButtonEvent.HoverEnter,
self.workspace_index,
self.parent().parent().parent()
)

def sendHoverLeave(self):
self.event_service.emit_event(
WorkspaceButtonEvent.HoverLeave,
self.workspace_index,
self.parent().parent().parent(),
self.isPressed
)

def enterEvent(self, event) -> None:
if not self._preview_workspace:
return super().enterEvent(event)

if self.status is not WORKSPACE_STATUS_ACTIVE:
self.hover_enter_debounce.start()

return super().enterEvent(event)

def leaveEvent(self, a0) -> None:
if not self._preview_workspace:
return super().leaveEvent(a0)

self.hover_enter_debounce.stop()
self.sendHoverLeave()

if self.isPressed:
self.isPressed = False

return super().leaveEvent(a0)



class WorkspaceWidget(BaseWidget):
k_signal_connect = pyqtSignal(dict)
k_signal_update = pyqtSignal(dict, dict)
k_signal_disconnect = pyqtSignal()
b_signal_hover_enter = pyqtSignal(int, QObject)
b_signal_hover_leave = pyqtSignal(int, QObject, bool)

validation_schema = VALIDATION_SCHEMA
event_listener = KomorebiEventListener
Expand All @@ -60,8 +110,10 @@ def __init__(
label_workspace_btn: str,
label_default_name: str,
label_zero_index: bool,
hide_empty_workspaces: bool
hide_empty_workspaces: bool,
preview_workspace: bool
):

super().__init__(class_name="komorebi-workspaces")

self._event_service = EventService()
Expand All @@ -75,6 +127,10 @@ def __init__(
self._curr_workspace_index = None
self._workspace_buttons: list[WorkspaceButton] = []
self._hide_empty_workspaces = hide_empty_workspaces

self._preview_workspace = preview_workspace
self._prev_workspace_on_mouse_index = None
self._workspace_on_mouse_index = None

self._workspace_focus_events = [
KomorebiEvent.CycleFocusWorkspace.value,
Expand Down Expand Up @@ -123,17 +179,24 @@ def _register_signals_and_events(self):
self.k_signal_connect.connect(self._on_komorebi_connect_event)
self.k_signal_update.connect(self._on_komorebi_update_event)
self.k_signal_disconnect.connect(self._on_komorebi_disconnect_event)
self.b_signal_hover_enter.connect(self._show_workspace_on_mouse)
self.b_signal_hover_leave.connect(self._hide_workspace_on_mouse)

self._event_service.register_event(KomorebiEvent.KomorebiConnect, self.k_signal_connect)
self._event_service.register_event(KomorebiEvent.KomorebiDisconnect, self.k_signal_disconnect)
self._event_service.register_event(KomorebiEvent.KomorebiUpdate, self.k_signal_update)

self._event_service.register_event(WorkspaceButtonEvent.HoverEnter, self.b_signal_hover_enter)
self._event_service.register_event(WorkspaceButtonEvent.HoverLeave, self.b_signal_hover_leave)

def _reset(self):
self._komorebi_state = None
self._komorebi_screen = None
self._komorebi_workspaces = []
self._curr_workspace_index = None
self._prev_workspace_index = None
self._prev_workspace_on_mouse_index = None
self._workspace_on_mouse_index = None
self._workspace_buttons = []
self._clear_container_layout()

Expand Down Expand Up @@ -264,7 +327,7 @@ def _try_add_workspace_button(self, workspace_index: int) -> WorkspaceButton:

if workspace_index not in workspace_button_indexes:
ws_label = self._get_workspace_label(workspace_index)
workspace_btn = WorkspaceButton(workspace_index, ws_label)
workspace_btn = WorkspaceButton(workspace_index, ws_label, self._preview_workspace)

self._update_button(workspace_btn)
self._workspace_buttons.append(workspace_btn)
Expand All @@ -283,3 +346,23 @@ def _show_offline_status(self):
def _hide_offline_status(self):
self._offline_text.hide()
self._workspace_container.show()

@pyqtSlot(int, QObject)
def _show_workspace_on_mouse(self, ws_idx: int, parent) -> None:
if id(self) != id(parent):
return

self._workspace_on_mouse_index = ws_idx
self._prev_workspace_on_mouse_index = self._curr_workspace_index
self._komorebic.preview_workspace(self._workspace_on_mouse_index, self._komorebi_state)


@pyqtSlot(int, QObject, bool)
def _hide_workspace_on_mouse(self, ws_idx: int, parent, is_pressed: bool) -> None:
if id(self) != id(parent):
return

if self._workspace_on_mouse_index is not None:
self._komorebic.hide_preview(self._prev_workspace_on_mouse_index, is_pressed)
self._prev_workspace_on_mouse_index = None
self._workspace_on_mouse_index = None