Added Configurator

This commit is contained in:
diamondburned (Forefront) 2020-06-28 18:38:09 -07:00
parent 5499788460
commit 5ac2567610
9 changed files with 315 additions and 41 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
const AppID = "com.github.diamondburned.cchat-gtk"
@ -30,6 +31,23 @@ func NewModalDialog() (*gtk.Dialog, error) {
}
d.SetModal(true)
d.SetTransientFor(App.Window)
return d, nil
}
func NewEmptyModalDialog() (*gtk.Dialog, error) {
d, err := NewModalDialog()
if err != nil {
return nil, err
}
b, err := d.GetContentArea()
if err != nil {
return nil, errors.Wrap(err, "Failed to get content area")
}
d.Remove(b)
return d, nil
}

View File

@ -62,7 +62,7 @@ func Section(entries []config.Entry) *gtk.Grid {
}
grid.SetRowSpacing(4)
grid.SetColumnSpacing(4)
grid.SetColumnSpacing(8)
grid.Show()
return grid
}

View File

@ -23,21 +23,20 @@ func Combo(selected *int, options []string, change func(int)) EntryValue {
return &_combo{selected, options, change}
}
func (c *_combo) set(v int) {
*c.selected = v
if c.change != nil {
c.change(v)
}
}
func (c *_combo) Construct() gtk.IWidget {
var combo, _ = gtk.ComboBoxTextNew()
for _, opt := range c.options {
combo.Append(opt, opt)
}
combo.Connect("changed", func() {
active := combo.GetActive()
*c.selected = active
if c.change != nil {
c.change(active)
}
})
combo.Connect("changed", func() { c.set(combo.GetActive()) })
combo.SetActive(*c.selected)
combo.SetHAlign(gtk.ALIGN_END)
combo.Show()
@ -67,19 +66,17 @@ func Switch(value *bool, change func(bool)) EntryValue {
return &_switch{value, change}
}
func (s *_switch) set(v bool) {
*s.value = v
if s.change != nil {
s.change(v)
}
}
func (s *_switch) Construct() gtk.IWidget {
sw, _ := gtk.SwitchNew()
sw.SetActive(*s.value)
sw.Connect("notify::active", func() {
v := sw.GetActive()
*s.value = v
if s.change != nil {
s.change(v)
}
})
sw.Connect("notify::active", func() { s.set(sw.GetActive()) })
sw.SetHAlign(gtk.ALIGN_END)
sw.Show()
@ -95,7 +92,7 @@ func (s *_switch) UnmarshalJSON(b []byte) error {
if err := json.Unmarshal(b, &value); err != nil {
return err
}
*s.value = value
s.set(value)
return nil
}
@ -108,6 +105,14 @@ func InputEntry(value *string, change func(string) error) EntryValue {
return &_inputentry{value, change}
}
func (e *_inputentry) set(v string) error {
*e.value = v
if e.change != nil {
return e.change(v)
}
return nil
}
func (e *_inputentry) Construct() gtk.IWidget {
entry, _ := gtk.EntryNew()
entry.SetHExpand(true)
@ -119,14 +124,11 @@ func (e *_inputentry) Construct() gtk.IWidget {
return
}
*e.value = v
if e.change != nil {
if err := e.change(v); err != nil {
entry.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "dialog-error")
entry.SetIconTooltipText(gtk.ENTRY_ICON_SECONDARY, err.Error())
} else {
entry.RemoveIcon(gtk.ENTRY_ICON_SECONDARY)
}
if err := e.set(v); err != nil {
entry.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "dialog-error")
entry.SetIconTooltipText(gtk.ENTRY_ICON_SECONDARY, err.Error())
} else {
entry.RemoveIcon(gtk.ENTRY_ICON_SECONDARY)
}
})
@ -144,6 +146,6 @@ func (e *_inputentry) UnmarshalJSON(b []byte) error {
if err := json.Unmarshal(b, &value); err != nil {
return err
}
*e.value = value
e.set(value)
return nil
}

