diff --git a/model/gurps/conditional_modifier.go b/model/gurps/conditional_modifier.go index e83f6080..9312f1c9 100644 --- a/model/gurps/conditional_modifier.go +++ b/model/gurps/conditional_modifier.go @@ -232,6 +232,11 @@ func (c *ConditionalModifier) DataOwner() DataOwner { func (c *ConditionalModifier) SetDataOwner(_ DataOwner) { } +// NameableReplacements returns the replacements to be used with Nameables. +func (c *ConditionalModifier) NameableReplacements() map[string]string { + return nil +} + // FillWithNameableKeys adds any nameable keys found to the provided map. func (c *ConditionalModifier) FillWithNameableKeys(_, _ map[string]string) { } diff --git a/model/gurps/nameables.go b/model/gurps/nameables.go index 362c14b3..23cc6e9c 100644 --- a/model/gurps/nameables.go +++ b/model/gurps/nameables.go @@ -21,6 +21,7 @@ type NameableFiller interface { // Nameables defines methods types that want to participate the nameable adjustments should implement. type Nameables interface { + NameableAccesser NameableFiller // ApplyNameableKeys applies the nameable keys to this object. ApplyNameableKeys(m map[string]string) diff --git a/model/gurps/node.go b/model/gurps/node.go index ecdfd3ee..5ec74b47 100644 --- a/model/gurps/node.go +++ b/model/gurps/node.go @@ -32,6 +32,8 @@ type DataOwnerProvider interface { // NodeTypes is a constraint that defines the types that may be nodes. type NodeTypes interface { *ConditionalModifier | *Equipment | *EquipmentModifier | *Note | *Skill | *Spell | *Trait | *TraitModifier | *Weapon + Nameables + fmt.Stringer } // Node defines the methods required of nodes in our tables. diff --git a/svg/naming.svg b/svg/naming.svg new file mode 100644 index 00000000..c351c664 --- /dev/null +++ b/svg/naming.svg @@ -0,0 +1,4 @@ + + + diff --git a/svg/svg.go b/svg/svg.go index e02f6517..dfde005c 100644 --- a/svg/svg.go +++ b/svg/svg.go @@ -173,6 +173,10 @@ var ( menuData string Menu = unison.MustSVGFromContentString(menuData) + //go:embed naming.svg + namingData string + Naming = unison.MustSVGFromContentString(namingData) + //go:embed new_folder.svg newFolderData string NewFolder = unison.MustSVGFromContentString(newFolderData) diff --git a/ux/container_conversion.go b/ux/container_conversion.go index 9fba69da..5d9646c1 100644 --- a/ux/container_conversion.go +++ b/ux/container_conversion.go @@ -10,6 +10,8 @@ package ux import ( + "fmt" + "github.com/richardwilkes/gcs/v5/model/gurps" "github.com/richardwilkes/unison" ) @@ -17,6 +19,8 @@ import ( // ConvertableNodeTypes defines the types that the container conversion can work on. type ConvertableNodeTypes interface { *gurps.Equipment | *gurps.Note + fmt.Stringer + gurps.Nameables Container() bool CanConvertToFromContainer() bool ConvertToContainer() diff --git a/ux/editor.go b/ux/editor.go index 97e9656c..159ee6c3 100644 --- a/ux/editor.go +++ b/ux/editor.go @@ -47,6 +47,7 @@ type editor[N gurps.NodeTypes, D gurps.EditorData[N]] struct { scroll *unison.ScrollPanel applyButton *unison.Button cancelButton *unison.Button + nameablesButton *unison.Button beforeData D editorData D modificationCallback func() @@ -193,6 +194,20 @@ func (e *editor[N, D]) createToolbar(helpMD string, initToolbar func(*editor[N, } toolbar.AddChild(e.cancelButton) + if _, ok := any(e.target).(*gurps.Weapon); !ok { + e.nameablesButton = unison.NewSVGButton(svg.Naming) + e.nameablesButton.Tooltip = newWrappedTooltip(i18n.Text("Set Substitutions")) + e.nameablesButton.ClickCallback = func() { + if tmp, m := e.prepareForSubstitutions(); len(m) > 0 { + ShowNameablesDialog([]string{tmp.String()}, []map[string]string{m}) + tmp.ApplyNameableKeys(m) + e.editorData.CopyFrom(tmp) + e.Rebuild(false) + } + } + toolbar.AddChild(e.nameablesButton) + } + if initToolbar != nil { initToolbar(e, toolbar) } @@ -208,6 +223,15 @@ func (e *editor[N, D]) createToolbar(helpMD string, initToolbar func(*editor[N, return toolbar } +func (e *editor[N, D]) prepareForSubstitutions() (N, map[string]string) { + node := gurps.AsNode(e.target) + tmp := node.Clone(node.GetSource().LibraryFile, node.DataOwner(), nil, true) + e.editorData.ApplyTo(tmp) + m := make(map[string]string) + tmp.FillWithNameableKeys(m, nil) + return tmp, m +} + func (e *editor[N, D]) TitleIcon(suggestedSize unison.Size) unison.Drawable { return &unison.DrawableSVG{ SVG: e.svg, @@ -251,6 +275,8 @@ func (e *editor[N, D]) Modified() bool { modified := e.isModified() e.applyButton.SetEnabled(modified) e.cancelButton.SetEnabled(modified) + _, m := e.prepareForSubstitutions() + e.nameablesButton.SetEnabled(len(m) > 0) return modified } diff --git a/ux/modifier_processing.go b/ux/modifier_processing.go index eb4f2a88..3695c3db 100644 --- a/ux/modifier_processing.go +++ b/ux/modifier_processing.go @@ -10,6 +10,8 @@ package ux import ( + "fmt" + "github.com/richardwilkes/gcs/v5/model/gurps" "github.com/richardwilkes/toolbox/i18n" "github.com/richardwilkes/toolbox/txt" @@ -19,6 +21,12 @@ import ( "github.com/richardwilkes/unison/enums/check" ) +type modifiersOnly interface { + *gurps.TraitModifier | *gurps.EquipmentModifier + fmt.Stringer + gurps.Nameables +} + // ProcessModifiersForSelection processes the selected rows for modifiers that can be toggled on or off. func ProcessModifiersForSelection[T gurps.NodeTypes](table *unison.Table[*Node[T]]) { rows := table.SelectedRows(true) @@ -48,7 +56,7 @@ func ProcessModifiers[T gurps.NodeTypes](owner unison.Paneler, rows []T) { } } -func processModifiers[T *gurps.TraitModifier | *gurps.EquipmentModifier](title string, modifiers []T) bool { +func processModifiers[T modifiersOnly](title string, modifiers []T) bool { if len(modifiers) == 0 { return false } diff --git a/ux/nameables_processing.go b/ux/nameables_processing.go index 5867795c..0f3de0bb 100644 --- a/ux/nameables_processing.go +++ b/ux/nameables_processing.go @@ -31,6 +31,7 @@ func ProcessNameablesForSelection[T gurps.NodeTypes](table *unison.Table[*Node[T // ProcessNameables processes the rows and their children for any nameables. func ProcessNameables[T gurps.NodeTypes](owner unison.Paneler, rows []T) { var data []T + var titles []string var nameables []map[string]string for _, row := range rows { gurps.Traverse(func(row T) bool { @@ -38,85 +39,93 @@ func ProcessNameables[T gurps.NodeTypes](owner unison.Paneler, rows []T) { gurps.AsNode(row).FillWithNameableKeys(m, nil) if len(m) > 0 { data = append(data, row) + titles = append(titles, gurps.AsNode(row).String()) nameables = append(nameables, m) } return false }, false, false, row) } if len(data) > 0 { - list := unison.NewPanel() - list.SetBorder(unison.NewEmptyBorder(unison.NewUniformInsets(unison.StdHSpacing))) - list.SetLayout(&unison.FlexLayout{ - Columns: 2, - HSpacing: unison.StdHSpacing, - VSpacing: unison.StdVSpacing, - }) - for i, one := range data { - keys := make([]string, 0, len(nameables[i])) - for k := range nameables[i] { - keys = append(keys, k) + if ShowNameablesDialog(titles, nameables) { + for i, row := range data { + gurps.AsNode(row).ApplyNameableKeys(nameables[i]) } - txt.SortStringsNaturalAscending(keys) - if i != 0 { - sep := unison.NewSeparator() - sep.SetLayoutData(&unison.FlexLayoutData{ - HSpan: 2, - HAlign: align.Fill, - VAlign: align.Middle, - HGrab: true, - }) - list.AddChild(sep) + if rebuildable := unison.Ancestor[Rebuildable](owner); rebuildable != nil { + rebuildable.Rebuild(true) } - header := unison.NewLabel() - header.Font = unison.SystemFont - header.SetTitle(txt.Truncate(gurps.AsNode(one).String(), 40, true)) - header.SetLayoutData(&unison.FlexLayoutData{ + } + } +} + +// ShowNameablesDialog shows a dialog for editing nameables. +func ShowNameablesDialog(titles []string, nameables []map[string]string) bool { + list := unison.NewPanel() + list.SetBorder(unison.NewEmptyBorder(unison.NewUniformInsets(unison.StdHSpacing))) + list.SetLayout(&unison.FlexLayout{ + Columns: 2, + HSpacing: unison.StdHSpacing, + VSpacing: unison.StdVSpacing, + }) + for i, one := range titles { + keys := make([]string, 0, len(nameables[i])) + for k := range nameables[i] { + keys = append(keys, k) + } + txt.SortStringsNaturalAscending(keys) + if i != 0 { + sep := unison.NewSeparator() + sep.SetLayoutData(&unison.FlexLayoutData{ HSpan: 2, HAlign: align.Fill, VAlign: align.Middle, HGrab: true, }) - list.AddChild(header) - for _, k := range keys { - label := unison.NewLabel() - label.SetTitle(k) - label.SetLayoutData(&unison.FlexLayoutData{ - HAlign: align.End, - VAlign: align.Middle, - }) - list.AddChild(label) - list.AddChild(createNameableField(k, nameables[i])) - } + list.AddChild(sep) } - scroll := unison.NewScrollPanel() - scroll.SetBorder(unison.NewLineBorder(unison.ThemeSurfaceEdge, 0, unison.NewUniformInsets(1), false)) - scroll.SetContent(list, behavior.Fill, behavior.Fill) - scroll.BackgroundInk = unison.ThemeSurface - scroll.SetLayoutData(&unison.FlexLayoutData{ + header := unison.NewLabel() + header.Font = unison.SystemFont + header.SetTitle(txt.Truncate(one, 40, true)) + header.SetLayoutData(&unison.FlexLayoutData{ + HSpan: 2, HAlign: align.Fill, - VAlign: align.Fill, + VAlign: align.Middle, HGrab: true, - VGrab: true, }) - panel := unison.NewPanel() - panel.SetLayout(&unison.FlexLayout{ - Columns: 1, - HSpacing: unison.StdHSpacing, - VSpacing: unison.StdVSpacing, - HAlign: align.Fill, - VAlign: align.Fill, - }) - label := unison.NewLabel() - label.SetTitle(i18n.Text("Provide substitutions:")) - panel.AddChild(label) - panel.AddChild(scroll) - if unison.QuestionDialogWithPanel(panel) == unison.ModalResponseOK { - for i, row := range data { - gurps.AsNode(row).ApplyNameableKeys(nameables[i]) - } - unison.Ancestor[Rebuildable](owner).Rebuild(true) + list.AddChild(header) + for _, k := range keys { + label := unison.NewLabel() + label.SetTitle(k) + label.SetLayoutData(&unison.FlexLayoutData{ + HAlign: align.End, + VAlign: align.Middle, + }) + list.AddChild(label) + list.AddChild(createNameableField(k, nameables[i])) } } + scroll := unison.NewScrollPanel() + scroll.SetBorder(unison.NewLineBorder(unison.ThemeSurfaceEdge, 0, unison.NewUniformInsets(1), false)) + scroll.SetContent(list, behavior.Fill, behavior.Fill) + scroll.BackgroundInk = unison.ThemeSurface + scroll.SetLayoutData(&unison.FlexLayoutData{ + HAlign: align.Fill, + VAlign: align.Fill, + HGrab: true, + VGrab: true, + }) + panel := unison.NewPanel() + panel.SetLayout(&unison.FlexLayout{ + Columns: 1, + HSpacing: unison.StdHSpacing, + VSpacing: unison.StdVSpacing, + HAlign: align.Fill, + VAlign: align.Fill, + }) + label := unison.NewLabel() + label.SetTitle(i18n.Text("Provide substitutions:")) + panel.AddChild(label) + panel.AddChild(scroll) + return unison.QuestionDialogWithPanel(panel) == unison.ModalResponseOK } func createNameableField(key string, m map[string]string) *unison.Field {