2025-12-03 22:09:53 +01:00

760 lines
35 KiB
Python

from PyQt6.QtWidgets import QLayout, QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QStackedWidget, QSizePolicy, QSpacerItem, QLabel
from PyQt6.QtGui import QIcon
from PyQt6.QtCore import QSize, Qt
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 # Barre en haut ou en bas
VERTICAL = 1 # Barre à gauche ou à droite
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" # Bord gauche
RIGHT = "right" # Bord droit
TOP = "top" # Bord supérieur
BOTTOM = "bottom" # Bord inférieur
NONE = None
class TextPosition(Enum):
LEFT = 0 # Texte à gauche de l'icône
RIGHT = 1 # Texte à droite de l'icône
TOP = 2 # Texte au-dessus de l'icône
BOTTOM = 3 # Texte en dessous de l'icône
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, border_side=BorderSide.LEFT, tab_side=None, text_position=TextPosition.BOTTOM):
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.onTabChange = onTabChange
self.text_position = text_position # Position du texte par rapport à l'icône
# 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:
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_text_positions = [] # Individual text positions for each button
self._icon_cache = {}
self._original_icon_paths = []
self._square_buttons = []
# 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)
widget = container_widget
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, text_position=None):
"""Add a widget with its corresponding button at specified position"""
# Use provided text_position or default to widget's text_position
btn_text_position = text_position if text_position is not None else self.text_position
# Create button container with custom layout
button_container = self._create_button_with_layout(button_text, icon_path, btn_text_position)
self._original_icon_paths.append(icon_path)
button_container.setCheckable(True)
self.button_text_positions.append(btn_text_position)
# Make button square with specified ratio
self._style_square_button(button_container)
# Configurer le widget pour qu'il soit responsive
widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# Add to collections first
widget_index = len(self.widgets)
self.buttons.append(button_container)
self.widgets.append(widget)
self.button_positions.append(position)
# Connect button to switch function
button_container.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_container, position, after_button_index)
# Select first tab by default
if len(self.buttons) == 1:
self.switch_to_tab(0)
return widget_index
def _create_button_with_layout(self, text: str, icon_path: str, text_position: TextPosition) -> QPushButton:
"""Create a button with custom layout for icon and text positioning"""
button = QPushButton()
has_icon = icon_path is not None and icon_path != ""
has_text = text is not None and text.strip() != ""
# Create icon label only if there's an icon
icon_label = None
if has_icon:
icon_label = QLabel()
icon_label.setObjectName("icon_label")
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter)
colored_icon = self.apply_color_to_svg_icon(icon_path, self.unselected_icon_color)
pixmap = colored_icon.pixmap(QSize(32, 32)) # Taille par défaut, sera ajustée
icon_label.setPixmap(pixmap)
# Create text label only if there's text
text_label = None
if has_text:
text_label = QLabel(text)
text_label.setObjectName("text_label")
text_label.setAlignment(Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter)
text_label.setWordWrap(True)
# Create layout based on what we have and text position
if has_icon and has_text:
# Both icon and text
if text_position == TextPosition.LEFT:
layout = QHBoxLayout()
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif text_position == TextPosition.RIGHT:
layout = QHBoxLayout()
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif text_position == TextPosition.TOP:
layout = QVBoxLayout()
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
else: # BOTTOM (default)
layout = QVBoxLayout()
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif has_icon:
# Only icon
layout = QVBoxLayout()
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif has_text:
# Only text
layout = QVBoxLayout()
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
else:
# Neither icon nor text - empty button
layout = QVBoxLayout()
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(2)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
button.setLayout(layout)
# Store references to labels for later updates (can be None)
button.icon_label = icon_label
button.text_label = text_label
return button
def _apply_text_position(self, button: QPushButton, text_position: TextPosition):
"""Apply text position to button by recreating its layout"""
# Get existing labels
if not hasattr(button, 'icon_label') or not hasattr(button, 'text_label'):
return
icon_label = button.icon_label
text_label = button.text_label
has_icon = icon_label is not None
has_text = text_label is not None
# Remove old layout
old_layout = button.layout()
if old_layout:
# Remove widgets from layout
while old_layout.count():
item = old_layout.takeAt(0)
if item.widget():
item.widget().setParent(None)
QWidget().setLayout(old_layout) # Delete old layout
# Create new layout based on what we have and text position
if has_icon and has_text:
if text_position == TextPosition.LEFT:
layout = QHBoxLayout()
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif text_position == TextPosition.RIGHT:
layout = QHBoxLayout()
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif text_position == TextPosition.TOP:
layout = QVBoxLayout()
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
else: # BOTTOM (default)
layout = QVBoxLayout()
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif has_icon:
layout = QVBoxLayout()
layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
elif has_text:
layout = QVBoxLayout()
layout.addWidget(text_label, alignment=Qt.AlignmentFlag.AlignCenter)
else:
layout = QVBoxLayout()
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(2)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
button.setLayout(layout)
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
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
button_index = self.buttons.index(obj)
if hasattr(obj, 'icon_label') and button_index < len(self._original_icon_paths):
icon_path = self._original_icon_paths[button_index]
if icon_path:
colored_icon = self.apply_color_to_svg_icon(icon_path, self.hover_icon_color)
current_size = obj.icon_label.size()
pixmap = colored_icon.pixmap(current_size)
obj.icon_label.setPixmap(pixmap)
elif event.type() == event.Type.Leave:
# Mouse left button
button_index = self.buttons.index(obj)
if hasattr(obj, 'icon_label') and button_index < len(self._original_icon_paths):
icon_path = self._original_icon_paths[button_index]
if icon_path:
color = self.unselected_icon_color if not obj.isChecked() else self.selected_icon_color
colored_icon = self.apply_color_to_svg_icon(icon_path, color)
current_size = obj.icon_label.size()
pixmap = colored_icon.pixmap(current_size)
obj.icon_label.setPixmap(pixmap)
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 - will be recalculated globally"""
# This method now triggers a global recalculation
self._recalculate_all_buttons_size()
def _recalculate_all_buttons_size(self):
"""Recalculate size for all buttons to ensure uniform sizing"""
if not self.buttons:
return
max_secondary_size = 0
# First pass: calculate the maximum secondary dimension needed
for i, button in enumerate(self.buttons):
has_icon = button.icon_label is not None
has_text = button.text_label is not None
if has_icon and has_text:
text_pos = self.button_text_positions[i] if i < len(self.button_text_positions) else self.text_position
if self.direction == MenuDirection.VERTICAL:
# Vertical menu: calculate needed height
if text_pos in [TextPosition.TOP, TextPosition.BOTTOM]:
# Vertical layout: need square
max_secondary_size = max(max_secondary_size, self.menu_width)
else:
# Horizontal layout: need less height but ensure text fits
if has_text and button.text_label:
text_height = button.text_label.sizeHint().height() + 20
icon_height = int(self.menu_width * 0.4) + 10
needed_height = max(text_height, icon_height, int(self.menu_width * 0.6))
max_secondary_size = max(max_secondary_size, needed_height)
else:
# Horizontal menu: calculate needed width
if text_pos in [TextPosition.LEFT, TextPosition.RIGHT]:
# Horizontal layout: need square
max_secondary_size = max(max_secondary_size, self.menu_width)
else:
# Vertical layout: need less width but ensure text fits
if has_text and button.text_label:
text_width = button.text_label.sizeHint().width() + 20
icon_width = int(self.menu_width * 0.4) + 10
needed_width = max(text_width, icon_width, int(self.menu_width * 0.6))
max_secondary_size = max(max_secondary_size, needed_width)
elif has_icon or has_text:
# Only icon or only text
if has_text and button.text_label:
if self.direction == MenuDirection.VERTICAL:
text_height = button.text_label.sizeHint().height() + 20
max_secondary_size = max(max_secondary_size, text_height, int(self.menu_width * 0.6))
else:
text_width = button.text_label.sizeHint().width() + 20
max_secondary_size = max(max_secondary_size, text_width, int(self.menu_width * 0.6))
else:
max_secondary_size = max(max_secondary_size, int(self.menu_width * 0.6))
else:
# Empty button
max_secondary_size = max(max_secondary_size, int(self.menu_width * 0.4))
# Ensure minimum size
max_secondary_size = max(max_secondary_size, int(self.menu_width * 0.6))
# Second pass: apply uniform size to all buttons
for i, button in enumerate(self.buttons):
has_icon = button.icon_label is not None
if self.direction == MenuDirection.VERTICAL:
# Vertical: width = menu_width, height = max calculated
button_width = self.menu_width
button_height = max_secondary_size
else:
# Horizontal: height = menu_width, width = max calculated
button_height = self.menu_width
button_width = max_secondary_size
button.setFixedSize(QSize(button_width, button_height))
# Update icon size if it exists
if has_icon and i < len(self._original_icon_paths) and self._original_icon_paths[i]:
icon_path = self._original_icon_paths[i]
# Icon size is 40% of the smaller dimension
icon_size = int(min(button_width, button_height) * 0.4)
is_selected = button.isChecked()
color = self.selected_icon_color if is_selected else self.unselected_icon_color
colored_icon = self.apply_color_to_svg_icon(icon_path, color)
pixmap = colored_icon.pixmap(QSize(icon_size, icon_size))
button.icon_label.setPixmap(pixmap)
button.icon_label.setFixedSize(QSize(icon_size, icon_size))
def _update_all_button_sizes(self):
"""Update all button sizes when container is resized"""
if hasattr(self, '_square_buttons') and self._square_buttons:
self._recalculate_all_buttons_size()
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):
"""Deprecated: Button sizes now adapt automatically to content"""
pass
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(button, 'icon_label') 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)
# Get current icon size
current_size = button.icon_label.size()
pixmap = colored_icon.pixmap(current_size)
button.icon_label.setPixmap(pixmap)
# 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)
# Update icon color if icon_label exists
if hasattr(button, 'icon_label') 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)
current_size = button.icon_label.size()
pixmap = colored_icon.pixmap(current_size)
button.icon_label.setPixmap(pixmap)
# Apply button styling based on selection state
button.setProperty("selected", is_selected)
# Force style update
button.style().unpolish(button)
button.style().polish(button)
def update_button_text(self, index, new_text):
"""Update the text of a button at the specified index"""
if 0 <= index < len(self.buttons):
button = self.buttons[index]
if hasattr(button, 'text_label') and button.text_label is not None:
button.text_label.setText(new_text)
# Optionally, update button size after text change
self._update_button_size(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 size"""
self._update_all_button_sizes()