From 3ddbc4ff5e16ca714dd1a16d273db1b4c2aa765d Mon Sep 17 00:00:00 2001
From: amnweb <16545063+forumwt@users.noreply.github.com>
Date: Wed, 15 Jan 2025 22:43:23 +0100
Subject: [PATCH] feat(widget/github): enhance menu and icon configuration
options
Updated the widget's menu and icon settings to allow for more customization. The menu now supports properties like blur, round corners, and alignment, while icons for issues, pull requests, releases, discussions, and the GitHub logo have been added for better visual representation.
---
src/core/validation/widgets/yasb/github.py | 96 ++++++-
src/core/widgets/yasb/github.py | 288 ++++++++++++---------
2 files changed, 244 insertions(+), 140 deletions(-)
diff --git a/src/core/validation/widgets/yasb/github.py b/src/core/validation/widgets/yasb/github.py
index fdb9245..a2aa822 100644
--- a/src/core/validation/widgets/yasb/github.py
+++ b/src/core/validation/widgets/yasb/github.py
@@ -4,12 +4,26 @@
'update_interval': 600,
'token': "",
'tooltip': True,
- 'max_notification':20,
+ 'max_notification':30,
'only_unread': False,
'max_field_size': 100,
- 'menu_width': 400,
- 'menu_height': 400,
- 'menu_offset': 240,
+ 'menu': {
+ 'blur': True,
+ 'round_corners': True,
+ 'round_corners_type': 'normal',
+ 'border_color': 'System',
+ 'alignment': 'right',
+ 'direction': 'down',
+ 'distance': 6
+ },
+ 'icons': {
+ 'issue': '\uf41b',
+ 'pull_request': '\uea64',
+ 'release': '\uea84',
+ 'discussion': '\uf442',
+ 'default': '\uea84',
+ 'github_logo': '\uea84'
+ },
'animation': {
'enabled': True,
'type': 'fadeInOut',
@@ -54,17 +68,71 @@
'type': 'integer',
'default': DEFAULTS['max_field_size']
},
- 'menu_width': {
- 'type': 'integer',
- 'default': DEFAULTS['menu_width']
- },
- 'menu_height': {
- 'type': 'integer',
- 'default': DEFAULTS['menu_height']
+ 'menu': {
+ 'type': 'dict',
+ 'required': False,
+ 'schema': {
+ 'blur': {
+ 'type': 'boolean',
+ 'default': DEFAULTS['menu']['blur']
+ },
+ 'round_corners': {
+ 'type': 'boolean',
+ 'default': DEFAULTS['menu']['round_corners']
+ },
+ 'round_corners_type': {
+ 'type': 'string',
+ 'default': DEFAULTS['menu']['round_corners_type']
+ },
+ 'border_color': {
+ 'type': 'string',
+ 'default': DEFAULTS['menu']['border_color']
+ },
+ 'alignment': {
+ 'type': 'string',
+ 'default': DEFAULTS['menu']['alignment']
+ },
+ 'direction': {
+ 'type': 'string',
+ 'default': DEFAULTS['menu']['direction']
+ },
+ 'distance': {
+ 'type': 'integer',
+ 'default': DEFAULTS['menu']['distance']
+ }
+ },
+ 'default': DEFAULTS['menu']
},
- 'menu_offset': {
- 'type': 'integer',
- 'default': DEFAULTS['menu_offset']
+ 'icons': {
+ 'type': 'dict',
+ 'required': False,
+ 'schema': {
+ 'issue': {
+ 'type': 'string',
+ 'default': DEFAULTS['icons']['issue']
+ },
+ 'pull_request': {
+ 'type': 'string',
+ 'default': DEFAULTS['icons']['pull_request']
+ },
+ 'release': {
+ 'type': 'string',
+ 'default': DEFAULTS['icons']['release']
+ },
+ 'discussion': {
+ 'type': 'string',
+ 'default': DEFAULTS['icons']['discussion']
+ },
+ 'default': {
+ 'type': 'string',
+ 'default': DEFAULTS['icons']['default']
+ },
+ 'github_logo': {
+ 'type': 'string',
+ 'default': DEFAULTS['icons']['github_logo']
+ }
+ },
+ 'default': DEFAULTS['icons']
},
'animation': {
'type': 'dict',
diff --git a/src/core/widgets/yasb/github.py b/src/core/widgets/yasb/github.py
index 509cd70..ee1fd71 100644
--- a/src/core/widgets/yasb/github.py
+++ b/src/core/widgets/yasb/github.py
@@ -3,32 +3,17 @@
import logging
import threading
import requests
+from settings import DEBUG
from datetime import datetime
+from core.utils.utilities import PopupWidget
from core.validation.widgets.yasb.github import VALIDATION_SCHEMA
from core.widgets.base import BaseWidget
from PyQt6.QtGui import QDesktopServices,QCursor
-from PyQt6.QtWidgets import QMenu, QWidget, QVBoxLayout, QLabel, QPushButton, QHBoxLayout, QScrollArea, QVBoxLayout, QWidgetAction
+from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QScrollArea, QVBoxLayout, QGraphicsOpacityEffect
from PyQt6.QtCore import Qt, QPoint, QTimer, QUrl
from core.utils.widgets.animation_manager import AnimationManager
-
logging.getLogger("urllib3").setLevel(logging.WARNING)
-class HoverWidget(QWidget):
- def __init__(self):
- super().__init__()
- self.setAutoFillBackground(True)
-
- def enterEvent(self, event):
- # Change background color on hover
- self.setStyleSheet("background-color: rgba(255, 255, 255, 0.05)")
- super().enterEvent(event)
-
- def leaveEvent(self, event):
- # Reset background color when not hovering
- self.setStyleSheet("background-color:transparent")
- super().leaveEvent(event)
-
-
class GithubWidget(BaseWidget):
validation_schema = VALIDATION_SCHEMA
@@ -41,29 +26,29 @@ def __init__(
max_notification: int,
only_unread: bool,
max_field_size: int,
- menu_width: int,
- menu_height: int,
- menu_offset: str,
+ menu: dict[str, str],
+ icons: dict[str, str],
update_interval: int,
animation: dict[str, str],
container_padding: dict[str, int]
):
super().__init__((update_interval * 1000), class_name="github-widget")
- self._menu_open = False
+
self._show_alt_label = False
self._label_content = label
self._label_alt_content = label_alt
self._token = token if token != 'env' else os.getenv('YASB_GITHUB_TOKEN')
self._tooltip = tooltip
- self._menu_width = menu_width
- self._menu_height = menu_height
- self._menu_offset = menu_offset
+ self._menu_popup = menu
+ self._icons = icons
self._max_notification = max_notification
self._only_unread = only_unread
self._max_field_size = max_field_size
self._animation = animation
- self._github_data = []
self._padding = container_padding
+
+ self._github_data = []
+
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'])
@@ -93,9 +78,11 @@ def __init__(
def _toggle_menu(self):
if self._animation['enabled']:
AnimationManager.animate(self, self._animation['type'], self._animation['duration'])
- self.show_menu(self)
+ self.show_menu()
def _toggle_label(self):
+ if self._animation['enabled']:
+ AnimationManager.animate(self, self._animation['type'], self._animation['duration'])
self._show_alt_label = not self._show_alt_label
for widget in self._widgets:
widget.setVisible(not self._show_alt_label)
@@ -137,7 +124,6 @@ def process_content(content, is_alt=False):
def _update_label(self):
-
notification_count = len([notification for notification in self._github_data if notification['unread']])
active_widgets = self._widgets_alt if self._show_alt_label else self._widgets
active_label_content = self._label_alt_content if self._show_alt_label else self._label_content
@@ -172,146 +158,193 @@ def _update_label(self):
- def mark_as_read(self, notification_id, text_label, icon_label):
+ def mark_as_read(self, notification_id, container_label):
for notification in self._github_data:
if notification['id'] == notification_id:
notification['unread'] = False
break
self._update_label()
- text_label_stylesheet = text_label.styleSheet()
- icon_label_stylesheet = icon_label.styleSheet()
- if "color:" in text_label_stylesheet:
- updated_stylesheet = re.sub(r"color:\s*#[0-9a-fA-F]+", f"color:#9399b2", text_label_stylesheet)
- text_label.setStyleSheet(updated_stylesheet)
-
- if "color:" in icon_label_stylesheet:
- updated_stylesheet = re.sub(r"color:\s*#[0-9a-fA-F]+", f"color:#9399b2", icon_label_stylesheet)
- icon_label.setStyleSheet(updated_stylesheet)
- text_label.repaint()
- icon_label.repaint()
-
-
- def show_menu(self, button):
- if self._menu_open: # Check if the menu is already open
- self._menu_open = False
- return # Exit the function if the menu is open
- self._menu_open = True # Set the menu state to open
-
- def reset_menu_open():
- self._menu_open = False
-
- global_position = button.mapToGlobal(QPoint(0, button.height()))
+ current_classes = container_label.property("class").split()
+ if "new" in current_classes:
+ current_classes.remove("new")
+ container_label.setProperty("class", " ".join(current_classes))
+ container_label.setStyleSheet(container_label.styleSheet())
+ container_label.repaint()
+
+
+ def mark_as_read_notification_on_github(self, notification_id):
+ headers = {
+ 'Authorization': f'token {self._token}',
+ 'Accept': 'application/vnd.github.v3+json'
+ }
+ url = f'https://api.github.com/notifications/threads/{notification_id}'
+ try:
+ response = requests.patch(url, headers=headers)
+ response.raise_for_status()
+ if DEBUG:
+ logging.info(f"Notification {notification_id} marked as read on GitHub.")
+ except requests.HTTPError as e:
+ logging.error(f"HTTP Error occurred: {e.response.status_code} - {e.response.text}")
+ except Exception as e:
+ logging.error(f"An unexpected error occurred: {str(e)}, in most cases this error when there is no internet connection.")
+
+
+ def _handle_mouse_press_event(self, event, notification_id, url, container_label):
+ self.mark_as_read(notification_id, container_label)
+ self._menu.hide()
+ QDesktopServices.openUrl(QUrl(url))
+ self.mark_as_read_notification_on_github(notification_id)
+
+
+ def _create_container_mouse_press_event(self, notification_id, url, container_label):
+ def mouse_press_event(event):
+ self._handle_mouse_press_event(event, notification_id, url, container_label)
+ return mouse_press_event
+
+
+ def show_menu(self):
notifications_count = len(self._github_data)
notifications_unread_count = len([notification for notification in self._github_data if notification['unread']])
- main_widget = QWidget()
- main_layout = QVBoxLayout(main_widget)
- header_label = QLabel(f"GitHub Notifications")
- header_label.setStyleSheet("border-bottom:1px solid rgba(255,255,255,0.1);font-size:16px;padding:8px;color:white;background-color:rgba(255,255,255,0.05)")
+ self._menu = PopupWidget(self, self._menu_popup['blur'], self._menu_popup['round_corners'], self._menu_popup['round_corners_type'], self._menu_popup['border_color'])
+ self._menu.setProperty('class', 'github-menu')
+ self._menu.setWindowFlag(Qt.WindowType.FramelessWindowHint)
+ self._menu.setWindowFlag(Qt.WindowType.Popup)
+ self._menu.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint)
+ main_layout = QVBoxLayout(self._menu)
main_layout.setSpacing(0)
main_layout.setContentsMargins(0, 0, 0, 0)
+ header_label = QLabel(f"GitHub Notifications")
+ header_label.setProperty("class", "header")
+ main_layout.addWidget(header_label)
+
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
+
+ scroll_area.setViewportMargins(0, 0, -4, 0) # overlay the scrollbar 6px to the left
scroll_area.setStyleSheet("""
- QScrollArea { background: transparent; border-radius:8px; }
- QScrollBar:vertical { border: none; background:transparent; width: 6px; margin: 4px 0; }
- QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: rgba(20, 25, 36,0); }
- QScrollBar::handle:vertical { background: rgba(255, 255, 255, 0.3); min-height: 20px; border-radius: 3px; }
- QScrollBar::handle:vertical:hover { background: rgba(255, 255, 255, 0.5); }
- QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {background: rgba(20, 25, 36,0);}
+ QScrollArea { background: transparent; border: none; border-radius:0; }
+ QScrollBar:vertical { border: none; background:transparent; width: 4px; }
+ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: transparent; }
+ QScrollBar::handle:vertical { background: rgba(255, 255, 255, 0.2); min-height: 10px; border-radius: 2px; }
+ QScrollBar::handle:vertical:hover { background: rgba(255, 255, 255, 0.35); }
+ QScrollBar::sub-line:vertical, QScrollBar::add-line:vertical { height: 0px; }
+ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: transparent; }
""")
+ main_layout.addWidget(scroll_area)
+
scroll_widget = QWidget()
+ scroll_widget.setProperty("class", "contents")
scroll_layout = QVBoxLayout(scroll_widget)
- scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Align items to the top
- scroll_widget.setStyleSheet("background: transparent")
+ scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
scroll_layout.setContentsMargins(0, 0, 0, 0)
scroll_layout.setSpacing(0)
scroll_area.setWidget(scroll_widget)
if notifications_count > 0:
- _menu_height = self._menu_height
for notification in self._github_data:
repo_title = notification['title']
repo_description = f'{notification["type"]}: {notification["repository"]}'
- if len(notification['title']) > self._max_field_size:
- repo_title = notification['title'][:self._max_field_size - 3] + '...'
- if len(repo_description) > self._max_field_size:
- repo_description = repo_description[:self._max_field_size - 3] + '...'
+ repo_title = (notification['title'][:self._max_field_size - 3] + '...') if len(notification['title']) > self._max_field_size else notification['title']
+ repo_description = (repo_description[:self._max_field_size - 3] + '...') if len(repo_description) > self._max_field_size else repo_description
+
+ icon_type = {
+ 'Issue': self._icons['issue'],
+ 'PullRequest': self._icons['pull_request'],
+ 'Release': self._icons['release'],
+ 'Discussion': self._icons['discussion']
+ }.get(notification['type'], self._icons['default'])
- icon_type = '\uf41b' if notification['type'] == 'Issue' else '\uea64' if notification['type'] == 'PullRequest' else '\uea84'
+ new_item_class = 'new' if notification['unread'] else ""
+
+ container = QWidget()
+ container.setProperty("class", f"item {new_item_class}")
+ container.setContentsMargins(0, 0, 8, 0)
+ container.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
- container = HoverWidget()
icon_label = QLabel(f"{icon_type}")
- unread_text = '#ffffff' if notification['unread'] else '#9399b2'
- unread_icon = '#3fb950' if notification['unread'] else '#9399b2'
- icon_label.setStyleSheet(f"color:{unread_icon};font-size:16px;padding-right:0;padding-left:8px")
- text_label = QLabel(
- f"{repo_title}
"
- f"{repo_description}"
- )
- text_label.setStyleSheet(f"color:{unread_text};font-family:Segoe UI;font-weight:600;padding-left:0px;font-size:14px;padding-right:14px")
+ icon_label.setProperty("class", "icon")
+
+ title_label = QLabel(repo_title)
+ title_label.setProperty("class", "title")
+
+ description_label = QLabel(repo_description)
+ description_label.setProperty("class", "description")
+
+ text_content = QWidget()
+ text_content_layout = QVBoxLayout(text_content)
+ text_content_layout.addWidget(title_label)
+ text_content_layout.addWidget(description_label)
+ text_content_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
+ text_content_layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
+ text_content_layout.setContentsMargins(0, 0, 0, 0)
+ text_content_layout.setSpacing(0)
+
container_layout = QHBoxLayout(container)
container_layout.addWidget(icon_label)
- container_layout.addWidget(text_label, 1)
+ container_layout.addWidget(text_content, 1)
container_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
container_layout.setContentsMargins(0, 0, 0, 0)
container_layout.setSpacing(0)
- button = QPushButton()
- button.setStyleSheet("""
- QPushButton {
- background: rgba(0,0,0,0);
- border: none;
- color: white;
- text-align:left;
- font-size:14px;
- padding:6px 8px;
- height:40px;
- border-top:1px solid rgba(255,255,255,0.05)
- }
- """)
-
- button.clicked.connect(lambda checked, nid=notification['id'], text_label=text_label, icon_label=icon_label, url=notification['url']: (self.mark_as_read(nid, text_label, icon_label), QDesktopServices.openUrl(QUrl(url))))
- layout = QVBoxLayout(button)
- layout.addWidget(container)
- layout.setContentsMargins(0, 0, 0, 0)
- button.setLayout(layout)
- scroll_layout.addWidget(button)
+ scroll_layout.addWidget(container)
+
+ container.mousePressEvent = self._create_container_mouse_press_event(notification['id'], notification['url'], container)
+ #container.mousePressEvent = self._create_container_mouse_press_event(notification['id'], notification['url'])
else:
- large_label = QLabel("\uea84")
- large_label.setStyleSheet("font-size:88px;color:#313244")
- scroll_layout.addWidget(large_label, alignment=Qt.AlignmentFlag.AlignCenter)
+ large_label = QLabel(self._icons['github_logo'])
+ large_label.setStyleSheet("font-size:88px;font-weight:400")
+ opacity_effect = QGraphicsOpacityEffect()
+ opacity_effect.setOpacity(0.4)
+ large_label.setGraphicsEffect(opacity_effect)
+
no_data = QLabel("No unread notifications")
- no_data.setStyleSheet("font-size:16px;color:#616172")
- scroll_layout.addWidget(no_data, alignment=Qt.AlignmentFlag.AlignCenter)
- _menu_height = 200
+ no_data.setStyleSheet("font-size:18px;font-weight:400;font-family: Segoe UI")
+ opacity_effect = QGraphicsOpacityEffect()
+ opacity_effect.setOpacity(0.5)
+ no_data.setGraphicsEffect(opacity_effect)
- # Add Footer
- footer_label = QLabel(f"Unread notifications ({notifications_unread_count})")
- footer_label.setStyleSheet("border-top:1px solid rgba(255,255,255,0.1);font-size:12px;padding:4px 8px 6px 8px;color:#9399b2;background-color:rgba(255,255,255,0.05)")
- main_layout.addWidget(header_label)
- main_layout.addWidget(scroll_area)
+ # Create a vertical layout to center the widgets
+ center_layout = QVBoxLayout()
+ center_layout.addStretch()
+ center_layout.addWidget(large_label, alignment=Qt.AlignmentFlag.AlignCenter)
+ center_layout.addWidget(no_data, alignment=Qt.AlignmentFlag.AlignCenter)
+ center_layout.addStretch()
+
+ # Add the center layout to the scroll layout
+ scroll_layout.addLayout(center_layout)
if notifications_count > 0:
+ footer_label = QLabel(f"Unread notifications ({notifications_unread_count})")
+ footer_label.setProperty("class", "footer")
main_layout.addWidget(footer_label)
- scroll_area.setFixedSize(self._menu_width, _menu_height)
+ self._menu.adjustSize()
+ widget_global_pos = self.mapToGlobal(QPoint(0, self.height() + self._menu_popup['distance']))
- scroll_menu = QMenu()
- scroll_menu.setStyleSheet("""
- QMenu { background:rgb(20, 25, 36);border: 1px solid rgba(255,255,255,0.1);border-radius:8px }
- """)
- scroll_action = QWidgetAction(scroll_menu)
- scroll_action.setDefaultWidget(main_widget)
- scroll_menu.addAction(scroll_action)
- m_position = QPoint(global_position.x() - self._menu_offset, global_position.y())
- scroll_menu.exec(m_position)
+ if self._menu_popup['direction'] == 'up':
+ global_y = self.mapToGlobal(QPoint(0, 0)).y() - self._menu.height() - self._menu_popup['distance']
+ widget_global_pos = QPoint(self.mapToGlobal(QPoint(0, 0)).x(), global_y)
- # Reset the menu state after the menu is closed
- QTimer.singleShot(0, reset_menu_open)
+ if self._menu_popup['alignment'] == 'left':
+ global_position = widget_global_pos
+ elif self._menu_popup['alignment'] == 'right':
+ global_position = QPoint(
+ widget_global_pos.x() + self.width() - self._menu.width(),
+ widget_global_pos.y()
+ )
+ elif self._menu_popup['alignment'] == 'center':
+ global_position = QPoint(
+ widget_global_pos.x() + (self.width() - self._menu.width()) // 2,
+ widget_global_pos.y()
+ )
+ else:
+ global_position = widget_global_pos
+
+ self._menu.move(global_position)
+ self._menu.show()
-
def get_github_data(self):
threading.Thread(target=self._get_github_data).start()
@@ -322,7 +355,8 @@ def _get_github_data(self):
def _get_github_notifications(self, token):
- logging.info(f"Check for GitHub notifications at {datetime.now()}")
+ if DEBUG:
+ logging.info(f"Check for GitHub notifications at {datetime.now()}")
headers = {
'Authorization': f'token {token}',
'Accept': 'application/vnd.github.v3+json'
@@ -352,9 +386,11 @@ def _get_github_notifications(self, token):
github_url = subject_url.replace('api.github.com/repos', 'github.com').replace('/pulls/', '/pull/')
elif subject_type == 'Release':
github_url = f'https://github.com/{repo_full_name}/releases'
+ elif subject_type == 'Discussion':
+ github_url = subject_url.replace('api.github.com/repos', 'github.com')
else:
github_url = notification['repository']['html_url']
-
+
result.append({
'id': notification['id'],
'repository': repo_full_name,