improvments
This commit is contained in:
parent
deb80bc9b2
commit
69891c9d6d
13
.env.example
13
.env.example
@ -1,13 +0,0 @@
|
||||
# Python Configuration
|
||||
PYTHON_PATH=C:/Path/To/Your/Python/python.exe
|
||||
|
||||
# Email configuration for suggestion system
|
||||
EMAIL_ADDRESS=your_email@gmail.com
|
||||
EMAIL_PASSWORD=your_app_password
|
||||
EMAIL_SMTP_SERVER=smtp.gmail.com
|
||||
EMAIL_SMTP_PORT=587
|
||||
|
||||
# Licensing configuration
|
||||
LICENSE_API_URL=https://your-server.com/api/licenses
|
||||
PURCHASE_URL=https://your-website.com/buy
|
||||
LICENSE_API_KEY=your_license_api_key
|
||||
@ -11,14 +11,11 @@ with config_path.open("r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
|
||||
# --- Extract values ---
|
||||
version = config.get("app_version", "0.0.0")
|
||||
arch = config.get("architecture", "x64")
|
||||
python_version = config.get("python_version", "3.x")
|
||||
os_name = config.get("app_os", sys.platform)
|
||||
app_name = config.get("app_name", "Application")
|
||||
|
||||
# --- Construct dynamic name ---
|
||||
name = f"{app_name}-{os_name}-{arch}-v{version}"
|
||||
name = f"{app_name}"
|
||||
|
||||
# --- Optional icon path ---
|
||||
icon = getenv("ICON_PATH", "")
|
||||
|
||||
12
LICENSE
12
LICENSE
@ -1,12 +0,0 @@
|
||||
Attribution License
|
||||
|
||||
Copyright (c) 2025 LouisMazin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
**Attribution Requirement:**
|
||||
Any use, reproduction, or distribution of this Software, or derivative works thereof, must include a clear and visible attribution to the original author: LouisMazin.
|
||||
|
||||
The above copyright notice, this permission notice, and the attribution requirement shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
162
README.md
162
README.md
@ -1,162 +0,0 @@
|
||||
# 🚀 Python PyQt6 Application Template
|
||||
|
||||
Bienvenue ! Ce projet est **bien plus qu'un simple template** : c'est une boîte à outils moderne pour créer rapidement des applications desktop Python élégantes, robustes et évolutives.
|
||||
Vous voulez coder, personnaliser, traduire, mettre à jour, distribuer ? Tout est déjà prêt !
|
||||
|
||||
---
|
||||
|
||||
## ✨ Fonctionnalités clés
|
||||
|
||||
- **Interface moderne (PyQt6)** : Responsive, stylée, et facile à personnaliser.
|
||||
- **Thèmes dynamiques** : Passez du clair au sombre en un clic, ou créez le vôtre !
|
||||
- **Multi-langues** : Français, anglais... et ajoutez-en autant que vous voulez.
|
||||
- **Paramètres utilisateurs** : Tout est sauvegardé (thème, langue, taille de fenêtre...).
|
||||
- **Architecture modulaire** : Des managers pour chaque besoin, tout est organisé.
|
||||
- **Notifications automatiques** : Les widgets s'adaptent instantanément aux changements.
|
||||
- **Barre d’onglets flexible** : Ajoutez vos fenêtres où vous voulez, comme vous voulez.
|
||||
- **Système de suggestion** : Vos utilisateurs peuvent vous écrire directement depuis l’app.
|
||||
- **Mise à jour automatique** : Téléchargez la dernière version sans effort, avec barre de progression !
|
||||
- **Build & environnement** : Scripts pour tout automatiser, sur Windows, Linux, macOS.
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Structure du projet
|
||||
|
||||
```
|
||||
Template/
|
||||
├── app/
|
||||
│ ├── core/ # Managers (thème, langue, update, etc.)
|
||||
│ ├── ui/
|
||||
│ │ ├── widgets/ # Onglets, loading bar, etc.
|
||||
│ │ └── windows/ # Paramètres, suggestion...
|
||||
│ └── utils/ # Fonctions utilitaires
|
||||
├── data/
|
||||
│ ├── assets/ # Icônes, images
|
||||
│ ├── lang/ # Traductions
|
||||
│ ├── themes/ # Thèmes
|
||||
│ └── others/ # Autres
|
||||
├── tools/ # Scripts build/dev
|
||||
├── config.json # Config principale
|
||||
├── requirements.txt # Dépendances
|
||||
├── BUILD.spec # PyInstaller
|
||||
└── main.py # Point d’entrée
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Démarrage Express
|
||||
|
||||
1. **Configurez `config.json`**
|
||||
(nom, version, OS, architecture, icône, dépôt git...)
|
||||
|
||||
2. **Copiez `.env.example` → `.env` et configurez les variables requises**
|
||||
- Depuis la racine du projet :
|
||||
- Windows (PowerShell / cmd) : copy .env.example .env
|
||||
- Linux / macOS : cp .env.example .env
|
||||
- Au minimum, renseignez dans `.env` :
|
||||
- PYTHON_PATH : chemin absolu vers votre exécutable Python (utilisé par tools/open.bat)
|
||||
- les identifiants email si vous comptez utiliser l'envoi de suggestions (email + mot de passe / mot de passe d'application)
|
||||
- Remarque : l'outil `tools/open.bat` s'appuie sur PYTHON_PATH ; sans cette variable, l'ouverture/initialisation de l'environnement échouera.
|
||||
|
||||
3. **Lancez le dev**
|
||||
- Windows : tools\open.bat (nécessite PYTHON_PATH dans `.env`)
|
||||
- Linux : tools/open.sh (si présent / exécutable)
|
||||
- macOS : tools/open.command (si présent / exécutable)
|
||||
- Exécutez depuis leur fichier parent pour que les chemins relatifs fonctionnent correctement.
|
||||
|
||||
4. **Build en un clic**
|
||||
- Windows : tools\build.bat
|
||||
- Linux : tools/build.sh
|
||||
- macOS : tools/build.command
|
||||
- Ces scripts supposent que `.env` est configuré et que les outils requis (pyinstaller, etc.) sont installés.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Thèmes & 🌍 Langues
|
||||
|
||||
- **Thèmes** : Ajoutez vos fichiers dans `data/themes/` (JSON).
|
||||
Changez les couleurs, créez votre ambiance !
|
||||
- **Langues** : Ajoutez vos fichiers dans `data/lang/` (JSON).
|
||||
Traduisez tout, c’est instantané.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Managers & Architecture
|
||||
|
||||
- **MainManager** : Le chef d’orchestre.
|
||||
- **SettingsManager** : Les préférences utilisateur.
|
||||
- **ThemeManager** : Les couleurs et le style.
|
||||
- **LanguageManager** : Les textes traduits.
|
||||
- **AlertManager** : Les messages, confirmations, erreurs.
|
||||
- **UpdateManager** : Les mises à jour automatiques.
|
||||
- **ObserverManager** : Les notifications internes.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Mise à jour automatique
|
||||
|
||||
- Vérifie la dernière version sur le dépôt Git à chaque démarrage.
|
||||
- Propose la mise à jour si disponible.
|
||||
- Télécharge le bon fichier selon votre OS/architecture.
|
||||
- Affiche une barre de progression stylée.
|
||||
- Lance la nouvelle version automatiquement !
|
||||
|
||||
---
|
||||
|
||||
## 💡 Suggestions & Feedback
|
||||
|
||||
- Fenêtre dédiée pour envoyer vos idées ou questions par email.
|
||||
- Sécurisé via `.env`.
|
||||
- Gestion des erreurs/succès avec AlertManager.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Ajouter vos fenêtres & widgets
|
||||
|
||||
- Créez une classe héritant de `QWidget`.
|
||||
- Ajoutez-la dans la barre d’onglets (`TabsWidget`).
|
||||
- Abonnez-vous aux notifications pour la langue/le thème.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Dépendances
|
||||
|
||||
- **Python 3.10+ recommandé** (compatibilité testée avec 3.10/3.11).
|
||||
- **PyQt6** : GUI moderne.
|
||||
- **pyinstaller** : Build d’exécutables.
|
||||
- **python-dotenv** : Variables d’environnement.
|
||||
- **requests** : Requêtes HTTP (update).
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Sécurité
|
||||
|
||||
- **Ne versionnez jamais `.env`** (déjà dans `.gitignore`).
|
||||
- Utilisez un mot de passe d’application pour Gmail.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Licence
|
||||
|
||||
Attribution License — voir le fichier local [`LICENSE`](https://gitea.louismazin.ovh/LouisMazin/PythonApplicationTemplate/src/branch/main/LICENSE) pour le texte complet.
|
||||
Toute utilisation ou distribution doit inclure une attribution visible à l'auteur : LouisMazin.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contribution
|
||||
|
||||
1. Forkez le dépôt
|
||||
2. Créez une branche
|
||||
3. Proposez vos modifications via pull request
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
- Ouvrez une issue sur le dépôt
|
||||
- Précisez votre OS, version Python, logs d’erreur
|
||||
|
||||
---
|
||||
|
||||
**Ce template est fait pour vous faire gagner du temps et coder avec plaisir !
|
||||
Testez-le, améliorez-le, partagez-le 🚀**
|
||||
@ -40,6 +40,7 @@ class ThemeManager:
|
||||
color: {self.current_theme.get_color("text_color")};
|
||||
}}
|
||||
QLabel {{
|
||||
background-color: transparent;
|
||||
color: {self.current_theme.get_color("text_color")};
|
||||
font-size: 20px;
|
||||
}}
|
||||
@ -70,7 +71,7 @@ class ThemeManager:
|
||||
border-radius: 8px;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
background-color: {self.current_theme.get_color("background_secondary_color")};
|
||||
background-color: {self.current_theme.get_color("background_tertiary_color")};
|
||||
color: {self.current_theme.get_color("text_color")};
|
||||
}}
|
||||
QLineEdit {{
|
||||
@ -78,9 +79,24 @@ class ThemeManager:
|
||||
border-radius: 8px;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
background-color: {self.current_theme.get_color("background_secondary_color")};
|
||||
background-color: {self.current_theme.get_color("background_tertiary_color")};
|
||||
color: {self.current_theme.get_color("text_color")};
|
||||
}}
|
||||
|
||||
QDateEdit {{
|
||||
border: 2px solid {self.current_theme.get_color("border_color")};
|
||||
padding: 5px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
min-height: 30px;
|
||||
}}
|
||||
QDateEdit::drop-down {{
|
||||
border: none;
|
||||
background: transparent;
|
||||
}}
|
||||
QDateEdit:hover {{
|
||||
border-color: {self.current_theme.get_color("primary_hover_color")};
|
||||
}}
|
||||
QComboBox {{
|
||||
border: 2px solid {self.current_theme.get_color("border_color")};
|
||||
padding: 5px;
|
||||
@ -117,6 +133,42 @@ class ThemeManager:
|
||||
QComboBox:hover {{
|
||||
border-color: {self.current_theme.get_color("primary_hover_color")};
|
||||
}}
|
||||
|
||||
#table_combobox {{
|
||||
border: 1px solid {self.current_theme.get_color("border_color")};
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
min-height: 20px;
|
||||
max-height: 28px;
|
||||
}}
|
||||
#table_combobox::drop-down {{
|
||||
border: none;
|
||||
background: transparent;
|
||||
}}
|
||||
#table_combobox::down-arrow {{
|
||||
image: none;
|
||||
}}
|
||||
#table_combobox QAbstractItemView {{
|
||||
border-radius: 4px;
|
||||
padding: 0px;
|
||||
outline: none;
|
||||
}}
|
||||
#table_combobox QAbstractItemView::item {{
|
||||
padding: 8px 10px;
|
||||
margin: 0px;
|
||||
min-height: 16px;
|
||||
border: 1px solid {self.current_theme.get_color("border_color")};
|
||||
border-radius: 4px;
|
||||
}}
|
||||
#table_combobox QAbstractItemView::item:hover {{
|
||||
background-color: {self.current_theme.get_color("background_tertiary_color")};
|
||||
color: {self.current_theme.get_color("text_color")};
|
||||
}}
|
||||
#table_combobox QAbstractItemView::item:selected {{
|
||||
color: {self.current_theme.get_color("text_color")};
|
||||
}}
|
||||
|
||||
QSlider::groove:horizontal {{
|
||||
border: 1px solid {self.current_theme.get_color("primary_color")};
|
||||
height: 10px;
|
||||
|
||||
@ -2,7 +2,7 @@ from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel
|
||||
from PyQt6.QtGui import QResizeEvent, QCloseEvent
|
||||
from PyQt6.QtCore import QSize, QEvent
|
||||
from app.core.main_manager import MainManager, NotificationType
|
||||
from app.ui.widgets.tabs_widget import TabsWidget, MenuDirection, ButtonPosition, BorderSide, TabSide
|
||||
from app.ui.widgets.tabs_widget import TabsWidget, MenuDirection, ButtonPosition, BorderSide, TabSide, TextPosition
|
||||
from app.ui.windows.settings_window import SettingsWindow
|
||||
from app.ui.windows.suggestion_window import SuggestionWindow
|
||||
from app.ui.windows.activation_window import ActivationWindow
|
||||
@ -20,6 +20,7 @@ class MainWindow(QMainWindow):
|
||||
self.settings_manager = self.main_manager.get_settings_manager()
|
||||
self.observer_manager = self.main_manager.get_observer_manager()
|
||||
self.observer_manager.subscribe(NotificationType.THEME, self.update_theme)
|
||||
self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_language)
|
||||
self.is_maximizing: bool = False
|
||||
|
||||
# Initialiser les attributs de taille AVANT setup_ui
|
||||
@ -33,11 +34,13 @@ class MainWindow(QMainWindow):
|
||||
self.previous_size: QSize = QSize(window_size["width"], window_size["height"])
|
||||
|
||||
# Configuration des tailles de police de référence
|
||||
self.base_width = 600 # Largeur de référence
|
||||
self.base_height = 450 # Hauteur de référence
|
||||
self.base_width = 1200 # Largeur de référence (taille par défaut)
|
||||
self.base_height = 700 # Hauteur de référence (taille par défaut)
|
||||
self.base_tab_height = 70 # Hauteur de base du tab menu
|
||||
|
||||
# Cache pour stocker les font-sizes de base de chaque widget
|
||||
self._base_font_sizes = {}
|
||||
self._font_sizes_extracted = False # Flag pour savoir si on a déjà extrait les tailles
|
||||
|
||||
# UI elements
|
||||
self.side_menu: TabsWidget
|
||||
@ -88,11 +91,11 @@ class MainWindow(QMainWindow):
|
||||
self.previous_size = self.current_size
|
||||
self.current_size = self.size()
|
||||
|
||||
# Ajuster dynamiquement les font-sizes
|
||||
# Ajuster dynamiquement les font-sizes avec un ratio
|
||||
self.adjust_all_font_sizes()
|
||||
|
||||
def adjust_all_font_sizes(self):
|
||||
"""Ajuste dynamiquement les font-sizes de tous les labels dans toutes les tabs"""
|
||||
"""Ajuste dynamiquement les font-sizes de tous les éléments avec un ratio proportionnel"""
|
||||
# Calculer le ratio basé sur la largeur ET la hauteur actuelle
|
||||
current_width = self.width()
|
||||
current_height = self.height()
|
||||
@ -101,78 +104,131 @@ class MainWindow(QMainWindow):
|
||||
width_ratio = current_width / self.base_width
|
||||
height_ratio = current_height / self.base_height
|
||||
|
||||
# Prendre le ratio le plus petit pour éviter que le texte dépasse
|
||||
ratio = min(width_ratio, height_ratio)
|
||||
# Utiliser la moyenne des deux ratios pour un scaling plus naturel
|
||||
# Ou utiliser le minimum pour éviter le débordement
|
||||
ratio = min(width_ratio,height_ratio) * 1.5
|
||||
|
||||
# Limiter le ratio pour éviter des tailles extrêmes
|
||||
ratio = max(0.5, min(ratio, 2.0)) # Entre 50% et 200%
|
||||
|
||||
# Récupérer tous les widgets des tabs
|
||||
all_widgets = []
|
||||
if hasattr(self, 'side_menu'):
|
||||
all_widgets = self.side_menu.widgets
|
||||
|
||||
# Parcourir tous les widgets et ajuster leurs labels
|
||||
# Extraire les tailles de base une seule fois
|
||||
if not self._font_sizes_extracted:
|
||||
self._extract_base_font_sizes(all_widgets)
|
||||
self._font_sizes_extracted = True
|
||||
|
||||
# Parcourir tous les widgets et ajuster leurs tailles
|
||||
for widget in all_widgets:
|
||||
if widget:
|
||||
self._adjust_widget_labels(widget, ratio)
|
||||
self._adjust_widget_font_sizes(widget, ratio)
|
||||
|
||||
def _adjust_widget_labels(self, widget, ratio):
|
||||
"""Ajuste récursivement tous les QLabel, QPushButton, QLineEdit, QTextEdit et QComboBox d'un widget"""
|
||||
def _extract_base_font_sizes(self, widgets):
|
||||
"""Extrait les tailles de police de base de tous les widgets une seule fois"""
|
||||
from PyQt6.QtWidgets import QPushButton, QLineEdit, QTextEdit, QComboBox
|
||||
|
||||
# Types de widgets à ajuster
|
||||
widget_types = [QLabel, QPushButton, QLineEdit, QTextEdit, QComboBox]
|
||||
font_size_dict = self.extract_base_font_size()
|
||||
# Extraire les tailles des boutons d'onglets du side menu
|
||||
if hasattr(self, 'side_menu') and hasattr(self.side_menu, 'buttons'):
|
||||
for button in self.side_menu.buttons:
|
||||
if button:
|
||||
widget_id = id(button)
|
||||
current_style = button.styleSheet()
|
||||
base_size = self._extract_font_size_from_style(current_style)
|
||||
if base_size is None:
|
||||
base_size = 14 # Taille par défaut
|
||||
self._base_font_sizes[widget_id] = base_size
|
||||
|
||||
for widget in widgets:
|
||||
if not widget:
|
||||
continue
|
||||
|
||||
for widget_type in widget_types:
|
||||
for child in widget.findChildren(widget_type):
|
||||
widget_id = id(child)
|
||||
|
||||
# Ignorer les widgets avec un objectName (généralement stylisés spécifiquement)
|
||||
if child.objectName() != "":
|
||||
continue
|
||||
|
||||
# Extraire la taille de police depuis le stylesheet
|
||||
current_style = child.styleSheet()
|
||||
base_size = self._extract_font_size_from_style(current_style)
|
||||
|
||||
# Si pas trouvé dans le style, utiliser la taille par défaut
|
||||
if base_size is None:
|
||||
base_size = 14 # Taille par défaut
|
||||
|
||||
# Stocker la taille de base
|
||||
self._base_font_sizes[widget_id] = base_size
|
||||
|
||||
def _extract_font_size_from_style(self, style: str) -> Optional[int]:
|
||||
"""Extrait la taille de police depuis un stylesheet"""
|
||||
import re
|
||||
|
||||
# Chercher "font-size: XXpx"
|
||||
match = re.search(r'font-size:\s*(\d+)px', style)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
|
||||
return None
|
||||
|
||||
def _adjust_widget_font_sizes(self, widget, ratio):
|
||||
"""Ajuste les font-sizes de tous les éléments d'un widget avec un ratio proportionnel"""
|
||||
from PyQt6.QtWidgets import QPushButton, QLineEdit, QTextEdit, QComboBox
|
||||
import re
|
||||
|
||||
# Ajuster les boutons d'onglets du side menu
|
||||
if hasattr(self, 'side_menu') and hasattr(self.side_menu, 'buttons'):
|
||||
for button in self.side_menu.buttons:
|
||||
if button:
|
||||
widget_id = id(button)
|
||||
if widget_id in self._base_font_sizes:
|
||||
base_size = self._base_font_sizes[widget_id]
|
||||
new_size = max(8, int(base_size * ratio))
|
||||
current_style = button.styleSheet()
|
||||
style_without_font = re.sub(r'font-size:\s*\d+px;?', '', current_style)
|
||||
style_without_font = re.sub(r';+', ';', style_without_font)
|
||||
style_without_font = style_without_font.strip()
|
||||
if style_without_font and not style_without_font.endswith(';'):
|
||||
style_without_font += ';'
|
||||
new_style = f"{style_without_font} font-size: {new_size}px;"
|
||||
button.setStyleSheet(new_style)
|
||||
|
||||
widget_types = [QLabel, QPushButton, QLineEdit, QTextEdit, QComboBox]
|
||||
|
||||
for widget_type in widget_types:
|
||||
for child in widget.findChildren(widget_type):
|
||||
# Obtenir l'identifiant unique du widget
|
||||
widget_id = id(child)
|
||||
|
||||
# Si c'est la première fois qu'on voit ce widget, extraire sa font-size de base
|
||||
# Récupérer la taille de base
|
||||
if widget_id not in self._base_font_sizes:
|
||||
base_size = font_size_dict.get(child.__class__.__name__, 14)
|
||||
self._base_font_sizes[widget_id] = base_size
|
||||
else:
|
||||
base_size = self._base_font_sizes[widget_id]
|
||||
continue # Pas de taille de base, ignorer
|
||||
|
||||
# Calculer la nouvelle taille
|
||||
new_size = int(base_size * ratio)
|
||||
base_size = self._base_font_sizes[widget_id]
|
||||
|
||||
# Calculer la nouvelle taille avec le ratio
|
||||
new_size = max(8, int(base_size * ratio)) # Minimum 8px
|
||||
|
||||
# Appliquer le style (en préservant les autres styles existants)
|
||||
# Appliquer le style en préservant les autres propriétés
|
||||
current_style = child.styleSheet()
|
||||
# Retirer l'ancienne font-size si elle existe
|
||||
style_parts = [s.strip() for s in current_style.split(';') if s.strip()]
|
||||
style_parts = [s for s in style_parts if not s.startswith('font-size')]
|
||||
|
||||
# Retirer l'ancienne font-size
|
||||
style_without_font = re.sub(r'font-size:\s*\d+px;?', '', current_style)
|
||||
|
||||
# Nettoyer les points-virgules multiples
|
||||
style_without_font = re.sub(r';+', ';', style_without_font)
|
||||
style_without_font = style_without_font.strip()
|
||||
|
||||
# Ajouter la nouvelle font-size
|
||||
style_parts.append(f'font-size: {new_size}px')
|
||||
if style_without_font and not style_without_font.endswith(';'):
|
||||
style_without_font += ';'
|
||||
|
||||
new_style = '; '.join(style_parts)
|
||||
new_style = f"{style_without_font} font-size: {new_size}px;"
|
||||
child.setStyleSheet(new_style)
|
||||
|
||||
def extract_base_font_size(self) -> dict:
|
||||
"""Extrait la font-size de base d'un widget depuis son stylesheet"""
|
||||
base_font_sizes = {}
|
||||
try:
|
||||
style = self.theme_manager.get_sheet()
|
||||
|
||||
# Chercher "font-size: XXpx" dans le style, puis chercher à quel widget cela correspond
|
||||
lines = style.splitlines()
|
||||
component = None
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith("font-size:"):
|
||||
size_part = line.split(":")[1].strip().rstrip(";")
|
||||
if size_part.endswith("px"):
|
||||
size_value = int(size_part[:-2])
|
||||
base_font_sizes[component] = size_value
|
||||
elif line.startswith("Q"):
|
||||
component = line.split("{")[0].strip()
|
||||
return base_font_sizes
|
||||
|
||||
|
||||
except Exception:
|
||||
# En cas d'erreur, retourner une valeur par défaut
|
||||
return {}
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None:
|
||||
"""Handle application close event"""
|
||||
@ -191,20 +247,31 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def setup_ui(self) -> None:
|
||||
|
||||
self.side_menu = TabsWidget(self, MenuDirection.HORIZONTAL, 70, None, 10, 1, BorderSide.BOTTOM, TabSide.TOP)
|
||||
self.side_menu = TabsWidget(self, MenuDirection.HORIZONTAL, 70, None, 10, BorderSide.BOTTOM, TabSide.TOP)
|
||||
|
||||
self.suggestion_window = SuggestionWindow(self)
|
||||
self.side_menu.add_widget(self.suggestion_window, "", paths.get_asset_svg_path("suggestion"), position=ButtonPosition.CENTER)
|
||||
|
||||
self.side_menu.add_widget(self.suggestion_window, self.language_manager.get_text("tab_suggestions"), paths.get_asset_svg_path("suggestion"), position=ButtonPosition.CENTER, text_position=TextPosition.BOTTOM)
|
||||
|
||||
self.settings_window = SettingsWindow(self)
|
||||
self.side_menu.add_widget(self.settings_window, "", paths.get_asset_svg_path("settings"), position=ButtonPosition.CENTER)
|
||||
|
||||
self.side_menu.add_widget(self.settings_window, self.language_manager.get_text("tab_settings"), paths.get_asset_svg_path("settings"), position=ButtonPosition.CENTER, text_position=TextPosition.BOTTOM)
|
||||
|
||||
# Ajouter la tab d'activation uniquement si le système de licence est activé
|
||||
if self.settings_manager.get_config("enable_licensing"):
|
||||
self.activation_window = ActivationWindow(self)
|
||||
self.side_menu.add_widget(self.activation_window, "", paths.get_asset_svg_path("license"), position=ButtonPosition.END)
|
||||
self.side_menu.add_widget(self.activation_window, self.language_manager.get_text("tab_licensing"), paths.get_asset_svg_path("license"), position=ButtonPosition.END, text_position=TextPosition.BOTTOM)
|
||||
|
||||
self.setCentralWidget(self.side_menu)
|
||||
|
||||
def get_tab_widget(self):
|
||||
"""Retourne le widget TabsWidget pour permettre le changement d'onglet"""
|
||||
return self.side_menu
|
||||
|
||||
def update_theme(self) -> None:
|
||||
self.setStyleSheet(self.theme_manager.get_sheet())
|
||||
self.setStyleSheet(self.theme_manager.get_sheet())
|
||||
|
||||
def update_language(self) -> None:
|
||||
# Mettre à jour les textes des onglets
|
||||
self.side_menu.update_button_text(0, self.language_manager.get_text("tab_suggestions"))
|
||||
self.side_menu.update_button_text(1, self.language_manager.get_text("tab_settings"))
|
||||
if self.settings_manager.get_config("enable_licensing"):
|
||||
self.side_menu.update_button_text(2, self.language_manager.get_text("tab_licensing"))
|
||||
@ -1,4 +1,4 @@
|
||||
from PyQt6.QtWidgets import QLayout, QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QStackedWidget, QSizePolicy, QSpacerItem
|
||||
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
|
||||
@ -25,6 +25,12 @@ class BorderSide(Enum):
|
||||
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)
|
||||
@ -32,7 +38,7 @@ class TabSide(Enum):
|
||||
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):
|
||||
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()
|
||||
@ -40,8 +46,8 @@ class TabsWidget(QWidget):
|
||||
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
|
||||
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):
|
||||
@ -60,7 +66,7 @@ class TabsWidget(QWidget):
|
||||
self.buttons = []
|
||||
self.widgets = []
|
||||
self.button_positions = []
|
||||
self.button_size_ratios = [] # Individual ratios for each button
|
||||
self.button_text_positions = [] # Individual text positions for each button
|
||||
self._icon_cache = {}
|
||||
self._original_icon_paths = []
|
||||
self._square_buttons = []
|
||||
@ -184,43 +190,40 @@ class TabsWidget(QWidget):
|
||||
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):
|
||||
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"""
|
||||
# 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)
|
||||
# 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
|
||||
|
||||
button.setCheckable(True)
|
||||
# Create button container with custom layout
|
||||
button_container = self._create_button_with_layout(button_text, icon_path, btn_text_position)
|
||||
|
||||
# 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)
|
||||
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)
|
||||
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)
|
||||
self.buttons.append(button_container)
|
||||
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))
|
||||
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, position, after_button_index)
|
||||
self._insert_button_with_alignment(button_container, position, after_button_index)
|
||||
|
||||
# Select first tab by default
|
||||
if len(self.buttons) == 1:
|
||||
@ -228,6 +231,127 @@ class TabsWidget(QWidget):
|
||||
|
||||
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)
|
||||
@ -250,10 +374,25 @@ class TabsWidget(QWidget):
|
||||
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))
|
||||
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
|
||||
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))
|
||||
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)
|
||||
|
||||
@ -299,32 +438,97 @@ class TabsWidget(QWidget):
|
||||
"""
|
||||
|
||||
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
|
||||
"""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
|
||||
|
||||
if button_index == -1:
|
||||
return # Button not found
|
||||
max_secondary_size = 0
|
||||
|
||||
# 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
|
||||
# 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))
|
||||
|
||||
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))
|
||||
# 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'):
|
||||
for button in self._square_buttons:
|
||||
if button.parent(): # Check if button still exists
|
||||
self._update_button_size(button)
|
||||
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"""
|
||||
@ -388,23 +592,8 @@ class TabsWidget(QWidget):
|
||||
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()
|
||||
"""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)"""
|
||||
@ -419,13 +608,17 @@ class TabsWidget(QWidget):
|
||||
button.setChecked(is_selected)
|
||||
|
||||
# Update icon color based on selection
|
||||
if hasattr(self, '_original_icon_paths') and i < len(self._original_icon_paths):
|
||||
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)
|
||||
button.setIcon(colored_icon)
|
||||
|
||||
# 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)
|
||||
@ -456,19 +649,18 @@ class TabsWidget(QWidget):
|
||||
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
|
||||
# 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)
|
||||
button.setIcon(colored_icon)
|
||||
# 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)
|
||||
@ -476,7 +668,16 @@ class TabsWidget(QWidget):
|
||||
# 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)
|
||||
@ -555,9 +756,5 @@ class TabsWidget(QWidget):
|
||||
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))
|
||||
"""Update all buttons size"""
|
||||
self._update_all_button_sizes()
|
||||
@ -29,7 +29,7 @@ class SplashScreen(QWidget):
|
||||
# Configuration de la fenêtre
|
||||
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.setFixedSize(800, 600)
|
||||
self.setFixedSize(2000, 2000)
|
||||
|
||||
# Layout principal
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
@ -60,5 +60,8 @@
|
||||
"cancel": "Cancel",
|
||||
"activation_required": "Activation is required to continue.",
|
||||
"compare_versions": "Compare Versions",
|
||||
"no_license": "No License"
|
||||
"no_license": "No License",
|
||||
"tab_suggestions": "Suggestions",
|
||||
"tab_settings": "Settings",
|
||||
"tab_licensing": "Licensing"
|
||||
}
|
||||
@ -61,5 +61,8 @@
|
||||
"cancel": "Annuler",
|
||||
"activation_required": "L'activation est requise pour continuer.",
|
||||
"compare_versions": "Comparer les versions",
|
||||
"no_license": "Pas de licence"
|
||||
"no_license": "Pas de licence",
|
||||
"tab_suggestions": "Suggestions",
|
||||
"tab_settings": "Paramètres",
|
||||
"tab_licensing": "Licence"
|
||||
}
|
||||
@ -4,6 +4,7 @@ setlocal enabledelayedexpansion
|
||||
REM === PATH SETUP ===
|
||||
set PARENT_DIR=%~dp0..
|
||||
set CONFIG_FILE=%PARENT_DIR%\config.json
|
||||
set ICON_FILE=%PARENT_DIR%\data\assets\icon.png
|
||||
set ENV_FILE=%PARENT_DIR%\.env
|
||||
|
||||
REM Check if .env file exists
|
||||
@ -24,8 +25,10 @@ REM === Extract python path from .env file ===
|
||||
for /f "usebackq tokens=2 delims==" %%i in (`findstr "PYTHON_PATH" "%ENV_FILE%"`) do set SYSTEM_PYTHON=%%i
|
||||
|
||||
set VENV_PATH=%PARENT_DIR%\WINenv_%ARCHITECTURE%
|
||||
set EXE_NAME=%APP_NAME%-Windows-%ARCHITECTURE%
|
||||
set EXE_NAME=%APP_NAME%.exe
|
||||
set PYTHON_IN_VENV=%VENV_PATH%\Scripts\python.exe
|
||||
set BUILD_DIR=%PARENT_DIR%\build
|
||||
set ZIP_FILE=%BUILD_DIR%\%APP_NAME%.zip
|
||||
|
||||
REM === Verify Python existence ===
|
||||
if not exist "%SYSTEM_PYTHON%" (
|
||||
@ -45,12 +48,44 @@ if not exist "%VENV_PATH%\Scripts\activate.bat" (
|
||||
|
||||
REM === Run PyInstaller ===
|
||||
"%PYTHON_IN_VENV%" -m PyInstaller ^
|
||||
--distpath "%PARENT_DIR%\build" ^
|
||||
--workpath "%PARENT_DIR%\build\dist" ^
|
||||
--distpath "%BUILD_DIR%" ^
|
||||
--workpath "%BUILD_DIR%\dist" ^
|
||||
--clean ^
|
||||
"%PARENT_DIR%\BUILD.spec"
|
||||
|
||||
REM === Clean build cache ===
|
||||
rmdir /s /q "%PARENT_DIR%\build\dist"
|
||||
rmdir /s /q "%BUILD_DIR%\dist"
|
||||
|
||||
REM === Create ZIP ===
|
||||
echo [INFO] Creating ZIP archive...
|
||||
|
||||
set TEMP_ZIP_DIR=%BUILD_DIR%\temp_zip
|
||||
|
||||
REM Remove old temp dir if exists
|
||||
if exist "%TEMP_ZIP_DIR%" rmdir /s /q "%TEMP_ZIP_DIR%"
|
||||
mkdir "%TEMP_ZIP_DIR%"
|
||||
|
||||
REM Copy compiled app - tout le contenu du build sauf les ZIP existants
|
||||
move /Y "%BUILD_DIR%\%EXE_NAME%" "%TEMP_ZIP_DIR%\"
|
||||
|
||||
REM Copy config.json
|
||||
copy "%CONFIG_FILE%" "%TEMP_ZIP_DIR%\" /Y
|
||||
|
||||
REM Copy icon.png
|
||||
copy "%ICON_FILE%" "%TEMP_ZIP_DIR%\" /Y
|
||||
|
||||
REM Copy data/lang
|
||||
xcopy /E /I /Y "%PARENT_DIR%\data\lang" "%TEMP_ZIP_DIR%\lang"
|
||||
|
||||
REM Remove old ZIP if exists
|
||||
if exist "%ZIP_FILE%" del "%ZIP_FILE%"
|
||||
|
||||
REM Create ZIP
|
||||
powershell -NoProfile -Command "Add-Type -AssemblyName 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::CreateFromDirectory('%TEMP_ZIP_DIR%', '%ZIP_FILE%')"
|
||||
|
||||
REM Remove temp folder
|
||||
rmdir /s /q "%TEMP_ZIP_DIR%"
|
||||
|
||||
echo [INFO] ZIP created at: %ZIP_FILE%
|
||||
|
||||
endlocal
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# === PATH SETUP ===
|
||||
PARENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CONFIG_FILE="$PARENT_DIR/config.json"
|
||||
ENV_FILE="$PARENT_DIR/.env"
|
||||
|
||||
# --- Check .env file ---
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "[ERROR] .env file not found. Please copy .env.example to .env and configure it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Check jq availability ---
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo "[ERROR] 'jq' is required. Install it with: brew install jq"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Extract values from config.json ---
|
||||
ICON_PATH=$(jq -r '.icon_path' "$CONFIG_FILE")
|
||||
APP_NAME=$(jq -r '.app_name' "$CONFIG_FILE")
|
||||
ARCHITECTURE=$(jq -r '.architecture' "$CONFIG_FILE")
|
||||
|
||||
# --- Extract PYTHON_PATH from .env ---
|
||||
SYSTEM_PYTHON=$(grep -E "^PYTHON_PATH=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
|
||||
VENV_PATH="$PARENT_DIR/MACenv_$ARCHITECTURE"
|
||||
EXE_NAME="${APP_NAME}-MacOS-${ARCHITECTURE}"
|
||||
PYTHON_IN_VENV="$VENV_PATH/bin/python"
|
||||
|
||||
# --- Verify Python existence ---
|
||||
if [[ ! -x "$SYSTEM_PYTHON" ]]; then
|
||||
echo "[ERROR] Python not found at: $SYSTEM_PYTHON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Check if virtual environment exists ---
|
||||
if [[ ! -f "$VENV_PATH/bin/activate" ]]; then
|
||||
echo "[INFO] Virtual environment not found. Creating..."
|
||||
"$SYSTEM_PYTHON" -m venv "$VENV_PATH"
|
||||
"$PYTHON_IN_VENV" -m pip install --upgrade pip
|
||||
"$PYTHON_IN_VENV" -m pip install -r "$PARENT_DIR/requirements.txt"
|
||||
else
|
||||
echo "[INFO] Virtual environment found."
|
||||
fi
|
||||
|
||||
# --- Run PyInstaller ---
|
||||
"$PYTHON_IN_VENV" -m PyInstaller \
|
||||
--distpath "$PARENT_DIR/build" \
|
||||
--workpath "$PARENT_DIR/build/dist" \
|
||||
--clean \
|
||||
"$PARENT_DIR/BUILD.spec"
|
||||
|
||||
# --- Clean build cache ---
|
||||
rm -rf "$PARENT_DIR/build/dist"
|
||||
|
||||
echo "[INFO] Build complete: $EXE_NAME"
|
||||
@ -1,59 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Root paths
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CONFIG_FILE="$ROOT_DIR/config.json"
|
||||
REQUIREMENTS="$ROOT_DIR/requirements.txt"
|
||||
ENV_FILE="$ROOT_DIR/.env"
|
||||
|
||||
# Check if .env exists
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "[ERROR] .env file not found. Please copy .env.example to .env and configure it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract architecture from config.json (requires jq)
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo "[ERROR] 'jq' is required. Install it with: brew install jq"
|
||||
exit 1
|
||||
fi
|
||||
ARCHITECTURE=$(jq -r '.architecture' "$CONFIG_FILE")
|
||||
|
||||
# Extract python path from .env
|
||||
PYTHON_EXEC=$(grep -E "^PYTHON_PATH=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
|
||||
# Construct venv path
|
||||
ENV_NAME="MACenv_$ARCHITECTURE"
|
||||
ENV_PATH="$ROOT_DIR/$ENV_NAME"
|
||||
|
||||
# Check python executable
|
||||
if [[ ! -x "$PYTHON_EXEC" ]]; then
|
||||
echo "[ERROR] Python not found at: $PYTHON_EXEC"
|
||||
echo "Please check your .env or installation path."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show info
|
||||
echo "[INFO] Configuration:"
|
||||
echo " Python: $PYTHON_EXEC"
|
||||
echo " Env: $ENV_NAME"
|
||||
|
||||
# Create virtual env if missing
|
||||
if [[ ! -d "$ENV_PATH/bin" ]]; then
|
||||
echo "[INFO] Virtual environment not found, creating..."
|
||||
"$PYTHON_EXEC" -m venv "$ENV_PATH"
|
||||
echo "[INFO] Installing dependencies..."
|
||||
"$ENV_PATH/bin/python" -m pip install --upgrade pip
|
||||
"$ENV_PATH/bin/pip" install -r "$REQUIREMENTS"
|
||||
else
|
||||
echo "[INFO] Virtual environment found."
|
||||
fi
|
||||
|
||||
# Activate and launch VS Code
|
||||
echo "[INFO] Launching VS Code..."
|
||||
# shellcheck source=/dev/null
|
||||
source "$ENV_PATH/bin/activate"
|
||||
open -a "Visual Studio Code" "$ROOT_DIR"
|
||||
|
||||
exit 0
|
||||
Loading…
x
Reference in New Issue
Block a user