feat(gui): Quick Picker, Gestionnaire, Éditeur, formulaire de variables, Paramètres

- 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
This commit is contained in:
Spark
2026-10-11 10:17:30 -04:00
parent b102a4a206
commit 348036daf3
5 changed files with 814 additions and 0 deletions
+250
View File
@@ -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
}
+199
View File
@@ -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")
}
}
+161
View File
@@ -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()
}
}
+89
View File
@@ -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()
}
}
+115
View File
@@ -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("")
}
}