View File

@ -1,10 +1,15 @@
package primitives
import (
"path/filepath"
"runtime"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
type Namer interface {
@ -225,3 +230,22 @@ func ActionPopover(p *gtk.Popover, actions [][2]string) {
box.Show()
p.Add(box)
}
func PrepareCSS(css string) *gtk.CssProvider {
p, _ := gtk.CssProviderNew()
if err := p.LoadFromData(css); err != nil {
_, fn, caller, _ := runtime.Caller(1)
fn = filepath.Base(fn)
log.Error(errors.Wrapf(err, "CSS fail at %s:%d", fn, caller))
}
return p
}
type StyleContextGetter interface {
GetStyleContext() (*gtk.StyleContext, error)
}
func AttachCSS(ctx StyleContextGetter, prov *gtk.CssProvider) {
s, _ := ctx.GetStyleContext()
s.AddProvider(prov, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
}

View File

@ -0,0 +1,64 @@
package config
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
"github.com/gotk3/gotk3/gtk"
)
type Configurator interface {
cchat.Service
cchat.Configurator
}
func MenuItem(conf Configurator) menu.Item {
return menu.SimpleItem("Configure", func() {
SpawnConfigurator(conf)
})
}
func SpawnConfigurator(conf Configurator) error {
c, err := conf.Configuration()
if err != nil {
return err
}
Spawn(conf.Name().Content, c, func() error {
return conf.SetConfiguration(c)
})
return nil
}
func Spawn(name string, conf map[string]string, apply func() error) {
container := newContainer(conf, apply)
container.Grid.SetVAlign(gtk.ALIGN_START)
sw, _ := gtk.ScrolledWindowNew(nil, nil)
sw.Add(container.Grid)
sw.Show()
b, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, vmargin)
b.SetMarginTop(vmargin)
b.SetMarginBottom(vmargin)
b.SetMarginStart(hmargin)
b.SetMarginEnd(hmargin)
b.PackStart(sw, true, true, 0)
b.PackStart(container.ErrHeader, false, false, 0)
b.Show()
var title = "Configure " + name
h, _ := gtk.HeaderBarNew()
h.SetTitle(title)
h.SetShowCloseButton(true)
h.Show()
d, _ := gts.NewEmptyModalDialog()
d.SetDefaultSize(400, 300)
d.Add(b)
d.SetTitle(title)
d.SetTitlebar(h)
d.Show()
}

View File

@ -0,0 +1,163 @@
package config
import (
"errors"
"html"
"sort"
"strings"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/gtk"
)
const (
vmargin = 8
hmargin = 16
)
var monospaceCSS = primitives.PrepareCSS(`
* {
font-family: monospace;
}
`)
type container struct {
Grid *gtk.Grid
ErrHeader *errorHeader
Entries map[string]*entry
fielderr *cchat.ErrInvalidConfigAtField
apply func() error
}
func newContainer(conf map[string]string, apply func() error) *container {
var keys = make([]string, 0, len(conf))
for k := range conf {
keys = append(keys, k)
}
sort.Strings(keys)
errh := newErrorHeader()
errh.Show()
var grid, _ = gtk.GridNew()
var entries = make(map[string]*entry, len(keys))
var cc = &container{
Grid: grid,
ErrHeader: errh,
Entries: entries,
fielderr: &cchat.ErrInvalidConfigAtField{},
apply: apply,
}
for i, k := range keys {
l, _ := gtk.LabelNew(k)
l.SetHExpand(true)
l.SetXAlign(0)
l.Show()
primitives.AttachCSS(l, monospaceCSS)
e := newEntry(k, conf, cc.onEntryChange)
e.Show()
entries[k] = e
grid.Attach(l, 0, i, 1, 1)
grid.Attach(e, 1, i, 1, 1)
}
grid.SetRowHomogeneous(true)
grid.SetRowSpacing(4)
grid.SetColumnHomogeneous(true)
grid.SetColumnSpacing(8)
grid.Show()
return cc
}
func (c *container) onEntryChange() {
err := c.apply()
c.ErrHeader.SetError(err)
// Reset the field error before unmarshaling into it again. If As() fails,
// then all field errors will be cleared, as no keys will match.
c.fielderr.Key = ""
// fieldErred is true if the error is a field-specific error.
var fieldErred = errors.As(err, &c.fielderr)
// Loop over entries even if there is no field error. This clears up all
// errors even if that is not the case.
for k, entry := range c.Entries {
if fieldErred && k == c.fielderr.Key {
entry.SetError(c.fielderr.Err)
} else {
entry.SetError(nil)
}
}
}
type entry struct {
*gtk.Entry
}
func newEntry(k string, conf map[string]string, change func()) *entry {
e, _ := gtk.EntryNew()
e.SetText(conf[k])
e.SetHExpand(true)
e.Connect("changed", func() {
conf[k], _ = e.GetText()
change()
})
primitives.AttachCSS(e, monospaceCSS)
return &entry{e}
}
func (e *entry) SetError(err error) {
if err != nil {
e.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "dialog-error")
e.SetIconTooltipText(gtk.ENTRY_ICON_SECONDARY, err.Error())
} else {
e.RemoveIcon(gtk.ENTRY_ICON_SECONDARY)
}
}
type errorHeader struct {
*gtk.Revealer
l *gtk.Label
}
func newErrorHeader() *errorHeader {
l, _ := gtk.LabelNew("")
l.SetXAlign(0)
l.Show()
r, _ := gtk.RevealerNew()
r.SetTransitionDuration(50)
r.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_UP)
r.SetRevealChild(false)
r.SetMarginTop(vmargin)
r.SetMarginBottom(vmargin)
r.Add(l)
return &errorHeader{r, l}
}
func (eh *errorHeader) SetError(err error) {
if err != nil {
// Cleanup the error message.
parts := strings.Split(err.Error(), ": ")
ermsg := parts[len(parts)-1]
eh.SetRevealChild(true)
eh.l.SetMarkup(`<span color="red">Error:</span> ` + html.EscapeString(ermsg))
eh.l.SetTooltipText(err.Error())
} else {
eh.SetRevealChild(false)
eh.l.SetText("")
eh.l.SetTooltipText("")
}
}

