570 lines
25 KiB
Python
570 lines
25 KiB
Python
from PyQt6.QtWidgets import QLayout, QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QStackedWidget, QSizePolicy, QSpacerItem
|
|
from PyQt6.QtGui import QIcon
|
|
from PyQt6.QtCore import QSize
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
import hashlib
|
|
import app.utils.paths as paths
|
|
from app.core.main_manager import MainManager, NotificationType
|
|
import xml.etree.ElementTree as ET
|
|
|
|
class MenuDirection(Enum):
|
|
HORIZONTAL = 0
|
|
VERTICAL = 1
|
|
|
|
class ButtonPosition(Enum):
|
|
START = 0 # Au début (aligné à gauche/haut)
|
|
END = 1 # À la fin (aligné à droite/bas)
|
|
CENTER = 2 # Au centre
|
|
AFTER = 3 # Après un bouton spécifique
|
|
|
|
class BorderSide(Enum):
|
|
LEFT = "left"
|
|
RIGHT = "right"
|
|
TOP = "top"
|
|
BOTTOM = "bottom"
|
|
NONE = None
|
|
|
|
class TabSide(Enum):
|
|
LEFT = 0 # Barre à gauche (pour VERTICAL)
|
|
RIGHT = 1 # Barre à droite (pour VERTICAL)
|
|
TOP = 0 # Barre en haut (pour HORIZONTAL)
|
|
BOTTOM = 1 # Barre en bas (pour HORIZONTAL)
|
|
|
|
class TabsWidget(QWidget):
|
|
def __init__(self, parent=None, direction=MenuDirection.VERTICAL, menu_width=80, onTabChange=None, spacing=10, button_size_ratio=0.8, border_side=BorderSide.LEFT, tab_side=None):
|
|
super().__init__(parent)
|
|
self.main_manager = MainManager.get_instance()
|
|
self.theme_manager = self.main_manager.get_theme_manager()
|
|
self.observer_manager = self.main_manager.get_observer_manager()
|
|
self.observer_manager.subscribe(NotificationType.THEME, self.set_theme)
|
|
self.direction = direction
|
|
self.menu_width = menu_width
|
|
self.button_size_ratio = button_size_ratio # Default ratio for button size relative to menu width
|
|
self.onTabChange = onTabChange
|
|
|
|
# Gérer border_side comme une liste ou un seul élément
|
|
if isinstance(border_side, list):
|
|
self.border_sides = border_side
|
|
elif border_side is not None:
|
|
self.border_sides = [border_side]
|
|
else:
|
|
self.border_sides = []
|
|
|
|
# Déterminer le côté de la barre d'onglets
|
|
if tab_side is None:
|
|
# Valeurs par défaut basées sur la direction
|
|
self.tab_side = TabSide.LEFT if direction == MenuDirection.VERTICAL else TabSide.TOP
|
|
else:
|
|
self.tab_side = tab_side
|
|
|
|
self.buttons = []
|
|
self.widgets = []
|
|
self.button_positions = []
|
|
self.button_size_ratios = [] # Individual ratios for each button
|
|
self._icon_cache = {}
|
|
# Track alignment zones
|
|
self.start_buttons = []
|
|
self.center_buttons = []
|
|
self.end_buttons = []
|
|
|
|
# Icon Colors
|
|
self.selected_icon_color = self.theme_manager.current_theme.get_color("icon_selected_color")
|
|
self.unselected_icon_color = self.theme_manager.current_theme.get_color("icon_unselected_color")
|
|
self.selected_border_icon_color = self.theme_manager.current_theme.get_color("icon_selected_border_color")
|
|
self.hover_icon_color = self.theme_manager.current_theme.get_color("icon_hover_color")
|
|
|
|
# Spacer items for alignment
|
|
self.left_spacer = None
|
|
self.center_spacer = None
|
|
self.right_spacer = None
|
|
self.spacing = spacing
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
"""Setup the main layout based on direction and tab_side"""
|
|
if self.direction == MenuDirection.HORIZONTAL:
|
|
self.main_layout = QVBoxLayout(self)
|
|
self.button_layout = QHBoxLayout()
|
|
else: # VERTICAL
|
|
self.main_layout = QHBoxLayout(self)
|
|
self.button_layout = QVBoxLayout()
|
|
|
|
# Remove all spacing and margins
|
|
self.main_layout.setSpacing(0)
|
|
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
|
self.button_layout.setSpacing(self.spacing)
|
|
self.button_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.stacked_widget = QStackedWidget()
|
|
# Create button container widget
|
|
self.button_container = QWidget()
|
|
self.button_container.setObjectName("tab_bar")
|
|
self.button_container.setLayout(self.button_layout)
|
|
|
|
# Remove all margins from button container
|
|
self.button_container.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Set minimum size for button container to prevent shrinking
|
|
if self.direction == MenuDirection.VERTICAL:
|
|
self.button_container.setMaximumWidth(self.menu_width)
|
|
self.button_container.setMinimumWidth(self.menu_width)
|
|
self.button_container.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
|
|
else:
|
|
self.button_container.setMinimumHeight(self.menu_width)
|
|
self.button_container.setMaximumHeight(self.menu_width)
|
|
self.button_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
|
|
# Initialize spacers for alignment zones
|
|
self._setup_alignment_zones()
|
|
|
|
# Add widgets to main layout based on direction and tab_side
|
|
if self.tab_side == TabSide.LEFT:
|
|
self.main_layout.addWidget(self.button_container)
|
|
self.main_layout.addWidget(self.stacked_widget)
|
|
else: # TabSide.RIGHT
|
|
self.main_layout.addWidget(self.stacked_widget)
|
|
self.main_layout.addWidget(self.button_container)
|
|
|
|
self.setLayout(self.main_layout)
|
|
|
|
def _setup_alignment_zones(self):
|
|
"""Setup spacers to create alignment zones"""
|
|
# Create spacers
|
|
if self.direction == MenuDirection.HORIZONTAL:
|
|
self.left_spacer = QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
|
self.center_spacer = QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
|
else:
|
|
self.left_spacer = QSpacerItem(0, 0, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
|
self.center_spacer = QSpacerItem(0, 0, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
|
|
|
# Add spacers to layout (no right spacer so END buttons stick to edge)
|
|
self.button_layout.addSpacerItem(self.left_spacer) # Before center
|
|
self.button_layout.addSpacerItem(self.center_spacer) # After center
|
|
# Removed right_spacer so END buttons are at the edge
|
|
|
|
# Add a widget on the menu, just the widget, with position
|
|
def add_random_widget(self, widget, position=ButtonPosition.END):
|
|
"""Add a widget directly to the menu at the specified position"""
|
|
if isinstance(widget, QLayout):
|
|
# If it's a layout, wrap it in a container widget
|
|
container_widget = QWidget()
|
|
container_widget.setLayout(widget)
|
|
container_widget.setContentsMargins(0, 0, 0, 0)
|
|
self._insert_widget_with_alignment(container_widget, position)
|
|
return container_widget
|
|
else:
|
|
# If it's already a widget, insert it directly
|
|
self._insert_widget_with_alignment(widget, position)
|
|
return widget
|
|
|
|
def add_random_layout(self, layout, position=ButtonPosition.END):
|
|
"""Add a layout at the specified position in the button layout"""
|
|
# Create a container widget for the layout
|
|
container_widget = QWidget()
|
|
container_widget.setLayout(layout)
|
|
container_widget.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Insert the container widget at the specified position
|
|
self._insert_widget_with_alignment(container_widget, position)
|
|
return container_widget
|
|
|
|
def _insert_widget_with_alignment(self, widget, position):
|
|
"""Insert any widget at the specified position with proper visual alignment"""
|
|
if position == ButtonPosition.START:
|
|
# Insert at the beginning (before left spacer)
|
|
self.button_layout.insertWidget(0, widget)
|
|
|
|
elif position == ButtonPosition.CENTER:
|
|
# Insert in center zone (after left spacer, before center spacer)
|
|
center_start_index = self._get_spacer_index(self.left_spacer) + 1
|
|
insert_index = center_start_index + len(self.center_buttons)
|
|
self.button_layout.insertWidget(insert_index, widget)
|
|
|
|
elif position == ButtonPosition.END:
|
|
# Insert in end zone (after center spacer, at the end)
|
|
end_start_index = self._get_spacer_index(self.center_spacer) + 1
|
|
insert_index = end_start_index + len(self.end_buttons)
|
|
self.button_layout.insertWidget(insert_index, widget)
|
|
|
|
def add_widget(self, widget, button_text, icon_path=None, position=ButtonPosition.END, after_button_index=None, button_size_ratio=None):
|
|
"""Add a widget with its corresponding button at specified position"""
|
|
# Store original icon path for theme updates
|
|
if not hasattr(self, '_original_icon_paths'):
|
|
self._original_icon_paths = []
|
|
|
|
# Create button
|
|
if icon_path:
|
|
colored_icon = self.apply_color_to_svg_icon(icon_path, self.unselected_icon_color)
|
|
button = QPushButton(colored_icon, button_text)
|
|
self._original_icon_paths.append(icon_path)
|
|
else:
|
|
button = QPushButton(button_text)
|
|
self._original_icon_paths.append(None)
|
|
|
|
button.setObjectName("menu_button")
|
|
button.setCheckable(True)
|
|
|
|
# Store button size ratio (use provided ratio or default)
|
|
ratio = button_size_ratio if button_size_ratio is not None else self.button_size_ratio
|
|
self.button_size_ratios.append(ratio)
|
|
|
|
# Make button square with specified ratio
|
|
self._style_square_button(button)
|
|
|
|
# Add to collections first
|
|
widget_index = len(self.widgets)
|
|
self.buttons.append(button)
|
|
self.widgets.append(widget)
|
|
self.button_positions.append(position)
|
|
|
|
# Connect button to switch function
|
|
button.clicked.connect(lambda checked, idx=widget_index: self.switch_to_tab(idx))
|
|
|
|
# Add widget to stacked widget
|
|
self.stacked_widget.addWidget(widget)
|
|
|
|
# Insert button at specified position with proper alignment
|
|
self._insert_button_with_alignment(button, position, after_button_index)
|
|
|
|
# Select first tab by default
|
|
if len(self.buttons) == 1:
|
|
self.switch_to_tab(0)
|
|
|
|
return widget_index
|
|
|
|
def _style_square_button(self, button):
|
|
# Set size policy
|
|
button.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
|
|
|
# Create border style based on border_sides setting
|
|
border_style = self._get_border_style()
|
|
button.setStyleSheet(border_style)
|
|
|
|
# Install event filter for hover detection
|
|
button.installEventFilter(self)
|
|
|
|
# Calculate initial size (will be updated in resizeEvent)
|
|
self._update_button_size(button)
|
|
|
|
# Store reference for resize updates
|
|
if not hasattr(self, '_square_buttons'):
|
|
self._square_buttons = []
|
|
self._square_buttons.append(button)
|
|
|
|
def eventFilter(self, obj : QPushButton, event):
|
|
"""Handle hover events for buttons"""
|
|
if obj in self.buttons:
|
|
if event.type() == event.Type.Enter:
|
|
# Mouse entered button
|
|
obj.setIcon(self.apply_color_to_svg_icon(self._original_icon_paths[self.buttons.index(obj)],self.hover_icon_color))
|
|
elif event.type() == event.Type.Leave:
|
|
# Mouse left button
|
|
obj.setIcon(self.apply_color_to_svg_icon(self._original_icon_paths[self.buttons.index(obj)],self.unselected_icon_color if not obj.isChecked() else self.selected_icon_color))
|
|
|
|
return super().eventFilter(obj, event)
|
|
|
|
def _get_border_style(self):
|
|
"""Generate CSS border style based on border_sides setting"""
|
|
if not self.border_sides or BorderSide.NONE in self.border_sides:
|
|
return f"""
|
|
QPushButton {{
|
|
border-radius: 0px;
|
|
background-color: transparent;
|
|
border: none;
|
|
}}
|
|
QPushButton[selected="true"] {{
|
|
border-radius: 0px;
|
|
background-color: transparent;
|
|
border: none;
|
|
}}
|
|
"""
|
|
|
|
# Construire le style CSS pour chaque côté de bordure
|
|
border_declarations = []
|
|
selected_border_declarations = []
|
|
|
|
for border_side in self.border_sides:
|
|
if border_side != BorderSide.NONE and border_side is not None:
|
|
border_declarations.append(f"border-{border_side.value}: 3px solid transparent")
|
|
selected_border_declarations.append(f"border-{border_side.value}: 3px solid {self.selected_border_icon_color}")
|
|
|
|
border_style = "; ".join(border_declarations)
|
|
selected_border_style = "; ".join(selected_border_declarations)
|
|
|
|
return f"""
|
|
QPushButton {{
|
|
border-radius: 0px;
|
|
background-color: transparent;
|
|
{border_style};
|
|
}}
|
|
QPushButton[selected="true"] {{
|
|
border-radius: 0px;
|
|
background-color: transparent;
|
|
{selected_border_style};
|
|
}}
|
|
"""
|
|
|
|
def _update_button_size(self, button):
|
|
"""Update button size based on menu width and button's individual ratio"""
|
|
# Find the button's index to get its specific ratio
|
|
button_index = -1
|
|
for i, btn in enumerate(self.buttons):
|
|
if btn == button:
|
|
button_index = i
|
|
break
|
|
|
|
if button_index == -1:
|
|
return # Button not found
|
|
|
|
# Get the specific ratio for this button
|
|
ratio = self.button_size_ratios[button_index] if button_index < len(self.button_size_ratios) else self.button_size_ratio
|
|
|
|
button_size = int(self.menu_width * ratio)
|
|
button.setFixedSize(QSize(button_size, button_size))
|
|
# Set proportional icon size
|
|
icon_size = int(button_size * 0.6)
|
|
button.setIconSize(QSize(icon_size, icon_size))
|
|
|
|
def _update_all_button_sizes(self):
|
|
"""Update all button sizes when container is resized"""
|
|
if hasattr(self, '_square_buttons'):
|
|
for button in self._square_buttons:
|
|
if button.parent(): # Check if button still exists
|
|
self._update_button_size(button)
|
|
|
|
def showEvent(self, event):
|
|
"""Handle show event to set initial button sizes"""
|
|
super().showEvent(event)
|
|
# Update button sizes when widget is first shown
|
|
self._update_all_button_sizes()
|
|
|
|
def _insert_button_with_alignment(self, button, position, after_button_index=None):
|
|
"""Insert button at the specified position with proper visual alignment"""
|
|
if position == ButtonPosition.START:
|
|
# Insert at the beginning (before left spacer)
|
|
self.button_layout.insertWidget(0, button)
|
|
self.start_buttons.append(button)
|
|
|
|
elif position == ButtonPosition.CENTER:
|
|
# Insert in center zone (after left spacer, before center spacer)
|
|
center_start_index = self._get_spacer_index(self.left_spacer) + 1
|
|
insert_index = center_start_index + len(self.center_buttons)
|
|
self.button_layout.insertWidget(insert_index, button)
|
|
self.center_buttons.append(button)
|
|
|
|
elif position == ButtonPosition.END:
|
|
# Insert in end zone (after center spacer, at the end)
|
|
end_start_index = self._get_spacer_index(self.center_spacer) + 1
|
|
insert_index = end_start_index + len(self.end_buttons)
|
|
self.button_layout.insertWidget(insert_index, button)
|
|
self.end_buttons.append(button)
|
|
|
|
elif position == ButtonPosition.AFTER and after_button_index is not None:
|
|
if 0 <= after_button_index < len(self.buttons) - 1:
|
|
target_button = self.buttons[after_button_index]
|
|
target_position = self.button_positions[after_button_index]
|
|
|
|
# Find the target button's layout index
|
|
target_layout_index = -1
|
|
for i in range(self.button_layout.count()):
|
|
item = self.button_layout.itemAt(i)
|
|
if item.widget() == target_button:
|
|
target_layout_index = i
|
|
break
|
|
|
|
if target_layout_index != -1:
|
|
self.button_layout.insertWidget(target_layout_index + 1, button)
|
|
|
|
# Add to the same alignment zone as target
|
|
if target_position == ButtonPosition.START:
|
|
self.start_buttons.append(button)
|
|
elif target_position == ButtonPosition.CENTER:
|
|
self.center_buttons.append(button)
|
|
elif target_position == ButtonPosition.END:
|
|
self.end_buttons.append(button)
|
|
else:
|
|
# Fallback to END position
|
|
self._insert_button_with_alignment(button, ButtonPosition.END)
|
|
|
|
def _get_spacer_index(self, spacer_item):
|
|
"""Get the index of a spacer item in the layout"""
|
|
for i in range(self.button_layout.count()):
|
|
if self.button_layout.itemAt(i).spacerItem() == spacer_item:
|
|
return i
|
|
return -1
|
|
|
|
def set_button_size_ratio(self, ratio, button_index=None):
|
|
"""Set the button size ratio globally or for a specific button"""
|
|
if button_index is not None:
|
|
# Set ratio for specific button
|
|
if 0 <= button_index < len(self.button_size_ratios):
|
|
self.button_size_ratios[button_index] = ratio
|
|
if button_index < len(self.buttons):
|
|
self._update_button_size(self.buttons[button_index])
|
|
else:
|
|
# Set global ratio
|
|
self.button_size_ratio = ratio
|
|
# Update all buttons that don't have individual ratios
|
|
for i in range(len(self.buttons)):
|
|
if i >= len(self.button_size_ratios):
|
|
self.button_size_ratios.append(ratio)
|
|
else:
|
|
self.button_size_ratios[i] = ratio
|
|
self._update_all_button_sizes()
|
|
|
|
def switch_to_tab(self, index):
|
|
"""Switch to the specified tab (only for widgets, not simple buttons)"""
|
|
if 0 <= index < len(self.buttons) and self.widgets[index] is not None:
|
|
old_index = self.stacked_widget.currentIndex()
|
|
|
|
# Update button states and apply theme colors
|
|
for i, button in enumerate(self.buttons):
|
|
# Only handle tab-like behavior for buttons with widgets
|
|
if self.widgets[i] is not None:
|
|
is_selected = (i == index)
|
|
button.setChecked(is_selected)
|
|
|
|
# Update icon color based on selection
|
|
if hasattr(self, '_original_icon_paths') and i < len(self._original_icon_paths):
|
|
icon_path = self._original_icon_paths[i]
|
|
if icon_path:
|
|
color = (self.selected_icon_color if is_selected
|
|
else self.unselected_icon_color)
|
|
colored_icon = self.apply_color_to_svg_icon(icon_path, color)
|
|
button.setIcon(colored_icon)
|
|
|
|
# Update button property for styling
|
|
button.setProperty("selected", is_selected)
|
|
button.style().unpolish(button)
|
|
button.style().polish(button)
|
|
|
|
# Switch stacked widget
|
|
self.stacked_widget.setCurrentIndex(index)
|
|
|
|
# Call the callback if provided and index actually changed
|
|
if self.onTabChange and old_index != index:
|
|
try:
|
|
self.onTabChange(index)
|
|
except Exception:
|
|
pass
|
|
|
|
def set_theme(self):
|
|
self.selected_icon_color = self.theme_manager.current_theme.get_color("icon_selected_color")
|
|
self.unselected_icon_color = self.theme_manager.current_theme.get_color("icon_unselected_color")
|
|
self.selected_border_icon_color = self.theme_manager.current_theme.get_color("icon_selected_border_color")
|
|
self.hover_icon_color = self.theme_manager.current_theme.get_color("icon_hover_color")
|
|
# Apply theme to all buttons
|
|
for i, button in enumerate(self.buttons):
|
|
# Check if button is currently selected
|
|
is_selected = button.isChecked()
|
|
|
|
# Update button stylesheet with current theme colors
|
|
border_style = self._get_border_style()
|
|
button.setStyleSheet(border_style)
|
|
|
|
# Get original icon if it exists
|
|
original_icon = button.icon()
|
|
if not original_icon.isNull():
|
|
# Apply color to SVG and create new icon
|
|
if hasattr(self, '_original_icon_paths') and i < len(self._original_icon_paths):
|
|
icon_path = self._original_icon_paths[i]
|
|
if icon_path:
|
|
# Choose color based on selection state
|
|
color = self.selected_icon_color if is_selected else self.unselected_icon_color
|
|
|
|
# Apply color to SVG and create new QIcon
|
|
colored_icon = self.apply_color_to_svg_icon(icon_path, color)
|
|
button.setIcon(colored_icon)
|
|
|
|
# Apply button styling based on selection state
|
|
button.setProperty("selected", is_selected)
|
|
|
|
# Force style update
|
|
button.style().unpolish(button)
|
|
button.style().polish(button)
|
|
|
|
def apply_color_to_svg_icon(self, icon_path, color) -> QIcon:
|
|
"""
|
|
Create or reuse a colored copy of the SVG in the user's data folder (temp_icons)
|
|
and return a QIcon that points to it. Original SVG is preserved.
|
|
|
|
Caching: deterministic filename based on sha256(icon_path + color) so repeated calls reuse files.
|
|
"""
|
|
try:
|
|
if not icon_path:
|
|
return QIcon()
|
|
|
|
# deterministic filename based on hash to avoid duplicates
|
|
hasher = hashlib.sha256()
|
|
hasher.update(str(Path(icon_path).resolve()).encode('utf-8'))
|
|
hasher.update(str(color).encode('utf-8'))
|
|
digest = hasher.hexdigest()
|
|
new_name = f"{Path(icon_path).stem}_{digest}.svg"
|
|
|
|
app_name = self.main_manager.get_settings_manager().get_config("app_name")
|
|
temp_dir = Path(paths.get_user_temp_icons(app_name))
|
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
tmp_path = temp_dir / new_name
|
|
|
|
# If exists in cache and file exists on disk, return immediately
|
|
cache_key = (str(Path(icon_path).resolve()), color)
|
|
if cache_key in self._icon_cache:
|
|
cached_path = Path(self._icon_cache[cache_key])
|
|
if cached_path.exists():
|
|
return QIcon(str(cached_path))
|
|
else:
|
|
# stale cache entry -> remove
|
|
del self._icon_cache[cache_key]
|
|
|
|
# If file already exists (from previous run), reuse it
|
|
if tmp_path.exists():
|
|
self._icon_cache[cache_key] = str(tmp_path)
|
|
return QIcon(str(tmp_path))
|
|
|
|
# Parse original SVG
|
|
tree = ET.parse(icon_path)
|
|
root = tree.getroot()
|
|
|
|
# Apply fill/stroke to root and to all relevant elements.
|
|
for el in root.iter():
|
|
tag = el.tag
|
|
if not isinstance(tag, str):
|
|
continue
|
|
lname = tag.split('}')[-1] # local name
|
|
if lname in ("svg", "g", "path", "rect", "circle", "ellipse", "polygon", "line", "polyline"):
|
|
if 'fill' in el.attrib:
|
|
el.attrib['fill'] = color
|
|
if 'stroke' in el.attrib:
|
|
el.attrib['stroke'] = color
|
|
|
|
style = el.attrib.get('style')
|
|
if style:
|
|
parts = [p.strip() for p in style.split(';') if p.strip()]
|
|
parts = [p for p in parts if not p.startswith('fill:') and not p.startswith('stroke:')]
|
|
parts.append(f'fill:{color}')
|
|
parts.append(f'stroke:{color}')
|
|
el.attrib['style'] = ';'.join(parts)
|
|
|
|
# Write to deterministic file
|
|
tree.write(tmp_path, encoding='utf-8', xml_declaration=True)
|
|
|
|
# Update cache
|
|
self._icon_cache[cache_key] = str(tmp_path)
|
|
|
|
return QIcon(str(tmp_path))
|
|
except Exception:
|
|
# On any error, fallback to original icon (do not crash)
|
|
try:
|
|
return QIcon(icon_path)
|
|
except Exception:
|
|
return QIcon()
|
|
|
|
def update_buttons_size(self, size):
|
|
"""Update all buttons to a specific pixel size (overrides ratio-based sizing)"""
|
|
for button in self.buttons:
|
|
button.setFixedSize(size, size)
|
|
# Update icon size proportionally
|
|
icon_size = max(int(size * 0.6), 12)
|
|
button.setIconSize(QSize(icon_size, icon_size)) |