-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(widget): implement HomeWidget with dynamic menu and validation s…
…chema This commit introduces the HomeWidget class, which includes a dynamic menu system and a validation schema for its configuration. The widget supports customizable labels and options for power and system menus. It also implements callback functionality for menu interactions.
- Loading branch information
Showing
2 changed files
with
256 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
DEFAULTS = { | ||
'label': '\ue71a', | ||
'container_padding': {'top': 0, 'left': 0, 'bottom': 0, 'right': 0}, | ||
'power_menu': True, | ||
'system_menu': True, | ||
'blur': False, | ||
'callbacks': { | ||
'on_left': 'toggle_menu' | ||
} | ||
} | ||
|
||
VALIDATION_SCHEMA = { | ||
'label': { | ||
'type': 'string', | ||
'default': DEFAULTS['label'] | ||
}, | ||
'menu_list': { | ||
'required': False, | ||
'type': 'list', | ||
'schema': { | ||
'type': 'dict', | ||
'schema': { | ||
'title': {'type': 'string'}, | ||
'path': {'type': 'string'} | ||
} | ||
} | ||
}, | ||
'container_padding': { | ||
'type': 'dict', | ||
'default': DEFAULTS['container_padding'], | ||
'required': False | ||
}, | ||
'power_menu': { | ||
'type': 'boolean', | ||
'default': DEFAULTS['power_menu'], | ||
'required': False | ||
}, | ||
'system_menu': { | ||
'type': 'boolean', | ||
'default': DEFAULTS['system_menu'], | ||
'required': False | ||
}, | ||
'blur': { | ||
'type': 'boolean', | ||
'default': DEFAULTS['blur'], | ||
'required': False | ||
}, | ||
'callbacks': { | ||
'required': False, | ||
'type': 'dict', | ||
'schema': { | ||
'on_left': { | ||
'type': 'string', | ||
'default': DEFAULTS['callbacks']['on_left'] | ||
} | ||
}, | ||
'default': DEFAULTS['callbacks'] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
""" | ||
This widget need to check, there is some bug with click event, sometimes need to click twice to trigger the event. | ||
""" | ||
import logging | ||
import os | ||
import re | ||
from core.widgets.base import BaseWidget | ||
from core.validation.widgets.yasb.home import VALIDATION_SCHEMA | ||
from PyQt6.QtWidgets import QLabel, QHBoxLayout, QWidget, QMenu, QWidgetAction | ||
from PyQt6.QtCore import Qt, QPoint, QTimer | ||
from core.utils.win32.blurWindow import Blur | ||
from core.utils.utilities import is_windows_10 | ||
import os | ||
from core.utils.widgets.power import PowerOperations | ||
|
||
class HomeWidget(BaseWidget): | ||
validation_schema = VALIDATION_SCHEMA | ||
def __init__( | ||
self,label: str, | ||
container_padding: dict, | ||
power_menu: bool, | ||
system_menu: bool, | ||
blur: bool, | ||
callbacks: dict[str, str], | ||
menu_list: list[str, dict[str]] = None | ||
): | ||
super().__init__(class_name="home-widget") | ||
self.power_operations = PowerOperations() | ||
self._label = label | ||
self._menu_list = menu_list | ||
self._padding = container_padding | ||
self._power_menu = power_menu | ||
self._system_menu = system_menu | ||
self._blur = blur | ||
# Construct container | ||
self._widget_container_layout: QHBoxLayout = QHBoxLayout() | ||
self._widget_container_layout.setSpacing(0) | ||
self._widget_container_layout.setContentsMargins( | ||
self._padding['left'], | ||
self._padding['top'], | ||
self._padding['right'], | ||
self._padding['bottom'] | ||
) | ||
# Initialize container | ||
self._widget_container: QWidget = QWidget() | ||
self._widget_container.setLayout(self._widget_container_layout) | ||
self._widget_container.setProperty("class", "widget-container") | ||
# Add the container to the main widget layout | ||
self.widget_layout.addWidget(self._widget_container) | ||
self._create_dynamically_label(self._label) | ||
|
||
self.register_callback("toggle_menu", self._toggle_menu) | ||
self.callback_left = callbacks["on_left"] | ||
|
||
self._create_menu() | ||
self.is_menu_visible = False | ||
|
||
def _create_dynamically_label(self, content: str): | ||
def process_content(content): | ||
label_parts = re.split('(<span.*?>.*?</span>)', content) | ||
label_parts = [part for part in label_parts if part] | ||
widgets = [] | ||
for part in label_parts: | ||
part = part.strip() # Remove any leading/trailing whitespace | ||
if not part: | ||
continue | ||
if '<span' in part and '</span>' in part: | ||
class_name = re.search(r'class=(["\'])([^"\']+?)\1', part) | ||
class_result = class_name.group(2) if class_name else 'icon' | ||
icon = re.sub(r'<span.*?>|</span>', '', part).strip() | ||
label = QLabel(icon) | ||
label.setProperty("class", class_result) | ||
else: | ||
label = QLabel(part) | ||
label.setProperty("class", "label") | ||
label.setAlignment(Qt.AlignmentFlag.AlignCenter) | ||
self._widget_container_layout.addWidget(label) | ||
widgets.append(label) | ||
label.show() | ||
label.setCursor(Qt.CursorShape.PointingHandCursor) | ||
return widgets | ||
self._widgets = process_content(content) | ||
|
||
def add_menu_action(self, menu, text, triggered_func): | ||
label = QLabel(text) | ||
label.setProperty('class', 'menu-item') | ||
label.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft) | ||
label.setCursor(Qt.CursorShape.PointingHandCursor) | ||
|
||
widget_action = QWidgetAction(menu) | ||
widget_action.setDefaultWidget(label) | ||
widget_action.triggered.connect(triggered_func) | ||
menu.addAction(widget_action) | ||
|
||
return widget_action | ||
|
||
def create_menu_action(self, path): | ||
expanded_path = os.path.expanduser(path) | ||
return lambda: os.startfile(expanded_path) | ||
|
||
def _create_menu(self): | ||
self._menu = QMenu(self) | ||
self._update_menu_style() | ||
self._setup_menu() | ||
|
||
def _update_menu_style(self): | ||
self._menu.setProperty('class', 'home-menu') | ||
self._menu.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) | ||
if self._blur: | ||
Blur( | ||
self._menu.winId(), | ||
Acrylic=True if is_windows_10() else False, | ||
DarkMode=True, | ||
RoundCorners=True, | ||
BorderColor="System" | ||
) | ||
|
||
def _setup_menu(self): | ||
if self._system_menu: | ||
self.add_menu_action( | ||
self._menu, | ||
"About this PC", | ||
lambda: os.system("winver") | ||
) | ||
self._menu.addSeparator() | ||
self.add_menu_action( | ||
self._menu, | ||
"System Settings", | ||
lambda: os.startfile("ms-settings:") | ||
) | ||
self.add_menu_action( | ||
self._menu, | ||
"Task Manager", | ||
lambda: os.system("taskmgr") | ||
) | ||
self._menu.addSeparator() | ||
|
||
if self._menu_list is not None: | ||
if isinstance(self._menu_list, list): | ||
for menu_item in self._menu_list: | ||
if 'title' in menu_item and 'path' in menu_item: | ||
action = self.create_menu_action(menu_item['path']) | ||
self.add_menu_action( | ||
self._menu, | ||
menu_item['title'], | ||
action | ||
) | ||
else: | ||
logging.error(f"Expected menu_list to be a list but got {type(self._menu_list)}") | ||
return | ||
|
||
if self._menu_list is not None and len(self._menu_list) > 0 and self._power_menu: | ||
self._menu.addSeparator() | ||
|
||
if self._power_menu: | ||
self.add_menu_action( | ||
self._menu, | ||
"Sleep", | ||
lambda: self.power_operations.sleep() | ||
) | ||
self.add_menu_action( | ||
self._menu, | ||
"Restart", | ||
lambda: self.power_operations.restart() | ||
) | ||
self.add_menu_action( | ||
self._menu, | ||
"Shut Down", | ||
lambda: self.power_operations.shutdown() | ||
) | ||
self._menu.addSeparator() | ||
self.add_menu_action( | ||
self._menu, | ||
"Lock Screen", | ||
lambda: self.power_operations.lock() | ||
) | ||
self.add_menu_action( | ||
self._menu, | ||
"Logout", | ||
lambda: self.power_operations.signout() | ||
) | ||
self._menu.triggered.connect(self.on_menu_triggered) | ||
|
||
def on_menu_triggered(self): | ||
self._menu.hide() | ||
self.is_menu_visible = False | ||
|
||
def _toggle_menu(self): | ||
if self.is_menu_visible: | ||
self._menu.hide() | ||
self.is_menu_visible = False | ||
return | ||
global_position = self.mapToGlobal(QPoint(0, self.height() + 6)) | ||
self._menu.move(global_position) | ||
self._update_menu_style() | ||
self._menu.show() | ||
self.is_menu_visible = True |