View File

@ -2,8 +2,9 @@ package service
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/service/config"
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
)
@ -18,7 +19,7 @@ type header struct {
icon *rich.Icon
Add *gtk.Button
Menu *gtk.Menu
Menu *menu.LazyMenu
}
func newHeader(svc cchat.Service) *header {
@ -58,9 +59,11 @@ func newHeader(svc cchat.Service) *header {
reveal.SetMode(true)
reveal.Show()
// Spawn the menu on right click.
menu, _ := gtk.MenuNew()
primitives.BindMenu(reveal, menu)
// Construct a menu and its items.
var menu = menu.NewLazyMenu(reveal)
if configurator, ok := svc.(config.Configurator); ok {
menu.AddItems(config.MenuItem(configurator))
}
return &header{reveal, box, l, i, add, menu}
}

View File

@ -28,6 +28,10 @@ func (m *LazyMenu) AddItems(items ...Item) {
m.items = append(m.items, items...)
}
func (m *LazyMenu) AddSimpleItem(name string, fn func()) {
m.AddItems(SimpleItem(name, fn))
}
func (m *LazyMenu) Reset() {
m.items = nil
}

View File

@ -117,12 +117,8 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
ctrl.AuthenticateSession(container, svc)
})
// Make menu items.
primitives.AppendMenuItems(header.Menu, []gtk.IMenuItem{
primitives.MenuItem("Save Sessions", func() {
container.SaveAllSessions()
}),
})
// Add more menu item(s).
header.Menu.AddSimpleItem("Save Sessions", container.SaveAllSessions)
return container
}