What changed, straight from the code
This isn't a marketing page: it's the actual CHANGELOG.md shipped inside the downloadable zip, read automatically on every visit — never retyped by hand, so it never drifts out of sync.
Current version : v1.4.0 Download this version
[0.1.2] - 2026-07-24
Ajouté/Corrigé (2026-07-24, icônes + clic droit + Suppr + texte riche + style Fusion)
- Icônes dessinées nous-mêmes (ui/icons.py, QPainter pur, aucun fichier
externe) pour Annuler/Rétablir/Copier/Coller/Dupliquer/Pinceau/Ajuster/
Supprimer — jusqu'ici du texte seul dans la toolbar de MainWindow et les
boutons de SlideTemplateEditorDialog, peu visuel (retour utilisateur,
capture d'écran). Rendu vérifié visuellement (export PNG) avant câblage.
- Menu contextuel (clic droit) sur le canevas, dans les DEUX éditeurs :
ui/element_context_menu.py (nouveau module partagé, mêmes actions que la
toolbar) + CanvasView.contextMenuEvent (sélectionne l'élément sous le
curseur s'il n'est pas déjà dans la sélection courante, préserve une
sélection multiple existante). build_element_context_menu séparé de
show_element_context_menu : QMenu.exec() est une méthode C++ liée
(PySide6/shiboken) qu'un monkeypatch Python n'intercepte pas de façon
fiable — la séparation permet de tester la CONSTRUCTION du menu sans jamais
appeler exec() (qui bloquerait indéfiniment en test).
- Touche Suppr pour supprimer rapidement le/les élément(s) sélectionné(s)
(_delete_selected, menu Édition + toolbar + clic droit dans les deux
éditeurs) — jusqu'ici AUCUN moyen de supprimer un élément nulle part dans
l'appli. Respecte le verrouillage (locked) : un élément verrouillé est
toujours exclu de la suppression, même au sein d'une sélection mixte.
- Mise en forme riche du texte SÉLECTIONNÉ (retour utilisateur : définir
manuellement police/taille/couleur sur une PARTIE du texte, pas toute la
zone) : l'éditeur de texte inline du canevas (double-clic sur un textbox/
une forme, PARTAGÉ entre les deux éditeurs via CanvasView) passe de
QPlainTextEdit à un QTextEdit riche, avec une mini barre flottante
(ui/rich_text_toolbar.py : police, taille en pt manuelle, couleur, gras/
italique/souligné) qui applique la mise en forme à la SÉLECTION courante via
QTextCursor.mergeCharFormat. render/richtext.py (nouveau, logique pure)
fait la conversion paragraphs/runs ↔ QTextDocument : chaque fragment porte,
en plus de son apparence RÉSOLUE (WYSIWYG), 3 propriétés custom mémorisant
la valeur EXPLICITE d'origine du run — le texte NON retouché par
l'utilisateur continue de suivre la charte si elle change plus tard (jamais
de mise en forme héritée figée en dur par accident). Validé par un
round-trip : appliquer une taille/couleur sur UNE portion de "Hello World"
ne touche que cette portion, le reste reste inchangé. Piège corrigé :
la mini-barre ouvre un sélecteur de couleur MODAL (QColorDialog) — sans
précaution, la perte de focus qui en résulte aurait été interprétée à tort
par l'éditeur comme "l'utilisateur a cliqué ailleurs", validant/fermant
l'édition en cours pendant que la boîte de dialogue est encore ouverte ;
neutralisé via un drapeau is_busy sur la barre, vérifié par l'éditeur
avant de valider sur perte de focus. Portée : textbox et shape (les 2 seuls
types avec paragraphs dans le schéma) ; les cellules de tableau (runs
par cellule, mais éditées via une grille QTableWidget sans notion de texte
riche) sont restées hors périmètre — refonte structurellement différente,
disproportionnée par rapport au reste de cette vague.
- Bug réel corrigé (capture d'écran) — flèches +/- des QSpinBox/
QDoubleSpinBox en simples rectangles bleus PLEINS, malgré le QSS
::up-arrow/::down-arrow déjà en place : root cause — sans style Qt
explicite, Windows utilise son style natif (windowsvista/windows11), qui ne
respecte pas fidèlement le QSS des sous-contrôles complexes. ui/style.py:: force désormais
apply_styleQApplication.setStyle(QStyleFactory. avant d'appliquer la feuille de style — seul style
create("Fusion"))
garantissant un support QSS complet (documenté par Qt lui-même), cohérent
avec l'ambition déjà affichée d'un rendu plat/moderne entièrement piloté
par ce QSS.
- 2 bugs d'infrastructure de test réels trouvés et corrigés en creusant un
crash natif intermittent ("Windows fatal exception: access violation") :
(1) prefs.py met en cache le contenu de prefs.json dans un dict
MODULE-NIVEAU jamais invalidé — tests/test_image_gen.py y écrit une
config OpenAI FACTICE mais "configurée" (openai_url="https://x/..."), qui
reste active pour TOUT LE RESTE de la session pytest ; un SpellcheckTextEdit
créé par un test ultérieur (ex via SlideTemplateEditorDialog) voit alors
ai_settings.is_configured() répondre vrai à tort, et son minuteur de
debounce déclenche un VRAI appel réseau bloqué en résolution DNS — fixé par
un autouse fixture (tests/conftest.py::_reset_prefs_cache) qui réinitialise
prefs._cache avant/après CHAQUE test. (2) CanvasView n'était pas suivi
comme MainWindow l'est déjà (_live_instances + nettoyage de fin de test)
— étendu par symétrie (ui/canvas_view.py::_live_instances/
disconnect_scene(), tests/conftest.py::_qt_worker_teardown élargi).
Statut honnête : un résidu de cette classe de bug (déjà documenté
PRÉ-EXISTANT, non résolu, cf. entrée du 2026-07-22) subsiste, concentré sur
tests/test_spinner_animation.py::test_spinner_phase_advances_while_running/
test_spinner_can_restart_after_stop_without_freezing (les 2 seuls tests
de la suite à exécuter une VRAIE boucle d'évènements Qt de 200ms, la
fenêtre la plus large pour livrer un signal en file d'attente d'un objet
déjà hors de portée) — 729 tests passent PROPREMENT sur 2 runs complets
consécutifs en excluant CES 2 tests précis ; la suite COMPLÈTE (731 tests)
reste intermittente, cohérent avec le ticket déjà ouvert.
Ajouté (2026-07-24, copier/coller d'éléments + pinceau de mise en forme)
- Copier / Coller / Dupliquer un ou plusieurs éléments (menu Édition +
toolbar de MainWindow, boutons dans SlideTemplateEditorDialog — Ctrl+C/
Ctrl+V/Ctrl+D) : jusqu'ici aucun moyen de dupliquer rapidement un élément
(ni même de le supprimer, d'ailleurs — signalé séparément). core/deck.py:: régénère TOUJOURS id/group (contrairement à
duplicate_elements
sanitize_element_ids, qui ne touche jamais un id déjà valide — une copie
DOIT avoir un id différent de l'original, sinon collision), et décale
légèrement la position (+3%, feedback visuel immédiat façon PowerPoint).
Fonctionne sur une sélection multiple (rubber-band déjà supporté par
CanvasView, mais jamais exploité jusqu'ici) via 2 nouvelles méthodes
CanvasView.selected_ids()/select_elements().
- Presse-papiers PARTAGÉ entre les deux éditeurs (ui/element_clipboard.py,
module-niveau, PAS le presse-papiers système) : copier un élément dans
MainWindow puis le coller dans « Modèle de mise en forme de la slide »
fonctionne (et inversement), pendant la même session.
- Pinceau (copier la mise en forme) : bouton/action checkable — sélectionner
UN élément, activer le pinceau, cliquer sur un AUTRE élément pour lui
appliquer sa mise en forme (taille, contour, ombre, opacité, couleur de
fond, et pour du texte police/taille/couleur/gras/italique/soulignage/
alignement). core/deck.py::copy_style fait un remplacement COMPLET (pas
une fusion) des champs de style — un contour que la cible avait mais pas la
source disparaît, comme le pinceau PowerPoint/Word — et ne touche JAMAIS id/
type/position/rôle/nom/texte réel/shape_kind/contenu structurel (lignes de
tableau, séries de graphique). Piège évité : un size_pt résiduel sur
la cible aurait sinon primé silencieusement sur le size_ref fraîchement
copié (core/theme.py::resolve_size_pt donne explicitement priorité à
size_pt) — chaque champ de run est donc soit copié soit explicitement
retiré, jamais laissé tel quel.
- 691 tests pytest passent (2 runs complets consécutifs, ~91s chacun).
Ajouté/Corrigé (2026-07-24, éditeur de modèle de type de slide : titre/description, auto-ajustement, taille de fenêtre, assistant IA)
- Titre et description d'usage éditables directement dans « Modèle de mise en
forme de la slide » (ui/slide_template_editor_dialog.py) : jusqu'ici ces
deux champs ne se modifiaient que via le menu de l'éditeur de charte
(QInputDialog). Ce sont pourtant eux que l'IA lit dans son catalogue de types
(generate/prompts.py::_slide_type_catalog) pour choisir le modèle le plus
adapté à chaque slide — les avoir sous les yeux pendant qu'on met en forme le
modèle est bien plus naturel. Répercutés dans theme.slide_types[] par
ui/theme_editor.py::_edit_slide_type_template (un titre vidé par erreur
retombe sur l'ancien pour ne jamais perdre le nom du type).
- Auto-ajustement du canevas à l'ouverture : le fit fait à la construction
utilisait une taille de viewport encore incorrecte (fenêtre pas encore
affichée/mise en page) — la slide n'occupait pas toute la place. Refait UNE
fois au showEvent, différé après la mise en page réelle (QTimer.singleShot),
sans écraser un zoom choisi ensuite par l'utilisateur.
- Bug réel corrigé — la taille de la fenêtre n'était pas conservée en passant
d'un type de slide à un autre depuis le sélecteur : le switch RECRÉE le
dialogue (il s'auto-enregistre puis se rouvre sur l'autre type), or la taille
était remise en dur à 1180×760 à chaque construction. La géométrie du dialogue
précédent est désormais transmise (start_geometry → restoreGeometry) et
restaurée à la réouverture.
- Assistant IA conversationnel dans le modèle (panneau repliable « Assistant
IA » en bas du dialogue) : un prompt en langage naturel + des documents joints
(Excel/PDF/PPTX/images…, ingérés via generate/ingest.py) pour améliorer
directement la mise en forme du modèle. Réutilise TEL QUEL le pipeline éprouvé
d'édition d'une slide (services/edit_service.edit_by_prompt) en enveloppant
le modèle dans un deck synthétique d'UNE slide conforme au schéma (id de slide
généré via core/ids.slide_id()), avec capture d'aperçu pour la vision,
mémoire de conversation d'un tour à l'autre (ai/conversation.Conversation),
question de clarification possible, aperçu avant/après (accepter/rejeter via
DiffPreviewDialog) et bouton d'interruption. Champs de texte (description +
prompt) enrichis de la détection d'erreurs d'écriture (SpellcheckTextEdit).
Ajouté/Corrigé (2026-07-23, journalisation des crashs + niveaux d'éléments + image de fond)
- Garantie de journalisation de tout crash applicatif (crash_reporting.py,
nouveau, installé au tout début de app.py::main()) : jusqu'ici AUCUN
mécanisme ne capturait un crash — ni sys.excepthook (une exception Python
non interceptée dans un slot Qt est journalisée puis le PROCESSUS SE TERMINE,
invisible sous pythonw sans console), ni faulthandler (seul mécanisme
capable de capturer un crash NATIF bas niveau — access violation, etc. — le
même que le message "Windows fatal exception" déjà vu pendant les tests).
Les 3 filets sont désormais actifs : sys.excepthook personnalisé (journalise
puis délègue au comportement par défaut), faulthandler.enable() ciblant un
fichier dédié logs/crash.log (jamais stderr, indisponible sous pythonw),
et un handler de messages Qt (avertissements/erreurs internes, souvent un
signal précurseur utile). N'empêche jamais le démarrage : chaque étape est
installée dans son propre try/except.
- Bug réel corrigé — les boutons de niveau (Arrière/Reculer/Avancer/Avant)
semblaient ne rien faire juste après avoir inséré un élément (capture
d'écran) : root cause — canvas.select_element() bloque VOLONTAIREMENT les
signaux de la scène (pour ne jamais redéclencher element_selected en
boucle), mais _insert_element() (ui/main_window.py ET ui/) s'appuyait dessus comme SEUL mécanisme de
slide_template_editor_dialog.py
sélection après une insertion — le panneau de propriétés (qui alimente les
boutons de niveau) restait donc sur l'ANCIENNE sélection (ou aucune),
ciblant le mauvais élément ou ne réagissant pas du tout. Fix : appel
explicite à _on_element_selected() juste après l'insertion, dans les deux
fichiers.
- Bibliothèque d'images de charte disponible pour l'image de fond de
slide (ui/background_image_dialog.py) : le bouton « Remplacer l'image… »
n'ouvrait qu'un sélecteur de fichier local — délègue désormais au dialogue
unifié ImageInsertDialog (bibliothèque de charte/projet, recherche libre
de droits, génération IA), déjà utilisé partout ailleurs pour l'insertion
d'image en élément.
Corrigé (2026-07-23, visibilité des flèches de spinbox + coût en tokens manquant)
- Flèches +/- des spinbox à peine visibles (une fois cliquables, cf.
correctif précédent) : triangles agrandis (4-5px → 5-6px) et recolorés en
couleur accent (au lieu d'un gris clair) pour un contraste net sur fond clair.
- Bug réel corrigé — la génération « Nouveau depuis un brief » n'affichait
jamais de coût en tokens en fin de traitement (seulement le temps),
contrairement à l'édition par prompt. Root cause : MainWindow._new_from_ ignorait délibérément le callback
brief()emit du job (_emit, jamais
utilisé) — worker.chunk n'était jamais connecté à self.chat.. Cause plus profonde :
append_streamplanner.make_plan, slide_gen. et
generate_slideservices/visual_review.py::review_and_fix utilisent
tous streaming.complete_chat (NON streamé), qui n'acceptait aucun callback
de progression — impossible à alimenter même en connectant worker.chunk.
Fix : complete_chat accepte désormais un on_chunk optionnel (appelé UNE
FOIS avec le texte complet, même mécanisme d'estimation caractères/4 que le
streaming), threadé à travers les 3 fonctions ci-dessus + services/ (qui avait le même trou pour ses
edit_service.py::_self_critique_round
appels de vérification/correction, même dans le flux d'édition qui
fonctionnait déjà). _new_from_brief connecte maintenant worker.chunk et
passe on_chunk=emit à chaque appel.
Ajouté/Corrigé (2026-07-23, éditeur de modèle de slide + charte + bordures)
- Zoom dans l'éditeur de modèle de mise en forme de slide (ui/slide_template_editor_dialog.py) :
boutons −/+/Ajuster identiques à la fenêtre principale (Ctrl+molette
fonctionnait déjà côté CanvasView mais n'était pas découvrable sans
contrôles visibles).
- Navigation directe entre types de slides personnalisés : le dialogue
affiche un sélecteur listant tous les types (si plus d'un) ; en choisir un
autre enregistre le travail en cours puis rouvre immédiatement sur le
nouveau type — plus besoin de fermer/rouvrir à chaque fois. ThemeEditor. boucle sur
_edit_slide_type_template()requested_switch_index.
- Galerie agrandie pour les images/icônes de charte (ui/theme_asset_gallery_dialog.py,
bouton « Agrandir… » à côté d'Ajouter/Renommer/Retirer) : grille de grandes
vignettes pour mieux visualiser l'ensemble des visuels disponibles et aider
au choix de l'IA, avec les mêmes actions d'édition.
- Bug réel corrigé — ajouter un contour sur une zone de texte altérait sa
mise en forme (taille/couleur/gras/italique) : root cause DOUBLE.
(1) PropertiesPanel._emit_change() réécrivait INCONDITIONNELLEMENT les
paragraphes à partir de l'état des widgets Texte à CHAQUE application, même
quand seul un réglage sans rapport (bordure, position, verrouillage…) avait
changé — ne retouche désormais le texte que si le contrôle à l'origine du
changement appartient au groupe Texte (ou application explicite via le
bouton « Appliquer »). (2) Root cause plus profonde : le chargement des
champs Texte était conditionné à self._text_group.isVisible(), un état Qt
PEU FIABLE (un widget pas encore affiché peut rapporter False même après
setVisible(True)) — les champs retombaient alors sur leurs valeurs par
défaut (Taille = "title", 1er item du combo) au lieu du vrai premier run.
Gate désormais sur la même condition LOGIQUE que setVisible() reçoit.
- Épaisseur de contour relevée automatiquement à 2pt en choisissant une
couleur de contour alors qu'elle est encore à 0pt (sinon aucun effet
visible, donnant à tort l'impression que ça ne fonctionne pas) — jamais si
une épaisseur non nulle est déjà choisie.
- Bug réel corrigé — flèches +/- des QSpinBox/QDoubleSpinBox difficiles à
cliquer : le QSS générique (border-radius + padding) n'était compensé
par aucune règle dédiée aux sous-contrôles ::up-button/::down-button,
écrasant leur zone cliquable native. Sous-contrôles redéfinis explicitement
(largeur fixe, flèches en CSS pur) dans ui/style.py.
Ajouté (2026-07-23, export/import d'une charte et d'un projet complet en .zip)
- Exporter/importer UNE charte (services/theme_transfer.py, boutons
« Exporter… »/« Importer… » dans l'éditeur de charte, à côté de Renommer/
Supprimer/Définir par défaut) : retour utilisateur — pouvoir la transmettre
à un collègue par email/Teams sans lui donner accès à toute la bibliothèque
partagée. Une charte est déjà un JSON entièrement autonome (images/icônes
en data_uri) : l'archive .zip ne contient qu'un seul fichier theme.json
avec un petit en-tête de format, validé avant tout import. Toujours importé
comme une NOUVELLE entrée de la bibliothèque (jamais un préréglage, jamais
un remplacement en place) ; nom suffixé automatiquement en cas de collision
(même mécanisme que « Enregistrer sous… »).
- Exporter/importer un PROJET COMPLET (services/project_transfer.py,
menu Fichier > Exporter le projet (.zip)… / Importer un projet
(.zip)…) : historique complet des versions + historique des échanges
avec l'assistant, transmissibles à un collègue. Option « Inclure/Importer
la charte associée », cochée par défaut, absente/désactivée si aucune
charte n'est associée. Un point de sauvegarde de l'état affiché à l'écran
est créé automatiquement avant chaque export (seul l'autosave anti-crash le
couvrait sinon, jamais inclus dans un export). L'import crée toujours un
NOUVEAU projet (jamais de fusion/écrasement), rejoue l'historique des
versions dans l'ordre chronologique, et l'ouvre immédiatement. N'inclut
JAMAIS les retours utilisateurs/synthèses (storage/feedback_store.py,
storage/synthesis_store.py) : données de télémétrie qualité GLOBALES à
l'application, pas propres à un projet.
Ajouté (2026-07-23, accès au guide utilisateur depuis l'application)
- Menu Aide > Guide utilisateur… (ui/user_guide_dialog.py, nouveau)
ouvre docs/guide-utilisateur.md rendu directement en Markdown (titres,
listes, captures d'écran incluses via QTextBrowser.setSearchPaths) dans
un dialogue intégré à l'application — pas besoin de savoir où se trouve le
fichier ni de dépendre de l'association .md du poste (souvent un simple
éditeur de texte sans rendu d'image).
Ajouté (2026-07-23, guide utilisateur avec captures d'écran)
- Nouveau docs/guide-utilisateur.md : documentation de l'ensemble de
l'application (fenêtre principale, édition par prompt IA, édition manuelle
à la souris, insertion d'éléments, bordures/relief, charte graphique,
versions, export PPTX, gestion des projets, paramètres IA), illustrée de
10 captures d'écran réelles dans docs/screenshots/ (sur la présentation
d'exemple générique — aucune donnée de projet réel n'y figure).
Corrigé (2026-07-23, flèches +/- des spinbox inopérantes)
- Bug réel corrigé — les flèches haut/bas des champs numériques (épaisseur
de contour en pt, position/taille X/Y/largeur/hauteur) ne déclenchaient
aucune application, alors que taper une valeur puis cliquer ailleurs
fonctionnait. Root cause : ces champs étaient connectés au signal
editingFinished de Qt, qui ne s'émet QUE sur perte de focus ou touche
Entrée — jamais sur un simple clic des petites flèches (qui ne fait pas
perdre le focus au champ). C'était très probablement la cause réelle du
signalement "le contour ne fonctionne pas sur une zone de texte" (la
couleur s'appliquait immédiatement via le menu déroulant, mais l'épaisseur
restait à 0 si on avait tenté de l'augmenter avec les flèches plutôt qu'en
tapant un nombre — sans épaisseur, aucun contour ne peut se dessiner, quel
que soit le type d'élément). Reconnecté sur valueChanged (s'émet dans
tous les cas : frappe, flèches, molette), acheminé via le garde-fou
_building existant pour ne jamais déclencher d'application parasite lors
du simple chargement programmatique d'un élément sélectionné.
Corrigé (2026-07-23, dépassement récurrent de la limite de tokens)
- Bug réel corrigé — échec récurrent "prompt token count... exceeds the
limit" malgré le correctif de comptage d'images de la vague précédente,
reproduit sur un vrai projet (1 763 556 tokens envoyés pour une limite de
936 000). Root cause EXACTE : le JSON complet du deck/de la slide envoyé à
l'IA à chaque tour d'édition (generate/prompts.py::user_edit_patch/
user_edit_patch_deck) réembarquait TEXTUELLEMENT toutes les data URI
d'image (fond de slide, éléments image, ET les images/icônes de la CHARTE —
potentiellement des dizaines issues d'une extraction PPTX réelle), alors
que l'IA n'a jamais besoin de lire des octets d'image en texte pour écrire
un patch structurel (elle voit déjà la slide visuellement via la capture
PNG jointe séparément). Nouveau core/deck.py::strip_image_data() remplace
ces data URI par un court placeholder avant sérialisation — mesuré sur le
projet concerné : le dump JSON passe de 1 338 420 à 38 983 caractères
(réduction ×34). Complété par la réinitialisation automatique du contexte
de conversation (MainWindow._on_ai_error) spécifiquement pour ce type
d'erreur : auparavant, un échec ne réinitialisait JAMAIS la conversation,
si bien qu'une tentative identique suivante réutilisait le MÊME historique
déjà trop volumineux (aggravé à chaque nouvel essai) — les erreurs sans
rapport (réseau, etc.) continuent de préserver le contexte comme avant.
Ajouté (2026-07-23, panneau assistant plus réductible)
- Largeur minimale du panneau "Assistant SlydeForge" trop grande sur petit
écran : la barre "Charte + Portée de la prochaine modification" (une
seule ligne horizontale non compressible) et le bouton "Retirer la
sélection" (toujours visible même sans aucun fichier joint) empêchaient de
réduire le panneau assez pour laisser de la place au canevas. Barre répartie
sur deux lignes empilées avec un libellé de portée raccourci ("Portée :",
détail conservé dans l'infobulle), et bouton masqué tant qu'il n'y a rien à
retirer (même logique que la liste de pièces jointes).
Ajouté (2026-07-23, dossier d'export PPTX dédié)
- Dossier de destination par défaut pour l'export PPTX : proposait
auparavant le dernier dossier ouvert dans l'explorateur (ou le dossier
courant) — utilise maintenant systématiquement un dossier dédié
(%APPDATA%\SlydeForge\output, prefs.py::exports_dir(), créé si absent)
pour regrouper les exports, les retrouver facilement et pouvoir faire le
nettoyage. Le nom de fichier proposé (projet + date) reste inchangé ;
l'utilisateur peut toujours choisir un autre dossier via le sélecteur.