- 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
251 lines
10 KiB
AutoHotkey
251 lines
10 KiB
AutoHotkey
; 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
|
|
}
|