mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-01-09 03:56:45 +00:00
Added Configurator
This commit is contained in:
parent
5499788460
commit
5ac2567610
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ func Section(entries []config.Entry) *gtk.Grid {
|
|||
}
|
||||
|
||||
grid.SetRowSpacing(4)
|
||||
grid.SetColumnSpacing(4)
|
||||
grid.SetColumnSpacing(8)
|
||||
grid.Show()
|
||||
return grid
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
64
internal/ui/service/config/config.go
Normal file
64
internal/ui/service/config/config.go
Normal 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()
|
||||
}
|
163
internal/ui/service/config/widgets.go
Normal file
163
internal/ui/service/config/widgets.go
Normal 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("")
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue