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:
+250
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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("")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user