Skip to content

Commit

Permalink
feat(widget): implement HomeWidget with dynamic menu and validation s…
Browse files Browse the repository at this point in the history
…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
amnweb committed Nov 20, 2024
1 parent 84b4ad7 commit 9064e35
Show file tree
Hide file tree
Showing 2 changed files with 256 additions and 0 deletions.
59 changes: 59 additions & 0 deletions src/core/validation/widgets/yasb/home.py
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']
}
}
197 changes: 197 additions & 0 deletions src/core/widgets/yasb/home.py
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

0 comments on commit 9064e35

Please sign in to comment.