mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-11-27 06:46:15 +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/gdk"
|
||||||
"github.com/gotk3/gotk3/glib"
|
"github.com/gotk3/gotk3/glib"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const AppID = "com.github.diamondburned.cchat-gtk"
|
const AppID = "com.github.diamondburned.cchat-gtk"
|
||||||
|
|
@ -30,6 +31,23 @@ func NewModalDialog() (*gtk.Dialog, error) {
|
||||||
}
|
}
|
||||||
d.SetModal(true)
|
d.SetModal(true)
|
||||||
d.SetTransientFor(App.Window)
|
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
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ func Section(entries []config.Entry) *gtk.Grid {
|
||||||
}
|
}
|
||||||
|
|
||||||
grid.SetRowSpacing(4)
|
grid.SetRowSpacing(4)
|
||||||
grid.SetColumnSpacing(4)
|
grid.SetColumnSpacing(8)
|
||||||
grid.Show()
|
grid.Show()
|
||||||
return grid
|
return grid
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,21 +23,20 @@ func Combo(selected *int, options []string, change func(int)) EntryValue {
|
||||||
return &_combo{selected, options, change}
|
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 {
|
func (c *_combo) Construct() gtk.IWidget {
|
||||||
var combo, _ = gtk.ComboBoxTextNew()
|
var combo, _ = gtk.ComboBoxTextNew()
|
||||||
for _, opt := range c.options {
|
for _, opt := range c.options {
|
||||||
combo.Append(opt, opt)
|
combo.Append(opt, opt)
|
||||||
}
|
}
|
||||||
|
|
||||||
combo.Connect("changed", func() {
|
combo.Connect("changed", func() { c.set(combo.GetActive()) })
|
||||||
active := combo.GetActive()
|
|
||||||
*c.selected = active
|
|
||||||
|
|
||||||
if c.change != nil {
|
|
||||||
c.change(active)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
combo.SetActive(*c.selected)
|
combo.SetActive(*c.selected)
|
||||||
combo.SetHAlign(gtk.ALIGN_END)
|
combo.SetHAlign(gtk.ALIGN_END)
|
||||||
combo.Show()
|
combo.Show()
|
||||||
|
|
@ -67,19 +66,17 @@ func Switch(value *bool, change func(bool)) EntryValue {
|
||||||
return &_switch{value, change}
|
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 {
|
func (s *_switch) Construct() gtk.IWidget {
|
||||||
sw, _ := gtk.SwitchNew()
|
sw, _ := gtk.SwitchNew()
|
||||||
sw.SetActive(*s.value)
|
sw.SetActive(*s.value)
|
||||||
|
sw.Connect("notify::active", func() { s.set(sw.GetActive()) })
|
||||||
sw.Connect("notify::active", func() {
|
|
||||||
v := sw.GetActive()
|
|
||||||
*s.value = v
|
|
||||||
|
|
||||||
if s.change != nil {
|
|
||||||
s.change(v)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sw.SetHAlign(gtk.ALIGN_END)
|
sw.SetHAlign(gtk.ALIGN_END)
|
||||||
sw.Show()
|
sw.Show()
|
||||||
|
|
||||||
|
|
@ -95,7 +92,7 @@ func (s *_switch) UnmarshalJSON(b []byte) error {
|
||||||
if err := json.Unmarshal(b, &value); err != nil {
|
if err := json.Unmarshal(b, &value); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
*s.value = value
|
s.set(value)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,6 +105,14 @@ func InputEntry(value *string, change func(string) error) EntryValue {
|
||||||
return &_inputentry{value, change}
|
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 {
|
func (e *_inputentry) Construct() gtk.IWidget {
|
||||||
entry, _ := gtk.EntryNew()
|
entry, _ := gtk.EntryNew()
|
||||||
entry.SetHExpand(true)
|
entry.SetHExpand(true)
|
||||||
|
|
@ -119,14 +124,11 @@ func (e *_inputentry) Construct() gtk.IWidget {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
*e.value = v
|
if err := e.set(v); err != nil {
|
||||||
if e.change != nil {
|
entry.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "dialog-error")
|
||||||
if err := e.change(v); err != nil {
|
entry.SetIconTooltipText(gtk.ENTRY_ICON_SECONDARY, err.Error())
|
||||||
entry.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "dialog-error")
|
} else {
|
||||||
entry.SetIconTooltipText(gtk.ENTRY_ICON_SECONDARY, err.Error())
|
entry.RemoveIcon(gtk.ENTRY_ICON_SECONDARY)
|
||||||
} 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 {
|
if err := json.Unmarshal(b, &value); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
*e.value = value
|
e.set(value)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
package primitives
|
package primitives
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
"github.com/gotk3/gotk3/glib"
|
"github.com/gotk3/gotk3/glib"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Namer interface {
|
type Namer interface {
|
||||||
|
|
@ -225,3 +230,22 @@ func ActionPopover(p *gtk.Popover, actions [][2]string) {
|
||||||
box.Show()
|
box.Show()
|
||||||
p.Add(box)
|
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 (
|
import (
|
||||||
"github.com/diamondburned/cchat"
|
"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/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/diamondburned/imgutil"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
@ -18,7 +19,7 @@ type header struct {
|
||||||
icon *rich.Icon
|
icon *rich.Icon
|
||||||
Add *gtk.Button
|
Add *gtk.Button
|
||||||
|
|
||||||
Menu *gtk.Menu
|
Menu *menu.LazyMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHeader(svc cchat.Service) *header {
|
func newHeader(svc cchat.Service) *header {
|
||||||
|
|
@ -58,9 +59,11 @@ func newHeader(svc cchat.Service) *header {
|
||||||
reveal.SetMode(true)
|
reveal.SetMode(true)
|
||||||
reveal.Show()
|
reveal.Show()
|
||||||
|
|
||||||
// Spawn the menu on right click.
|
// Construct a menu and its items.
|
||||||
menu, _ := gtk.MenuNew()
|
var menu = menu.NewLazyMenu(reveal)
|
||||||
primitives.BindMenu(reveal, menu)
|
if configurator, ok := svc.(config.Configurator); ok {
|
||||||
|
menu.AddItems(config.MenuItem(configurator))
|
||||||
|
}
|
||||||
|
|
||||||
return &header{reveal, box, l, i, add, menu}
|
return &header{reveal, box, l, i, add, menu}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ func (m *LazyMenu) AddItems(items ...Item) {
|
||||||
m.items = append(m.items, items...)
|
m.items = append(m.items, items...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *LazyMenu) AddSimpleItem(name string, fn func()) {
|
||||||
|
m.AddItems(SimpleItem(name, fn))
|
||||||
|
}
|
||||||
|
|
||||||
func (m *LazyMenu) Reset() {
|
func (m *LazyMenu) Reset() {
|
||||||
m.items = nil
|
m.items = nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,12 +117,8 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
|
||||||
ctrl.AuthenticateSession(container, svc)
|
ctrl.AuthenticateSession(container, svc)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Make menu items.
|
// Add more menu item(s).
|
||||||
primitives.AppendMenuItems(header.Menu, []gtk.IMenuItem{
|
header.Menu.AddSimpleItem("Save Sessions", container.SaveAllSessions)
|
||||||
primitives.MenuItem("Save Sessions", func() {
|
|
||||||
container.SaveAllSessions()
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
return container
|
return container
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue