From 348036daf31c8dbfcefdcae5cff5e01c764fa3ea Mon Sep 17 00:00:00 2001 From: Spark Date: Sun, 11 Oct 2026 10:17:30 -0400 Subject: [PATCH] =?UTF-8?q?feat(gui):=20Quick=20Picker,=20Gestionnaire,=20?= =?UTF-8?q?=C3=89diteur,=20formulaire=20de=20variables,=20Param=C3=A8tres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Picker : recherche instantanée, aperçu, navigation clavier complète, raccourcis actifs seulement dans sa fenêtre (HotIfWinActive) - Gestionnaire : arbre des catégories, favoris/récents/erreurs, aperçu, création depuis le presse-papiers, suppression par la corbeille - Éditeur : métadonnées, insertion/déclaration de variables synchronisées, validation en direct, Tester (prévisualisation résolue, sans injection) - VarForm : un contrôle par type de variable, requis validés, Échap annule toute l'injection et rend le focus --- gui/Editor.ahk | 250 ++++++++++++++++++++++++++++++++++++++++++++ gui/Manager.ahk | 199 +++++++++++++++++++++++++++++++++++ gui/Picker.ahk | 161 ++++++++++++++++++++++++++++ gui/SettingsGui.ahk | 89 ++++++++++++++++ gui/VarForm.ahk | 115 ++++++++++++++++++++ 5 files changed, 814 insertions(+) create mode 100644 gui/Editor.ahk create mode 100644 gui/Manager.ahk create mode 100644 gui/Picker.ahk create mode 100644 gui/SettingsGui.ahk create mode 100644 gui/VarForm.ahk diff --git a/gui/Editor.ahk b/gui/Editor.ahk new file mode 100644 index 0000000..e793b35 --- /dev/null +++ b/gui/Editor.ahk @@ -0,0 +1,250 @@ +; Editor.ahk — Éditeur de prompt intégré (architecture §9.2). +; Métadonnées + corps + déclaration des variables, validation en direct, bouton Tester. + +class PromptEditor { + GuiObj := "" + Item := "" ; PromptItem en cours d'édition ("" = nouveau, créé à l'enregistrement) + PendingVars := [] ; déclarations de variables en cours + F := Map() ; contrôles nommés + OnSaved := "" ; Func(item) + + Show(item, onSaved := "") { + this.Item := item + this.OnSaved := onSaved + this.PendingVars := [] + if (item != "" && item.Meta.Has("variables")) { + for v in item.Meta["variables"] + this.PendingVars.Push(v) + } + if (this.GuiObj != "") + this.GuiObj.Destroy() + g := Gui("-MaximizeBox +Resize", item = "" ? "Nouveau prompt" : "Modifier — " item.Title) + g.SetFont("s10", "Segoe UI") + this.GuiObj := g + F := this.F + + g.Add("Text", "xm w90", "Titre :") + F["title"] := g.Add("Edit", "x+5 w420", item != "" ? item.Title : "") + g.Add("Text", "xm w90", "Catégorie :") + cats := App.Store.Categories() + F["category"] := g.Add("ComboBox", "x+5 w420", cats) + F["category"].Text := item != "" ? item.Category : "Sans catégorie" + g.Add("Text", "xm w90", "Étiquettes :") + F["tags"] := g.Add("Edit", "x+5 w420", item != "" ? Join(item.Tags, ", ") : "") + g.Add("Text", "xm w90", "Profils :") + F["targets"] := g.Add("Edit", "x+5 w420", item != "" ? Join(item.Targets, ", ") : "") + F["favorite"] := g.Add("CheckBox", "xm", "Favori ★") + F["favorite"].Value := item != "" ? item.Favorite : 0 + F["pinned"] := g.Add("Edit", "x+30 w40 Number", item != "" ? item.Pinned : 0) + g.Add("Text", "x+5", "Épinglé n° (0-9)") + F["sensitive"] := g.Add("CheckBox", "x+30", "Sensible (vider le presse-papiers après)") + F["sensitive"].Value := item != "" ? item.Sensitive : 0 + g.Add("Text", "xm w90", "Raccourci :") + F["hotkey"] := g.Add("Edit", "x+5 w200", item != "" ? item.Hotkey : "") + g.Add("Text", "x+15 w90", "Hotstring :") + F["hotstring"] := g.Add("Edit", "x+5 w200", item != "" ? item.Hotstring : "") + + g.Add("Text", "xm", "Corps du prompt ({{variable}} pour un emplacement, Ctrl+J pour insérer) :") + F["body"] := g.Add("Edit", "xm w760 r16 -Wrap", item != "" ? item.Body : "") + F["varsInfo"] := g.Add("Text", "xm w760", "") + this.RefreshVarsInfo() + + btnVar := g.Add("Button", "xm w200", "Insérer une variable (Ctrl+J)") + btnVar.OnEvent("Click", (*) => this.InsertVariable()) + btnDeclare := g.Add("Button", "x+10 w200", "Déclarer une variable…") + btnDeclare.OnEvent("Click", (*) => this.DeclareVariable()) + btnTest := g.Add("Button", "x+10 w140", "Tester") + btnTest.OnEvent("Click", (*) => this.TestPrompt()) + btnExt := g.Add("Button", "xm w200", "Ouvrir dans l'éditeur externe") + btnExt.OnEvent("Click", (*) => this.OpenExternal()) + btnSave := g.Add("Button", "x+10 w160 Default", "Enregistrer") + btnSave.OnEvent("Click", (*) => this.Save()) + btnCancel := g.Add("Button", "x+10 w120", "Annuler") + btnCancel.OnEvent("Click", (*) => this.GuiObj.Destroy()) + g.OnEvent("Close", (*) => this.GuiObj.Destroy()) + ; Ctrl+J dans l'éditeur. + HotIfWinActive("ahk_id " g.Hwnd) + Hotkey("^j", (*) => this.InsertVariable(), "On") + HotIfWinActive() + g.Show("w800") + } + + RefreshVarsInfo() { + names := [] + for v in this.PendingVars { + if v.Has("name") + names.Push(v["name"] " (" (v.Has("type") ? v["type"] : "text") ")") + } + used := VariableEngine.FindVars(this.F["body"].Value) + warns := [] + for u in used { + if VariableEngine.IsAuto(u) + continue + declared := false + for v in this.PendingVars { + if (v.Has("name") && StrLower(String(v["name"])) = u) { + declared := true + break + } + } + if !declared + warns.Push("« " u " » utilisé mais non déclaré (traité comme texte libre)") + } + txt := "Variables déclarées : " (names.Length ? Join(names, ", ") : "aucune") + if (warns.Length) + txt .= " · ⚠ " Join(warns, " ; ") + this.F["varsInfo"].Text := txt + } + + InsertVariable() { + ib := InputBox("Nom de la variable à insérer (ex. ton, langage) :", "Insérer une variable") + if (ib.Result != "OK" || Trim(ib.Value) = "") + return + name := StrLower(Trim(ib.Value)) + body := this.F["body"] + body.Focus() + SendText("{{" name "}}") + ; Si elle n'est pas déclarée, proposer de la déclarer. + for v in this.PendingVars { + if (v.Has("name") && StrLower(String(v["name"])) = name) + return + } + r := MsgBox("La variable « " name " » n'est pas déclarée. La déclarer maintenant ?", "PromptKey", "YesNo") + if (r = "Yes") + this.DeclareVariable(name) + this.RefreshVarsInfo() + } + + DeclareVariable(presetName := "") { + ibName := InputBox("Nom de la variable :", "Déclarer une variable", , presetName) + if (ibName.Result != "OK" || Trim(ibName.Value) = "") + return + name := StrLower(Trim(ibName.Value)) + ibLabel := InputBox("Libellé affiché dans le formulaire :", "Déclarer une variable", , name) + if (ibLabel.Result != "OK") + return + ibType := InputBox("Type (text, multiline, enum, number, date, bool, password) :", "Déclarer une variable", , "text") + if (ibType.Result != "OK") + return + type := StrLower(Trim(ibType.Value)) + v := Map("name", name, "label", ibLabel.Value, "type", type) + if (type = "enum") { + ibChoices := InputBox("Choix, séparés par des virgules :", "Déclarer une variable") + if (ibChoices.Result != "OK") + return + choices := [] + for c in StrSplit(ibChoices.Value, ",") + choices.Push(Trim(c)) + v["choices"] := choices + if (choices.Length) + v["default"] := choices[1] + } else { + ibDef := InputBox("Valeur par défaut (optionnel) :", "Déclarer une variable") + if (ibDef.Result = "OK" && ibDef.Value != "") + v["default"] := ibDef.Value + } + ; Remplacer une déclaration existante du même nom. + newVars := [] + for existing in this.PendingVars { + if !(existing.Has("name") && StrLower(String(existing["name"])) = name) + newVars.Push(existing) + } + newVars.Push(v) + this.PendingVars := newVars + this.RefreshVarsInfo() + } + + ; Prévisualisation du prompt résolu avec les valeurs par défaut (aucune injection, §9.2). + TestPrompt() { + item := this.CollectToItem(false) + if (item = "") + return + defs := VariableEngine.ManualVars(item) + defaults := Map() + for d in defs + defaults[d["name"]] := d["default"] + ctx := Map("selection", "(texte sélectionné d'exemple)", "clipboardText", "", "process", "application") + resolved := VariableEngine.Resolve(item.Body, defaults, ctx) + g := Gui("+AlwaysOnTop", "Aperçu — " item.Title) + g.SetFont("s10", "Segoe UI") + g.Add("Edit", "w640 r18 ReadOnly -Wrap", resolved["text"]) + if (resolved["unresolved"].Length) + g.Add("Text", "w640 cRed", "Variables non résolues : " Join(resolved["unresolved"], ", ")) + btn := g.Add("Button", "Default w120", "Fermer") + btn.OnEvent("Click", (*) => g.Destroy()) + g.OnEvent("Escape", (*) => g.Destroy()) + g.Show() + } + + OpenExternal() { + item := this.Item + if (item = "" || item.Path = "") { + TrayTip("Enregistrez d'abord le prompt pour l'ouvrir dans l'éditeur externe.", "PromptKey") + return + } + editor := App.Settings.ExternalEditor + try { + if (editor != "") + Run(editor " `"" item.Path "`"") + else + Run(item.Path) + } catch as e { + MsgBox("Ouverture impossible : " e.Message, "PromptKey") + } + } + + ; Construit / met à jour le PromptItem depuis les contrôles. Crée le fichier seulement si save=true et nouveau. + CollectToItem(save) { + F := this.F + title := Trim(F["title"].Value) + if (title = "") { + MsgBox("Le titre est requis.", "PromptKey") + return "" + } + item := this.Item + if (item = "") { + if !save + item := PromptItem("", Trim(F["category"].Text), Map("meta", Map(), "body", "", "errors", [], "hasHeader", false)) + else + item := App.Store.CreateNew(title, Trim(F["category"].Text), F["body"].Value) + } + item.Title := title + item.Category := Trim(F["category"].Text) + item.Tags := SplitCsv(F["tags"].Value) + item.Targets := SplitCsv(F["targets"].Value) + item.Favorite := F["favorite"].Value + item.Pinned := F["pinned"].Value != "" ? Integer(F["pinned"].Value) : 0 + item.Sensitive := F["sensitive"].Value + item.Hotkey := Trim(F["hotkey"].Value) + item.Hotstring := Trim(F["hotstring"].Value) + item.Body := F["body"].Value + if (this.PendingVars.Length) + item.Meta["variables"] := this.PendingVars + else if item.Meta.Has("variables") + item.Meta.Delete("variables") + return item + } + + Save() { + item := this.CollectToItem(true) + if (item = "") + return + App.Store.Save(item) + this.Item := item + TrayTip("Prompt « " item.Title " » enregistré.", "PromptKey") + this.GuiObj.Destroy() + App.RefreshAfterLibraryChange() + if (this.OnSaved != "") + this.OnSaved(item) + } +} + +SplitCsv(s) { + out := [] + for part in StrSplit(s, ",") { + part := Trim(part) + if (part != "") + out.Push(part) + } + return out +} diff --git a/gui/Manager.ahk b/gui/Manager.ahk new file mode 100644 index 0000000..aa8df93 --- /dev/null +++ b/gui/Manager.ahk @@ -0,0 +1,199 @@ +; Manager.ahk — Le Gestionnaire : bibliothèque en trois volets (architecture §9.1). + +class PromptManager { + GuiObj := "" + TV := "" + LV := "" + Preview := "" + Status := "" + NodeFilter := Map() ; id de nœud TreeView -> filtre (catégorie ou __all__/__fav__/__recent__/__errors__) + Items := [] + Filter := "__all__" + OnUse := "" ; Func(prompt) + OnEdit := "" ; Func(prompt) + + Build(onUse, onEdit) { + this.OnUse := onUse + this.OnEdit := onEdit + g := Gui("-MaximizeBox +Resize", "PromptKey — Gestionnaire") + g.SetFont("s10", "Segoe UI") + this.GuiObj := g + this.TV := g.Add("TreeView", "xm w180 r18") + this.TV.OnEvent("ItemSelect", (tv, id) => this.NodeFilter.Has(id) ? this.SetFilter(this.NodeFilter[id]) : "") + this.LV := g.Add("ListView", "x+10 w430 r18", ["Titre", "Modifié", "Utilisations"]) + this.LV.ModifyCol(1, 240) + this.LV.ModifyCol(2, 90) + this.LV.ModifyCol(3, 95) + this.LV.OnEvent("ItemSelect", (lv, row, selected) => selected ? this.UpdatePreview(row) : "") + this.LV.OnEvent("DoubleClick", (lv, row) => row > 0 ? this.EditSelected() : "") + this.Preview := g.Add("Edit", "x+10 w330 r18 ReadOnly -Wrap", "") + + btnUse := g.Add("Button", "xm w130", "Utiliser") + btnUse.OnEvent("Click", (*) => this.UseSelected()) + btnEdit := g.Add("Button", "x+8 w130", "Modifier") + btnEdit.OnEvent("Click", (*) => this.EditSelected()) + btnNew := g.Add("Button", "x+8 w130", "Nouveau") + btnNew.OnEvent("Click", (*) => App.NewPromptInteractive()) + btnClip := g.Add("Button", "x+8 w180", "Nouveau depuis le presse-papiers") + btnClip.OnEvent("Click", (*) => this.NewFromClipboard()) + btnDup := g.Add("Button", "xm w130", "Dupliquer") + btnDup.OnEvent("Click", (*) => this.DuplicateSelected()) + btnDel := g.Add("Button", "x+8 w130", "Supprimer") + btnDel.OnEvent("Click", (*) => this.DeleteSelected()) + btnFolder := g.Add("Button", "x+8 w160", "Ouvrir le dossier") + btnFolder.OnEvent("Click", (*) => Run(App.Settings.LibraryPath)) + btnReload := g.Add("Button", "x+8 w130", "Recharger") + btnReload.OnEvent("Click", (*) => this.Refresh()) + this.Status := g.Add("Text", "xm w950", "") + g.OnEvent("Close", (*) => g.Hide()) + g.OnEvent("Escape", (*) => g.Hide()) + } + + Show() { + this.Refresh() + this.GuiObj.Show("w990") + } + + Hide() { + this.GuiObj.Hide() + } + + SetFilter(filter) { + this.Filter := filter + this.RefreshList() + } + + Refresh() { + App.Store.Load() + this.NodeFilter := Map() + this.TV.Delete() + root := this.TV.Add("Bibliothèque") + this.NodeFilter[root] := "__all__" + for cat in App.Store.Categories() { + node := this.TV.Add(cat, root) + this.NodeFilter[node] := cat + } + fav := this.TV.Add("★ Favoris", root) + this.NodeFilter[fav] := "__fav__" + rec := this.TV.Add("◷ Récents", root) + this.NodeFilter[rec] := "__recent__" + if (App.Store.ErrorFiles.Length > 0) { + err := this.TV.Add("⚠ En erreur (" App.Store.ErrorFiles.Length ")", root) + this.NodeFilter[err] := "__errors__" + } + this.TV.Modify(root, "Expand") + this.RefreshList() + } + + FilteredPrompts() { + out := [] + switch this.Filter { + case "__all__": + return App.Store.Prompts + case "__fav__": + for p in App.Store.Prompts { + if p.Favorite + out.Push(p) + } + case "__recent__": + for p in App.Store.Prompts { + if (App.Usage && App.Usage.Data.Has(p.Id)) + out.Push(p) + } + case "__errors__": + return App.Store.ErrorFiles + default: + for p in App.Store.Prompts { + if (p.Category = this.Filter) + out.Push(p) + } + } + return out + } + + RefreshList() { + this.Items := this.FilteredPrompts() + this.LV.Delete() + for p in this.Items { + count := App.Usage ? App.Usage.Count(p.Id) : 0 + marker := (p.Favorite ? "★ " : "") (p.Pinned ? "📌 " : "") + this.LV.Add("", marker p.Title, p.Updated, count) + } + if (this.Items.Length > 0) { + this.LV.Modify(1, "Select") + this.UpdatePreview(1) + } else { + this.Preview.Value := "" + } + this.Status.Text := App.Store.Prompts.Length " prompt(s) dans la bibliothèque · filtre : " this.Filter + } + + UpdatePreview(row) { + if (row < 1 || row > this.Items.Length) + return + p := this.Items[row] + txt := p.Body + if (p.ParseError != "") + txt := "⚠ Erreur d'en-tête : " p.ParseError "`r`nFichier : " p.Path "`r`n`r`n" txt + else + txt := "Catégorie : " p.Category " · Étiquettes : " Join(p.Tags, ", ") "`r`n" (p.Hotkey != "" ? "Raccourci : " p.Hotkey "`r`n" : "") (p.Hotstring != "" ? "Hotstring : ::" p.Hotstring "`r`n" : "") "`r`n" txt + this.Preview.Value := txt + } + + SelectedPrompt() { + row := this.LV.GetNext() + if (row >= 1 && row <= this.Items.Length) + return this.Items[row] + return "" + } + + UseSelected() { + p := this.SelectedPrompt() + if (p = "") + return + this.Hide() + this.OnUse(p) + } + + EditSelected() { + p := this.SelectedPrompt() + if (p = "") + return + this.OnEdit(p) + } + + DuplicateSelected() { + p := this.SelectedPrompt() + if (p = "") + return + App.Store.Duplicate(p) + this.Refresh() + } + + DeleteSelected() { + p := this.SelectedPrompt() + if (p = "") + return + r := MsgBox("Envoyer « " p.Title " » à la corbeille ?", "PromptKey", "YesNo Icon?") + if (r != "Yes") + return + App.Store.Delete(p) + App.RefreshAfterLibraryChange() + this.Refresh() + } + + NewFromClipboard() { + txt := ClipboardGuard.ClipboardText() + if (Trim(txt) = "") { + TrayTip("Le presse-papiers ne contient pas de texte.", "PromptKey") + return + } + ib := InputBox("Titre du nouveau prompt :", "PromptKey — Depuis le presse-papiers") + if (ib.Result != "OK" || Trim(ib.Value) = "") + return + item := App.Store.CreateNew(Trim(ib.Value), "Sans catégorie", txt) + App.RefreshAfterLibraryChange() + this.Refresh() + TrayTip("Prompt « " item.Title " » créé depuis le presse-papiers.", "PromptKey") + } +} diff --git a/gui/Picker.ahk b/gui/Picker.ahk new file mode 100644 index 0000000..c7cf3d0 --- /dev/null +++ b/gui/Picker.ahk @@ -0,0 +1,161 @@ +; Picker.ahk — Le Quick Picker : surface principale de PromptKey (architecture §8). +; Fenêtre pré-créée, recherche instantanée, navigation clavier, aperçu, retour de focus propre. + +class PromptPicker { + GuiObj := "" + SearchEdit := "" + LV := "" + Preview := "" + Status := "" + Items := [] ; prompts correspondant aux lignes affichées + Ctx := Map() + OnUse := "" ; Func(prompt, useDefaults) + OnEdit := "" ; Func(prompt) + ProfileLabel := "" + HotkeysArmed := false + + Build(onUse, onEdit) { + this.OnUse := onUse + this.OnEdit := onEdit + g := Gui("-MaximizeBox +Resize", "PromptKey") + g.SetFont("s10", "Segoe UI") + this.GuiObj := g + g.Add("Text", "xm", "Rechercher :") + this.SearchEdit := g.Add("Edit", "x+10 w470", "") + this.SearchEdit.OnEvent("Change", (*) => this.RefreshList()) + this.ProfileLabel := g.Add("Text", "x+10 w180", "") + this.LV := g.Add("ListView", "xm w500 r14", ["Titre", "Catégorie", "Étiquettes"]) + this.LV.ModifyCol(1, 240) + this.LV.ModifyCol(2, 110) + this.LV.ModifyCol(3, 140) + this.LV.OnEvent("ItemSelect", (lv, row, selected) => selected ? this.UpdatePreview(row) : "") + this.LV.OnEvent("DoubleClick", (lv, row) => row > 0 ? this.UseSelected(false) : "") + this.Preview := g.Add("Edit", "x+15 w330 r14 ReadOnly -Wrap", "") + this.Status := g.Add("Text", "xm w850", "") + g.OnEvent("Escape", (*) => this.Hide()) + g.OnEvent("Close", (*) => this.Hide()) + } + + Show(ctx) { + this.Ctx := ctx + this.SearchEdit.Value := "" + this.RefreshList() + this.GuiObj.Show("Center w880") + this.SearchEdit.Focus() + this.ArmHotkeys() + if (ctx.Has("profileName")) + this.ProfileLabel.Text := "Profil : " ctx["profileName"] + } + + Hide() { + this.DisarmHotkeys() + this.GuiObj.Hide() + } + + ; Raccourcis propres au Picker, actifs seulement quand sa fenêtre est active (§8.2). + ArmHotkeys() { + if this.HotkeysArmed + return + HotIfWinActive("ahk_id " this.GuiObj.Hwnd) + Hotkey("Enter", (*) => this.UseSelected(false), "On") + Hotkey("^Enter", (*) => this.UseSelected(true), "On") + Hotkey("^e", (*) => this.EditSelected(), "On") + Hotkey("Down", (*) => this.MoveSelection(1), "On") + Hotkey("Up", (*) => this.MoveSelection(-1), "On") + Hotkey("^c", (*) => this.CopySelected(), "On") + HotIfWinActive() + this.HotkeysArmed := true + } + + DisarmHotkeys() { + if !this.HotkeysArmed + return + HotIfWinActive("ahk_id " this.GuiObj.Hwnd) + for hk in ["Enter", "^Enter", "^e", "Down", "Up", "^c"] { + try Hotkey(hk, "Off") + } + HotIfWinActive() + this.HotkeysArmed := false + } + + MoveSelection(delta) { + row := this.LV.GetNext() + if (row = 0) + row := delta > 0 ? 1 : this.Items.Length + else + row := Max(1, Min(this.Items.Length, row + delta)) + if (row >= 1 && row <= this.Items.Length) { + this.LV.Modify(0, "-Select") + this.LV.Modify(row, "Select Focus Vis") + this.UpdatePreview(row) + } + } + + RefreshList() { + q := this.SearchEdit.Value + usageData := App.Usage ? App.Usage.Data : Map() + this.Items := Search.Filter(App.Store.Prompts, q, usageData) + this.LV.Delete() + for p in this.Items { + marker := "" + if p.Pinned + marker .= "📌 " + if p.Favorite + marker .= "★ " + this.LV.Add("", marker p.Title, p.Category, Join(p.Tags, ", ")) + } + if (this.Items.Length > 0) { + this.LV.Modify(1, "Select Focus") + this.UpdatePreview(1) + } else { + this.Preview.Value := "(aucun prompt ne correspond)" + } + this.Status.Text := this.Items.Length " résultat(s) · Entrée Utiliser · Ctrl+Entrée Valeurs par défaut · Ctrl+E Modifier · Échap Fermer" + } + + UpdatePreview(row) { + if (row < 1 || row > this.Items.Length) + return + p := this.Items[row] + txt := p.Body + sel := this.Ctx.Has("selection") ? this.Ctx["selection"] : "" + if (sel != "") + txt .= "`r`n`r`n— Sélection capturée (" StrLen(sel) " caractères) : « " Util.Truncate(sel, 120) " »" + if (p.ParseError != "") + txt := "⚠ Erreur de fichier : " p.ParseError "`r`n`r`n" txt + this.Preview.Value := txt + } + + SelectedPrompt() { + row := this.LV.GetNext() + if (row >= 1 && row <= this.Items.Length) + return this.Items[row] + return "" + } + + UseSelected(useDefaults) { + p := this.SelectedPrompt() + if (p = "") + return + this.Hide() + this.OnUse(p, useDefaults) + } + + EditSelected() { + p := this.SelectedPrompt() + if (p = "") + return + this.Hide() + this.OnEdit(p) + } + + ; Ctrl+C : copie le corps brut (sans résolution) — la résolution complète passe par l'injection. + CopySelected() { + p := this.SelectedPrompt() + if (p = "") + return + A_Clipboard := p.Body + TrayTip("Corps du prompt « " p.Title " » copié.", "PromptKey") + this.Hide() + } +} diff --git a/gui/SettingsGui.ahk b/gui/SettingsGui.ahk new file mode 100644 index 0000000..add61a2 --- /dev/null +++ b/gui/SettingsGui.ahk @@ -0,0 +1,89 @@ +; SettingsGui.ahk — Fenêtre des paramètres (architecture §17.1). +; Les champs essentiels ; le reste vit dans config.ini, ouvrable d'un clic. + +class SettingsGui { + GuiObj := "" + F := Map() + + Show() { + if (this.GuiObj != "") + this.GuiObj.Destroy() + st := App.Settings + g := Gui("-MaximizeBox", "PromptKey — Paramètres") + g.SetFont("s10", "Segoe UI") + this.GuiObj := g + F := this.F + + g.Add("Text", "xm w190", "Dossier de la bibliothèque :") + F["library"] := g.Add("Edit", "x+5 w420", st.LibraryPath) + btnBrowse := g.Add("Button", "x+5 w90", "Parcourir…") + btnBrowse.OnEvent("Click", (*) => this.BrowseLibrary()) + g.Add("Text", "xm w190", "Raccourci du Picker :") + F["pickerHk"] := g.Add("Edit", "x+5 w200", st.PickerHotkey) + g.Add("Text", "xm w190", "Raccourci du Gestionnaire :") + F["managerHk"] := g.Add("Edit", "x+5 w200", st.ManagerHotkey) + g.Add("Text", "xm w190", "Méthode d'injection :") + F["method"] := g.Add("ComboBox", "x+5 w200", ["clipboard", "sendtext"]) + F["method"].Text := st.Method + g.Add("Text", "xm w190", "Délai après collage (ms) :") + F["pasteDelay"] := g.Add("Edit", "x+5 w100 Number", st.PasteDelayMs) + g.Add("Text", "xm w190", "Délai de restauration (ms) :") + F["restoreDelay"] := g.Add("Edit", "x+5 w100 Number", st.RestoreDelayMs) + g.Add("Text", "xm w190", "Délai d'envoi auto (ms) :") + F["sendDelay"] := g.Add("Edit", "x+5 w100 Number", st.SendAfterDelayMs) + g.Add("Text", "xm w190", "Éditeur externe (optionnel) :") + F["editor"] := g.Add("Edit", "x+5 w420", st.ExternalEditor) + + g.Add("Text", "xm", "Profils de destination : " Join(App.Settings.ProfileNames(), ", ")) + conflicts := App.Hotkeys.Conflicts + g.Add("Text", "xm w620", "Raccourcis en conflit : " (conflicts.Length ? "" : "aucun")) + if (conflicts.Length) { + txt := "" + for c in conflicts + txt .= "• " c "`r`n" + g.Add("Edit", "xm w620 r4 ReadOnly", txt) + } + + btnSave := g.Add("Button", "xm w160 Default", "Enregistrer") + btnSave.OnEvent("Click", (*) => this.Save()) + btnIni := g.Add("Button", "x+10 w180", "Ouvrir config.ini") + btnIni.OnEvent("Click", (*) => Run(App.Settings.Path)) + btnStats := g.Add("Button", "x+10 w200", "Réinitialiser les statistiques") + btnStats.OnEvent("Click", (*) => this.ResetStats()) + g.OnEvent("Close", (*) => g.Hide()) + g.OnEvent("Escape", (*) => g.Hide()) + g.Show() + } + + BrowseLibrary() { + dir := DirSelect("*" App.Settings.LibraryPath, 3, "Choisir le dossier de la bibliothèque") + if (dir != "") + this.F["library"].Value := dir + } + + Save() { + st := App.Settings + F := this.F + st.LibraryPath := Trim(F["library"].Value) + st.PickerHotkey := Trim(F["pickerHk"].Value) + st.ManagerHotkey := Trim(F["managerHk"].Value) + st.Method := F["method"].Text + st.PasteDelayMs := Integer(F["pasteDelay"].Value) + st.RestoreDelayMs := Integer(F["restoreDelay"].Value) + st.SendAfterDelayMs := Integer(F["sendDelay"].Value) + st.ExternalEditor := Trim(F["editor"].Value) + st.Save() + DirCreate(st.LibraryPath) + App.Store.LibraryPath := st.LibraryPath + App.Store.Load() + App.RefreshAfterLibraryChange() + TrayTip("Paramètres enregistrés.", "PromptKey") + this.GuiObj.Hide() + } + + ResetStats() { + r := MsgBox("Réinitialiser les compteurs d'utilisation ?", "PromptKey", "YesNo") + if (r = "Yes" && App.Usage) + App.Usage.Reset() + } +} diff --git a/gui/VarForm.ahk b/gui/VarForm.ahk new file mode 100644 index 0000000..67485e1 --- /dev/null +++ b/gui/VarForm.ahk @@ -0,0 +1,115 @@ +; VarForm.ahk — Mini-formulaire des variables manuelles d'un prompt (architecture §10.4). +; Construit dynamiquement un contrôle par variable selon son type. Échap = annulation complète. + +class VarForm { + GuiObj := "" + Controls := Map() ; nom -> Map(ctrl, def) + Callback := "" ; Func(values Map) ou Func("") si annulé + PromptRef := "" + + ; defaults : Map nom -> valeur proposée (défaut déclaré, surchargé par les derniers choix) + Show(prompt, varDefs, defaults, callback) { + this.PromptRef := prompt + this.Callback := callback + this.Controls := Map() + if (this.GuiObj != "") + this.GuiObj.Destroy() + g := Gui("+AlwaysOnTop -MaximizeBox", prompt.Title " — paramètres") + g.SetFont("s10", "Segoe UI") + this.GuiObj := g + + for def in varDefs { + name := def["name"] + label := def["label"] + type := def["type"] + defaultVal := defaults.Has(name) ? String(defaults[name]) : def["default"] + g.Add("Text", "xm w340", label (def["required"] ? " *" : "")) + ctrl := "" + switch type { + case "enum": + choices := def["choices"].Length > 0 ? def["choices"] : [defaultVal] + ctrl := g.Add("ComboBox", "xm w340", choices) + idx := 0 + for i, c in choices { + if (String(c) = defaultVal) { + idx := i + break + } + } + ctrl.Value := idx > 0 ? idx : 1 + case "bool": + ctrl := g.Add("CheckBox", "xm w340", label) + ctrl.Value := (defaultVal = "true" || defaultVal = "1") ? 1 : 0 + ; Le libellé au-dessus fait doublon pour une case : on masque le texte de la case. + ctrl.Text := "Oui" + case "multiline": + ctrl := g.Add("Edit", "xm w340 r4", defaultVal) + case "password": + ctrl := g.Add("Edit", "xm w340 Password", "") + case "date": + ctrl := g.Add("Edit", "xm w340", defaultVal != "" ? defaultVal : FormatTime(, "yyyy-MM-dd")) + default: ; text, number + ctrl := g.Add("Edit", "xm w340", defaultVal) + } + this.Controls[name] := Map("ctrl", ctrl, "def", def) + } + + btnOk := g.Add("Button", "xm w160 Default", "Insérer le prompt") + btnOk.OnEvent("Click", (*) => this.Submit()) + btnCancel := g.Add("Button", "x+20 w160", "Annuler (Échap)") + btnCancel.OnEvent("Click", (*) => this.Cancel()) + g.OnEvent("Escape", (*) => this.Cancel()) + g.OnEvent("Close", (*) => this.Cancel()) + g.Show() + ; Focus sur le premier contrôle. + for name, entry in this.Controls { + entry["ctrl"].Focus() + break + } + } + + Submit() { + values := Map() + for name, entry in this.Controls { + ctrl := entry["ctrl"] + def := entry["def"] + type := def["type"] + val := "" + if (type = "bool") { + if (def["choices"].Length >= 2) + val := ctrl.Value ? String(def["choices"][1]) : String(def["choices"][2]) + else + val := ctrl.Value ? (def["default"] != "" ? def["default"] : "oui") : "" + } else if (type = "enum") { + val := ctrl.Text + } else { + val := ctrl.Value + } + if (def["required"] && Trim(val) = "") { + ctrl.Focus() + TrayTip("La variable « " def["label"] " » est requise.", "PromptKey") + return + } + if (type = "number" && val != "" && !IsNumber(val)) { + ctrl.Focus() + TrayTip("La variable « " def["label"] " » doit être un nombre.", "PromptKey") + return + } + values[name] := val + } + cb := this.Callback + this.GuiObj.Destroy() + this.GuiObj := "" + cb(values) + } + + Cancel() { + cb := this.Callback + if (this.GuiObj != "") { + this.GuiObj.Destroy() + this.GuiObj := "" + } + if (cb != "") + cb("") + } +}