mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2024-12-22 20:27:07 +00:00
Final commit before partial rewrite
This commit is contained in:
parent
ee657a8177
commit
b852498ee4
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
cchat-gtk
|
8
go.mod
8
go.mod
|
@ -2,12 +2,16 @@ module github.com/diamondburned/cchat-gtk
|
|||
|
||||
go 1.14
|
||||
|
||||
replace github.com/diamondburned/cchat-mock => ../cchat-mock/
|
||||
|
||||
require (
|
||||
github.com/Xuanwo/go-locale v0.2.0
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/diamondburned/cchat v0.0.9
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200525222906-807afeffb7d4
|
||||
github.com/diamondburned/cchat v0.0.15
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200604043646-de5384bd320d
|
||||
github.com/goodsign/monday v1.0.0
|
||||
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717
|
||||
golang.org/x/tools v0.0.0-20200529172331-a64b76657301 // indirect
|
||||
)
|
||||
|
|
37
go.sum
37
go.sum
|
@ -2,15 +2,23 @@ github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00
|
|||
github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y=
|
||||
github.com/Xuanwo/go-locale v0.2.0 h1:1N8SGG2VNpLl6VVa8ueZm3Nm+dxvk8ffY9aviKHl4IE=
|
||||
github.com/Xuanwo/go-locale v0.2.0/go.mod h1:6qbT9M726OJgyiGZro2YwPmx63wQzlH+VvtjJWQoftw=
|
||||
github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU=
|
||||
github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/diamondburned/cchat v0.0.9 h1:+F96eDDuaOg4v4dz3GBDWbEW4dZ/k5uGrDp33/yeXR8=
|
||||
github.com/diamondburned/cchat v0.0.9/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200525222906-807afeffb7d4 h1:k6vfDs6NR8yIi2V7YEpNp9Vujtl6mpDL5cWh7Pg09kk=
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200525222906-807afeffb7d4/go.mod h1:mOf8RsTQUOf9qJ1Z/OKbsMKnCMtA2gRexH12fc9LxiQ=
|
||||
github.com/diamondburned/cchat v0.0.10 h1:aiUVgGre5E/HV+Iw6tmBVbuGctQI+JndV9nIDoYuRPY=
|
||||
github.com/diamondburned/cchat v0.0.10/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.13 h1:p8SyFjiRVCTjvwSJ4FsICGVYVZ3g0Iu02FrwmLuKiKE=
|
||||
github.com/diamondburned/cchat v0.0.13/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.15 h1:1o4OX8zw/CdSv3Idaylz7vjHVOZKEi/xkg8BpEvtsHY=
|
||||
github.com/diamondburned/cchat v0.0.15/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200529184140-47fa2491d2fc h1:xYSN3re1QOd5af5zG15pLKfBM+fergw7Rg62UHmE22g=
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200529184140-47fa2491d2fc/go.mod h1:rQm5EKhNyBRYHKtirSkf+Db23nr3mTs2bnOThfTfzec=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/goodsign/monday v1.0.0 h1:Yyk/s/WgudMbAJN6UWSU5xAs8jtNewfqtVblAlw0yoc=
|
||||
github.com/goodsign/monday v1.0.0/go.mod h1:r4T4breXpoFwspQNM+u2sLxJb2zyTaxVGqUfTBjWOu8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
|
@ -27,17 +35,38 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
|
|||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717 h1:3M/uUZajYn/082wzUajekePxpUAZhMTfXvI9R+26SJ0=
|
||||
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200529172331-a64b76657301 h1:G6CNEgFU8/XwexSnuFw+Jq/WePjRitgy6ofBcPnAIPo=
|
||||
golang.org/x/tools v0.0.0-20200529172331-a64b76657301/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
|
|
|
@ -27,7 +27,7 @@ func init() {
|
|||
},
|
||||
}
|
||||
|
||||
App.Application, _ = gtk.ApplicationNew("com.github.diamondburned.gtkcord3", 0)
|
||||
App.Application, _ = gtk.ApplicationNew("com.github.diamondburned.cchat-gtk", 0)
|
||||
}
|
||||
|
||||
type Windower interface {
|
||||
|
@ -52,7 +52,7 @@ func Main(wfn func() WindowHeaderer) {
|
|||
App.Header.Show()
|
||||
|
||||
App.Window, _ = gtk.ApplicationWindowNew(App.Application)
|
||||
App.Window.SetDefaultSize(750, 400)
|
||||
App.Window.SetDefaultSize(1000, 500)
|
||||
App.Window.SetTitlebar(App.Header)
|
||||
App.Window.Show()
|
||||
|
||||
|
|
89
internal/keyring/keyring.go
Normal file
89
internal/keyring/keyring.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package keyring
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
func getThenDestroy(service string, v interface{}) error {
|
||||
s, err := keyring.Get("cchat-gtk", service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Deleting immediately does not work on a successful start-up.
|
||||
// keyring.Delete("cchat-gtk", service)
|
||||
|
||||
return gob.NewDecoder(strings.NewReader(s)).Decode(v)
|
||||
}
|
||||
|
||||
func set(service string, v interface{}) error {
|
||||
var b bytes.Buffer
|
||||
if err := gob.NewEncoder(&b).Encode(v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return keyring.Set("cchat-gtk", service, b.String())
|
||||
}
|
||||
|
||||
func SaveSessions(service cchat.Service, sessions []cchat.Session) (saveErrs []error) {
|
||||
var sessionData = make([]map[string]string, 0, len(sessions))
|
||||
|
||||
for _, session := range sessions {
|
||||
sv, ok := session.(cchat.SessionSaver)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
d, err := sv.Save()
|
||||
if err != nil {
|
||||
saveErrs = append(saveErrs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
sessionData = append(sessionData, d)
|
||||
}
|
||||
|
||||
if err := set(service.Name(), sessionData); err != nil {
|
||||
log.Warn(errors.Wrap(err, "Error saving session"))
|
||||
saveErrs = append(saveErrs, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// RestoreSessions restores all sessions of the service asynchronously, then
|
||||
// calls the auth callback inside the Gtk main thread.
|
||||
func RestoreSessions(service cchat.Service, auth func(cchat.Session)) {
|
||||
// If the service doesn't support restoring, treat it as a non-error.
|
||||
restorer, ok := service.(cchat.SessionRestorer)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var sessionData []map[string]string
|
||||
|
||||
// Ignore the error, it's not important.
|
||||
if err := getThenDestroy(service.Name(), &sessionData); err != nil {
|
||||
log.Warn(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, data := range sessionData {
|
||||
gts.Async(func() (func(), error) {
|
||||
s, err := restorer.RestoreSession(data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to restore")
|
||||
}
|
||||
|
||||
return func() { auth(s) }, nil
|
||||
})
|
||||
}
|
||||
}
|
|
@ -37,6 +37,10 @@ func Error(err error) {
|
|||
Write("Error: " + err.Error())
|
||||
}
|
||||
|
||||
func Warn(err error) {
|
||||
Write("Warn: " + err.Error())
|
||||
}
|
||||
|
||||
func Write(msg string) {
|
||||
WriteEntry(Entry{
|
||||
Time: time.Now(),
|
||||
|
|
41
internal/ui/message/autoscroll/autoscroll.go
Normal file
41
internal/ui/message/autoscroll/autoscroll.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package autoscroll
|
||||
|
||||
import "github.com/gotk3/gotk3/gtk"
|
||||
|
||||
type ScrolledWindow struct {
|
||||
gtk.ScrolledWindow
|
||||
vadj gtk.Adjustment
|
||||
bottomed bool // :floshed:
|
||||
}
|
||||
|
||||
func NewScrolledWindow() *ScrolledWindow {
|
||||
gtksw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||
gtksw.SetProperty("propagate-natural-height", true)
|
||||
|
||||
sw := &ScrolledWindow{*gtksw, *gtksw.GetVAdjustment(), true} // bottomed by default
|
||||
sw.Connect("size-allocate", func(_ *gtk.ScrolledWindow) {
|
||||
// We can't really trust Gtk to be competent.
|
||||
if sw.bottomed {
|
||||
sw.ScrollToBottom()
|
||||
}
|
||||
})
|
||||
sw.vadj.Connect("value-changed", func(adj *gtk.Adjustment) {
|
||||
// Manually check if we're anchored on scroll.
|
||||
sw.bottomed = (adj.GetUpper() - adj.GetPageSize()) <= adj.GetValue()
|
||||
})
|
||||
|
||||
return sw
|
||||
}
|
||||
|
||||
func (s *ScrolledWindow) Bottomed() bool {
|
||||
return s.bottomed
|
||||
}
|
||||
|
||||
// GetVAdjustment overrides gtk.ScrolledWindow's.
|
||||
func (s *ScrolledWindow) GetVAdjustment() *gtk.Adjustment {
|
||||
return &s.vadj
|
||||
}
|
||||
|
||||
func (s *ScrolledWindow) ScrollToBottom() {
|
||||
s.vadj.SetValue(s.vadj.GetUpper())
|
||||
}
|
156
internal/ui/message/compact/compact.go
Normal file
156
internal/ui/message/compact/compact.go
Normal file
|
@ -0,0 +1,156 @@
|
|||
package compact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/message/autoscroll"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/message/input"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
*autoscroll.ScrolledWindow
|
||||
main *gtk.Grid
|
||||
messages map[string]*Message
|
||||
nonceMsgs map[string]*Message
|
||||
|
||||
bottomed bool
|
||||
}
|
||||
|
||||
func NewContainer() *Container {
|
||||
grid, _ := gtk.GridNew()
|
||||
grid.SetColumnSpacing(10)
|
||||
grid.SetRowSpacing(5)
|
||||
grid.SetMarginStart(5)
|
||||
grid.SetMarginEnd(5)
|
||||
grid.SetMarginBottom(5)
|
||||
grid.Show()
|
||||
|
||||
sw := autoscroll.NewScrolledWindow()
|
||||
sw.Add(grid)
|
||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
|
||||
sw.Show()
|
||||
|
||||
container := Container{
|
||||
ScrolledWindow: sw,
|
||||
main: grid,
|
||||
messages: map[string]*Message{},
|
||||
nonceMsgs: map[string]*Message{},
|
||||
bottomed: true, // bottomed by default.
|
||||
}
|
||||
|
||||
return &container
|
||||
}
|
||||
|
||||
func (c *Container) Reset() {
|
||||
// does this actually work?
|
||||
var rows = c.len()
|
||||
for i := 0; i < rows; i++ {
|
||||
c.main.RemoveRow(i)
|
||||
}
|
||||
|
||||
c.messages = map[string]*Message{}
|
||||
c.nonceMsgs = map[string]*Message{}
|
||||
|
||||
// default to being bottomed
|
||||
c.bottomed = true
|
||||
}
|
||||
|
||||
func (c *Container) len() int {
|
||||
return len(c.messages) + len(c.nonceMsgs)
|
||||
}
|
||||
|
||||
// PresendMessage is not thread-safe.
|
||||
func (c *Container) PresendMessage(msg input.PresendMessage) func(error) {
|
||||
msgc := NewPresendMessage(msg.Content(), msg.Author(), msg.AuthorID(), msg.Nonce())
|
||||
msgc.index = c.len()
|
||||
|
||||
c.nonceMsgs[msgc.Nonce] = &msgc
|
||||
msgc.Attach(c.main, msgc.index)
|
||||
|
||||
return func(err error) {
|
||||
msgc.SetSensitive(true)
|
||||
|
||||
// Did we fail?
|
||||
if err != nil {
|
||||
msgc.Content.SetMarkup(fmt.Sprintf(
|
||||
`<span color="red">%s</span>`,
|
||||
html.EscapeString(msgc.Content.GetLabel()),
|
||||
))
|
||||
msgc.Content.SetTooltipText(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FindMessage is not thread-safe.
|
||||
func (c *Container) FindMessage(msg cchat.MessageHeader) *Message {
|
||||
// Search using the ID first.
|
||||
m, ok := c.messages[msg.ID()]
|
||||
if ok {
|
||||
return m
|
||||
}
|
||||
|
||||
// Is this an existing message?
|
||||
if noncer, ok := msg.(cchat.MessageNonce); ok {
|
||||
var nonce = noncer.Nonce()
|
||||
|
||||
m, ok := c.nonceMsgs[nonce]
|
||||
if ok {
|
||||
// Move the message outside nonceMsgs.
|
||||
delete(c.nonceMsgs, nonce)
|
||||
c.messages[msg.ID()] = m
|
||||
|
||||
// Set the right ID.
|
||||
m.ID = msg.ID()
|
||||
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
||||
gts.ExecAsync(func() {
|
||||
// Attempt update before insert (aka upsert).
|
||||
if msgc := c.FindMessage(msg); msgc != nil {
|
||||
msgc.SetSensitive(true)
|
||||
msgc.UpdateAuthor(msg.Author())
|
||||
msgc.UpdateContent(msg.Content())
|
||||
msgc.UpdateTimestamp(msg.Time())
|
||||
return
|
||||
}
|
||||
|
||||
msgc := NewMessage(msg)
|
||||
msgc.index = c.len() // unsure
|
||||
|
||||
c.messages[msgc.ID] = &msgc
|
||||
msgc.Attach(c.main, msgc.index)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
|
||||
gts.ExecAsync(func() {
|
||||
if msgc := c.FindMessage(msg); msgc != nil {
|
||||
if author := msg.Author(); author != nil {
|
||||
msgc.UpdateAuthor(author)
|
||||
}
|
||||
if content := msg.Content(); !content.Empty() {
|
||||
msgc.UpdateContent(content)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||
gts.ExecAsync(func() {
|
||||
// TODO: add nonce check.
|
||||
if m, ok := c.messages[msg.ID()]; ok {
|
||||
delete(c.messages, msg.ID())
|
||||
c.main.RemoveRow(m.index)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package message
|
||||
package compact
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
@ -11,9 +11,10 @@ import (
|
|||
)
|
||||
|
||||
type Message struct {
|
||||
index int
|
||||
ID string
|
||||
Nonce string
|
||||
index int
|
||||
ID string
|
||||
AuthorID string
|
||||
Nonce string
|
||||
|
||||
Timestamp *gtk.Label
|
||||
Username *gtk.Label
|
||||
|
@ -21,34 +22,8 @@ type Message struct {
|
|||
}
|
||||
|
||||
func NewMessage(msg cchat.MessageCreate) Message {
|
||||
ts, _ := gtk.LabelNew("")
|
||||
ts.SetLineWrap(true)
|
||||
ts.SetLineWrapMode(pango.WRAP_WORD)
|
||||
ts.SetHAlign(gtk.ALIGN_END)
|
||||
ts.SetVAlign(gtk.ALIGN_START)
|
||||
ts.Show()
|
||||
|
||||
user, _ := gtk.LabelNew("")
|
||||
user.SetLineWrap(true)
|
||||
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||
user.SetHAlign(gtk.ALIGN_END)
|
||||
user.SetVAlign(gtk.ALIGN_START)
|
||||
user.Show()
|
||||
|
||||
content, _ := gtk.LabelNew("")
|
||||
content.SetHExpand(true)
|
||||
content.SetXAlign(0) // left-align with size filled
|
||||
content.SetVAlign(gtk.ALIGN_START)
|
||||
content.SetLineWrap(true)
|
||||
content.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||
content.Show()
|
||||
|
||||
m := Message{
|
||||
ID: msg.ID(),
|
||||
Timestamp: ts,
|
||||
Username: user,
|
||||
Content: content,
|
||||
}
|
||||
m := NewEmptyMessage()
|
||||
m.ID = msg.ID()
|
||||
m.UpdateTimestamp(msg.Time())
|
||||
m.UpdateAuthor(msg.Author())
|
||||
m.UpdateContent(msg.Content())
|
||||
|
@ -60,6 +35,58 @@ func NewMessage(msg cchat.MessageCreate) Message {
|
|||
return m
|
||||
}
|
||||
|
||||
func NewPresendMessage(content string, author text.Rich, authorID, nonce string) Message {
|
||||
msgc := NewEmptyMessage()
|
||||
msgc.Nonce = nonce
|
||||
msgc.AuthorID = authorID
|
||||
msgc.SetSensitive(false)
|
||||
msgc.UpdateContent(text.Rich{Content: content})
|
||||
msgc.UpdateTimestamp(time.Now())
|
||||
msgc.updateAuthorName(author)
|
||||
|
||||
return msgc
|
||||
}
|
||||
|
||||
func NewEmptyMessage() Message {
|
||||
ts, _ := gtk.LabelNew("")
|
||||
ts.SetLineWrap(true)
|
||||
ts.SetLineWrapMode(pango.WRAP_WORD)
|
||||
ts.SetHAlign(gtk.ALIGN_END)
|
||||
ts.SetVAlign(gtk.ALIGN_START)
|
||||
ts.SetSelectable(true)
|
||||
ts.Show()
|
||||
|
||||
user, _ := gtk.LabelNew("")
|
||||
user.SetMaxWidthChars(35)
|
||||
user.SetLineWrap(true)
|
||||
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||
user.SetHAlign(gtk.ALIGN_END)
|
||||
user.SetVAlign(gtk.ALIGN_START)
|
||||
user.SetSelectable(true)
|
||||
user.Show()
|
||||
|
||||
content, _ := gtk.LabelNew("")
|
||||
content.SetHExpand(true)
|
||||
content.SetXAlign(0) // left-align with size filled
|
||||
content.SetVAlign(gtk.ALIGN_START)
|
||||
content.SetLineWrap(true)
|
||||
content.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||
content.SetSelectable(true)
|
||||
content.Show()
|
||||
|
||||
return Message{
|
||||
Timestamp: ts,
|
||||
Username: user,
|
||||
Content: content,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) SetSensitive(sensitive bool) {
|
||||
m.Timestamp.SetSensitive(sensitive)
|
||||
m.Username.SetSensitive(sensitive)
|
||||
m.Content.SetSensitive(sensitive)
|
||||
}
|
||||
|
||||
func (m *Message) Attach(grid *gtk.Grid, row int) {
|
||||
grid.Attach(m.Timestamp, 0, row, 1, 1)
|
||||
grid.Attach(m.Username, 1, row, 1, 1)
|
||||
|
@ -68,10 +95,17 @@ func (m *Message) Attach(grid *gtk.Grid, row int) {
|
|||
|
||||
func (m *Message) UpdateTimestamp(t time.Time) {
|
||||
m.Timestamp.SetLabel(humanize.TimeAgo(t))
|
||||
m.Timestamp.SetTooltipText(t.Format(time.Stamp))
|
||||
}
|
||||
|
||||
func (m *Message) UpdateAuthor(author text.Rich) {
|
||||
m.Username.SetLabel(author.Content)
|
||||
func (m *Message) UpdateAuthor(author cchat.MessageAuthor) {
|
||||
m.AuthorID = author.ID()
|
||||
m.updateAuthorName(author.Name())
|
||||
}
|
||||
|
||||
func (m *Message) updateAuthorName(name text.Rich) {
|
||||
m.Username.SetLabel(name.Content)
|
||||
m.Username.SetTooltipText(name.Content)
|
||||
}
|
||||
|
||||
func (m *Message) UpdateContent(content text.Rich) {
|
|
@ -1,74 +0,0 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
*gtk.ScrolledWindow
|
||||
main *gtk.Grid
|
||||
messages map[string]Message
|
||||
}
|
||||
|
||||
func NewContainer() *Container {
|
||||
grid, _ := gtk.GridNew()
|
||||
grid.SetColumnSpacing(8)
|
||||
grid.SetRowSpacing(5)
|
||||
grid.SetMarginStart(5)
|
||||
grid.SetMarginEnd(5)
|
||||
grid.Show()
|
||||
|
||||
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||
sw.Add(grid)
|
||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
|
||||
sw.Show()
|
||||
|
||||
return &Container{sw, grid, map[string]Message{}}
|
||||
}
|
||||
|
||||
func (c *Container) Reset() {
|
||||
// does this actually work?
|
||||
var rows = len(c.messages)
|
||||
for i := 0; i < rows; i++ {
|
||||
c.main.RemoveRow(i)
|
||||
}
|
||||
|
||||
c.messages = nil
|
||||
}
|
||||
|
||||
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
||||
gts.ExecAsync(func() {
|
||||
msgc := NewMessage(msg)
|
||||
msgc.index = len(c.messages) // unsure
|
||||
|
||||
c.messages[msgc.ID] = msgc
|
||||
msgc.Attach(c.main, msgc.index)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
|
||||
gts.ExecAsync(func() {
|
||||
mc, ok := c.messages[msg.ID()]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if author := msg.Author(); !author.Empty() {
|
||||
mc.UpdateAuthor(author)
|
||||
}
|
||||
if content := msg.Content(); !content.Empty() {
|
||||
mc.UpdateContent(content)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||
gts.ExecAsync(func() {
|
||||
if m, ok := c.messages[msg.ID()]; ok {
|
||||
delete(c.messages, msg.ID())
|
||||
c.main.RemoveRow(m.index)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -2,42 +2,115 @@ package input
|
|||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Field struct {
|
||||
*gtk.ScrolledWindow
|
||||
text *gtk.TextView
|
||||
buffer *gtk.TextBuffer
|
||||
*gtk.Box
|
||||
|
||||
namerev *gtk.Revealer
|
||||
username *rich.Label // TODO
|
||||
|
||||
TextScroll *gtk.ScrolledWindow
|
||||
text *gtk.TextView
|
||||
buffer *gtk.TextBuffer
|
||||
|
||||
UserID string
|
||||
|
||||
sender cchat.ServerMessageSender
|
||||
ctrl Controller
|
||||
}
|
||||
|
||||
func NewField() *Field {
|
||||
type Controller interface {
|
||||
PresendMessage(msg PresendMessage) (onErr func(error))
|
||||
}
|
||||
|
||||
const inputmargin = 3
|
||||
|
||||
func NewField(ctrl Controller) *Field {
|
||||
username := rich.NewLabel(text.Rich{})
|
||||
username.SetMaxWidthChars(35)
|
||||
username.SetVAlign(gtk.ALIGN_START)
|
||||
username.SetMarginTop(inputmargin)
|
||||
username.SetMarginBottom(inputmargin)
|
||||
username.SetMarginStart(10)
|
||||
username.SetMarginEnd(10)
|
||||
username.Show()
|
||||
|
||||
namerev, _ := gtk.RevealerNew()
|
||||
namerev.SetRevealChild(false)
|
||||
namerev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
|
||||
namerev.SetTransitionDuration(50)
|
||||
namerev.Add(username)
|
||||
namerev.Show()
|
||||
|
||||
text, _ := gtk.TextViewNew()
|
||||
text.SetSensitive(false)
|
||||
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||
text.SetProperty("top-margin", inputmargin)
|
||||
text.SetProperty("left-margin", inputmargin)
|
||||
text.SetProperty("right-margin", inputmargin)
|
||||
text.SetProperty("bottom-margin", inputmargin)
|
||||
text.Show()
|
||||
|
||||
buf, _ := text.GetBuffer()
|
||||
|
||||
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||
sw.Show()
|
||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||
sw.SetProperty("max-content-height", 150)
|
||||
sw.Add(text)
|
||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||
sw.SetProperty("propagate-natural-height", true)
|
||||
sw.SetProperty("max-content-height", 150)
|
||||
sw.Show()
|
||||
|
||||
return &Field{
|
||||
sw,
|
||||
text,
|
||||
buf,
|
||||
nil,
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
box.PackStart(namerev, false, false, 0)
|
||||
box.PackStart(sw, true, true, 0)
|
||||
box.Show()
|
||||
|
||||
field := &Field{
|
||||
Box: box,
|
||||
namerev: namerev,
|
||||
username: username,
|
||||
TextScroll: sw,
|
||||
text: text,
|
||||
buffer: buf,
|
||||
ctrl: ctrl,
|
||||
}
|
||||
|
||||
text.SetFocusHAdjustment(sw.GetHAdjustment())
|
||||
text.SetFocusVAdjustment(sw.GetVAdjustment())
|
||||
text.Connect("key-press-event", field.keyDown)
|
||||
|
||||
return field
|
||||
}
|
||||
|
||||
// SetSender changes the sender of the input field. If nil, the input will be
|
||||
// disabled.
|
||||
func (f *Field) SetSender(sender cchat.ServerMessageSender) {
|
||||
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
||||
f.UserID = session.ID()
|
||||
|
||||
// Does sender (aka Server) implement ServerNickname?
|
||||
var err error
|
||||
if nicknamer, ok := sender.(cchat.ServerNickname); ok {
|
||||
err = errors.Wrap(nicknamer.Nickname(f.username), "Failed to get nickname")
|
||||
} else {
|
||||
err = errors.Wrap(session.Name(f.username), "Failed to get username")
|
||||
}
|
||||
|
||||
// Do a bit of trivial error handling.
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
|
||||
// Reveal if the name is not empty.
|
||||
f.namerev.SetRevealChild(!f.username.GetLabel().Empty())
|
||||
|
||||
// Set the sender.
|
||||
f.sender = sender
|
||||
f.text.SetSensitive(sender != nil) // grey if sender is nil
|
||||
|
||||
|
@ -58,18 +131,24 @@ func (f *Field) SendMessage() {
|
|||
}
|
||||
|
||||
var sender = f.sender
|
||||
var data = NewSendMessageData(text, f.username.GetLabel(), f.UserID)
|
||||
|
||||
// presend message into the container through the controller
|
||||
var done = f.ctrl.PresendMessage(data)
|
||||
|
||||
go func() {
|
||||
if err := sender.SendMessage(SendMessageData(text)); err != nil {
|
||||
err := sender.SendMessage(data)
|
||||
|
||||
gts.ExecAsync(func() {
|
||||
done(err)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to send message"))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type SendMessageData string
|
||||
|
||||
func (s SendMessageData) Content() string { return string(s) }
|
||||
|
||||
// yankText cuts the text from the input field and returns it.
|
||||
func (f *Field) yankText() string {
|
||||
start, end := f.buffer.GetBounds()
|
||||
|
|
40
internal/ui/message/input/keydown.go
Normal file
40
internal/ui/message/input/keydown.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package input
|
||||
|
||||
import (
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
const shiftMask = uint(gdk.SHIFT_MASK)
|
||||
const cntrlMask = uint(gdk.CONTROL_MASK)
|
||||
|
||||
func bithas(bit, has uint) bool {
|
||||
return bit&has == has
|
||||
}
|
||||
|
||||
func convEvent(ev *gdk.Event) (key, mask uint) {
|
||||
var keyEvent = gdk.EventKeyNewFromEvent(ev)
|
||||
return keyEvent.KeyVal(), keyEvent.State()
|
||||
}
|
||||
|
||||
// connects to key-press-event
|
||||
func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
|
||||
var key, mask = convEvent(ev)
|
||||
|
||||
switch key {
|
||||
// If Enter is pressed.
|
||||
case gdk.KEY_Return:
|
||||
// If Shift is being held, insert a new line.
|
||||
if bithas(mask, shiftMask) {
|
||||
f.buffer.InsertAtCursor("\n")
|
||||
return true
|
||||
}
|
||||
|
||||
// Else, send the message.
|
||||
f.SendMessage()
|
||||
return true
|
||||
}
|
||||
|
||||
// Passthrough.
|
||||
return false
|
||||
}
|
53
internal/ui/message/input/send.go
Normal file
53
internal/ui/message/input/send.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package input
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
type SendMessageData struct {
|
||||
content string
|
||||
author text.Rich
|
||||
authorID string
|
||||
nonce string
|
||||
}
|
||||
|
||||
type PresendMessage interface {
|
||||
cchat.SendableMessage
|
||||
cchat.MessageNonce
|
||||
|
||||
Author() text.Rich
|
||||
AuthorID() string
|
||||
}
|
||||
|
||||
var (
|
||||
_ cchat.SendableMessage = (*SendMessageData)(nil)
|
||||
_ cchat.MessageNonce = (*SendMessageData)(nil)
|
||||
)
|
||||
|
||||
func NewSendMessageData(content string, author text.Rich, authorID string) SendMessageData {
|
||||
return SendMessageData{
|
||||
content: content,
|
||||
author: author,
|
||||
nonce: "cchat-gtk_" + strconv.FormatInt(time.Now().UnixNano(), 10),
|
||||
}
|
||||
}
|
||||
|
||||
func (s SendMessageData) Content() string {
|
||||
return s.content
|
||||
}
|
||||
|
||||
func (s SendMessageData) Author() text.Rich {
|
||||
return s.author
|
||||
}
|
||||
|
||||
func (s SendMessageData) AuthorID() string {
|
||||
return s.authorID
|
||||
}
|
||||
|
||||
func (s SendMessageData) Nonce() string {
|
||||
return s.nonce
|
||||
}
|
|
@ -3,40 +3,55 @@ package message
|
|||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/message/compact"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/message/input"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Container interface {
|
||||
gtk.IWidget
|
||||
cchat.MessagesContainer
|
||||
|
||||
Reset()
|
||||
ScrollToBottom()
|
||||
|
||||
// PresendMessage is for unsent messages.
|
||||
PresendMessage(input.PresendMessage) (done func(sendError error))
|
||||
}
|
||||
|
||||
type View struct {
|
||||
*gtk.Box
|
||||
Container *Container
|
||||
Container Container
|
||||
SendInput *input.Field
|
||||
|
||||
current cchat.ServerMessage
|
||||
author string
|
||||
}
|
||||
|
||||
func NewView() *View {
|
||||
container := NewContainer()
|
||||
sendinput := input.NewField()
|
||||
view := &View{}
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
box.PackStart(container, true, true, 0)
|
||||
box.PackStart(sendinput, false, false, 0)
|
||||
box.Show()
|
||||
view.Container = compact.NewContainer()
|
||||
view.SendInput = input.NewField(view)
|
||||
|
||||
return &View{
|
||||
Box: box,
|
||||
Container: container,
|
||||
SendInput: sendinput,
|
||||
}
|
||||
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
view.Box.PackStart(view.Container, true, true, 0)
|
||||
view.Box.PackStart(view.SendInput, false, false, 0)
|
||||
view.Box.Show()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func (v *View) JoinServer(server cchat.ServerMessage) {
|
||||
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
|
||||
func (v *View) JoinServer(session cchat.Session, server cchat.ServerMessage) {
|
||||
if v.current != nil {
|
||||
if err := v.current.LeaveServer(); err != nil {
|
||||
log.Error(errors.Wrap(err, "Error leaving server"))
|
||||
}
|
||||
// Backend should handle synchronizing joins and leaves if it needs to.
|
||||
go func() {
|
||||
if err := v.current.LeaveServer(); err != nil {
|
||||
log.Error(errors.Wrap(err, "Error leaving server"))
|
||||
}
|
||||
}()
|
||||
|
||||
// Clean all messages.
|
||||
v.Container.Reset()
|
||||
|
@ -47,9 +62,15 @@ func (v *View) JoinServer(server cchat.ServerMessage) {
|
|||
// Skipping ok check because sender can be nil. Without the empty check, Go
|
||||
// will panic.
|
||||
sender, _ := server.(cchat.ServerMessageSender)
|
||||
v.SendInput.SetSender(sender)
|
||||
v.SendInput.SetSender(session, sender)
|
||||
|
||||
if err := v.current.JoinServer(v.Container); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to join server"))
|
||||
}
|
||||
go func() {
|
||||
if err := v.current.JoinServer(v.Container); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to join server"))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (v *View) PresendMessage(msg input.PresendMessage) func(error) {
|
||||
return v.Container.PresendMessage(msg)
|
||||
}
|
||||
|
|
|
@ -21,5 +21,22 @@ var _ Bin = (*gtk.Bin)(nil)
|
|||
|
||||
func BinLeftAlignLabel(bin Bin) {
|
||||
widget, _ := bin.GetChild()
|
||||
widget.(interface{ SetXAlign(float64) }).SetXAlign(0)
|
||||
widget.(interface{ SetHAlign(gtk.Align) }).SetHAlign(gtk.ALIGN_START)
|
||||
}
|
||||
|
||||
func NewButtonIcon(icon string) *gtk.Image {
|
||||
img, _ := gtk.ImageNewFromIconName(icon, gtk.ICON_SIZE_BUTTON)
|
||||
return img
|
||||
}
|
||||
|
||||
func NewImageIconPx(icon string, sizepx int) *gtk.Image {
|
||||
img, _ := gtk.ImageNew()
|
||||
SetImageIcon(img, icon, sizepx)
|
||||
return img
|
||||
}
|
||||
|
||||
func SetImageIcon(img *gtk.Image, icon string, sizepx int) {
|
||||
img.SetProperty("icon-name", icon)
|
||||
img.SetProperty("pixel-size", sizepx)
|
||||
img.SetSizeRequest(sizepx, sizepx)
|
||||
}
|
||||
|
|
1
internal/ui/rich/parser/parser.go
Normal file
1
internal/ui/rich/parser/parser.go
Normal file
|
@ -0,0 +1 @@
|
|||
package parser
|
111
internal/ui/rich/rich.go
Normal file
111
internal/ui/rich/rich.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package rich
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
// TODO: parser
|
||||
|
||||
type Labeler interface {
|
||||
cchat.LabelContainer // thread-safe
|
||||
GetLabel() text.Rich // not thread-safe
|
||||
}
|
||||
|
||||
type Label struct {
|
||||
gtk.Label
|
||||
current text.Rich
|
||||
}
|
||||
|
||||
var (
|
||||
_ gtk.IWidget = (*Label)(nil)
|
||||
_ Labeler = (*Label)(nil)
|
||||
)
|
||||
|
||||
func NewLabel(content text.Rich) *Label {
|
||||
label, _ := gtk.LabelNew(content.Content)
|
||||
label.SetHAlign(gtk.ALIGN_START)
|
||||
return &Label{*label, content}
|
||||
}
|
||||
|
||||
// SetLabel is thread-safe.
|
||||
func (l *Label) SetLabel(content text.Rich) {
|
||||
gts.ExecAsync(func() {
|
||||
l.current = content
|
||||
l.SetText(content.Content)
|
||||
})
|
||||
}
|
||||
|
||||
// GetLabel is NOT thread-safe.
|
||||
func (l *Label) GetLabel() text.Rich {
|
||||
return l.current
|
||||
}
|
||||
|
||||
type ToggleButton struct {
|
||||
gtk.ToggleButton
|
||||
Label
|
||||
}
|
||||
|
||||
var (
|
||||
_ gtk.IWidget = (*ToggleButton)(nil)
|
||||
_ cchat.LabelContainer = (*ToggleButton)(nil)
|
||||
)
|
||||
|
||||
func NewToggleButton(content text.Rich) *ToggleButton {
|
||||
l := NewLabel(content)
|
||||
l.Show()
|
||||
|
||||
b, _ := gtk.ToggleButtonNew()
|
||||
primitives.BinLeftAlignLabel(b)
|
||||
|
||||
b.Add(l)
|
||||
|
||||
return &ToggleButton{*b, *l}
|
||||
}
|
||||
|
||||
type ToggleButtonImage struct {
|
||||
gtk.ToggleButton
|
||||
Labeler
|
||||
|
||||
Label gtk.Label
|
||||
Image gtk.Image
|
||||
|
||||
Box gtk.Box
|
||||
}
|
||||
|
||||
var (
|
||||
_ gtk.IWidget = (*ToggleButton)(nil)
|
||||
_ cchat.LabelContainer = (*ToggleButton)(nil)
|
||||
)
|
||||
|
||||
func NewToggleButtonImage(content text.Rich, iconName string) *ToggleButtonImage {
|
||||
l := NewLabel(content)
|
||||
l.Show()
|
||||
|
||||
var i *gtk.Image
|
||||
if iconName != "" {
|
||||
i, _ = gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_BUTTON)
|
||||
} else {
|
||||
i, _ = gtk.ImageNew()
|
||||
}
|
||||
i.Show()
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
box.PackStart(i, false, false, 0)
|
||||
box.PackStart(l, true, true, 5)
|
||||
box.Show()
|
||||
|
||||
b, _ := gtk.ToggleButtonNew()
|
||||
b.Add(box)
|
||||
|
||||
return &ToggleButtonImage{
|
||||
ToggleButton: *b,
|
||||
Labeler: l, // easy inheritance of methods
|
||||
Label: l.Label,
|
||||
Image: *i,
|
||||
Box: *box,
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/dialog"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
)
|
||||
|
||||
type Dialog struct {
|
||||
|
@ -116,19 +115,20 @@ func (d *Dialog) ok() {
|
|||
type Request struct {
|
||||
*gtk.Grid
|
||||
labels []*gtk.Label
|
||||
entries []*gtk.Entry
|
||||
entries []Texter
|
||||
}
|
||||
|
||||
func NewRequest(authEntries []cchat.AuthenticateEntry) *Request {
|
||||
grid, _ := gtk.GridNew()
|
||||
grid.Show()
|
||||
grid.SetRowHomogeneous(true)
|
||||
grid.SetRowSpacing(2)
|
||||
grid.SetRowSpacing(7)
|
||||
grid.SetColumnHomogeneous(true)
|
||||
grid.SetColumnSpacing(5)
|
||||
|
||||
req := &Request{
|
||||
Grid: grid,
|
||||
labels: make([]*gtk.Label, len(authEntries)),
|
||||
entries: make([]*gtk.Entry, len(authEntries)),
|
||||
entries: make([]Texter, len(authEntries)),
|
||||
}
|
||||
|
||||
for i, authEntry := range authEntries {
|
||||
|
@ -147,29 +147,91 @@ func NewRequest(authEntries []cchat.AuthenticateEntry) *Request {
|
|||
func (r *Request) values() []string {
|
||||
var values = make([]string, len(r.entries))
|
||||
for i, entry := range r.entries {
|
||||
values[i], _ = entry.GetText()
|
||||
values[i] = entry.GetText()
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func newEntry(authEntry cchat.AuthenticateEntry) (*gtk.Label, *gtk.Entry) {
|
||||
func newEntry(authEntry cchat.AuthenticateEntry) (*gtk.Label, Texter) {
|
||||
label, _ := gtk.LabelNew(authEntry.Name)
|
||||
label.Show()
|
||||
label.SetXAlign(1) // right align
|
||||
label.SetEllipsize(pango.ELLIPSIZE_END)
|
||||
label.SetJustify(gtk.JUSTIFY_RIGHT)
|
||||
label.SetLineWrap(true)
|
||||
|
||||
input, _ := gtk.EntryNew()
|
||||
input.Show()
|
||||
var texter Texter
|
||||
|
||||
if authEntry.Secret {
|
||||
input.SetInputPurpose(gtk.INPUT_PURPOSE_PASSWORD)
|
||||
input.SetVisibility(false)
|
||||
input.SetInvisibleChar('●')
|
||||
if authEntry.Multiline {
|
||||
texter = NewMultilineInput()
|
||||
} else {
|
||||
// usually; this is just an assumption
|
||||
input.SetInputPurpose(gtk.INPUT_PURPOSE_EMAIL)
|
||||
var input = NewEntryInput()
|
||||
if authEntry.Secret {
|
||||
input.SetInputPurpose(gtk.INPUT_PURPOSE_PASSWORD)
|
||||
input.SetVisibility(false)
|
||||
input.SetInvisibleChar('●')
|
||||
} else {
|
||||
// usually; this is just an assumption
|
||||
input.SetInputPurpose(gtk.INPUT_PURPOSE_EMAIL)
|
||||
}
|
||||
|
||||
texter = input
|
||||
}
|
||||
|
||||
return label, input
|
||||
return label, texter
|
||||
}
|
||||
|
||||
type Texter interface {
|
||||
gtk.IWidget
|
||||
GetText() string
|
||||
SetText(string)
|
||||
}
|
||||
|
||||
type EntryInput struct {
|
||||
*gtk.Entry
|
||||
}
|
||||
|
||||
var _ Texter = (*EntryInput)(nil)
|
||||
|
||||
func NewEntryInput() EntryInput {
|
||||
input, _ := gtk.EntryNew()
|
||||
input.SetVAlign(gtk.ALIGN_CENTER)
|
||||
input.Show()
|
||||
|
||||
return EntryInput{
|
||||
input,
|
||||
}
|
||||
}
|
||||
|
||||
func (i EntryInput) GetText() (text string) {
|
||||
text, _ = i.Entry.GetText()
|
||||
return
|
||||
}
|
||||
|
||||
type MultilineInput struct {
|
||||
*gtk.TextView
|
||||
Buffer *gtk.TextBuffer
|
||||
}
|
||||
|
||||
var _ Texter = (*MultilineInput)(nil)
|
||||
|
||||
func NewMultilineInput() MultilineInput {
|
||||
view, _ := gtk.TextViewNew()
|
||||
view.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||
view.SetEditable(true)
|
||||
view.Show()
|
||||
|
||||
buf, _ := view.GetBuffer()
|
||||
|
||||
return MultilineInput{view, buf}
|
||||
}
|
||||
|
||||
func (i MultilineInput) GetText() (text string) {
|
||||
start, end := i.Buffer.GetBounds()
|
||||
text, _ = i.Buffer.GetText(start, end, true)
|
||||
return
|
||||
}
|
||||
|
||||
func (i MultilineInput) SetText(text string) {
|
||||
i.Buffer.SetText(text)
|
||||
}
|
||||
|
|
|
@ -3,12 +3,19 @@ package service
|
|||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/auth"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
const IconSize = 32
|
||||
|
||||
type Controller interface {
|
||||
session.Controller
|
||||
AuthenticateSession(*Container, cchat.Service)
|
||||
}
|
||||
|
||||
type View struct {
|
||||
*gtk.ScrolledWindow
|
||||
Box *gtk.Box
|
||||
|
@ -33,10 +40,11 @@ func NewView() *View {
|
|||
}
|
||||
}
|
||||
|
||||
func (v *View) AddService(svc cchat.Service, rowctrl server.RowController) {
|
||||
s := NewContainer(svc, rowctrl)
|
||||
func (v *View) AddService(svc cchat.Service, ctrl Controller) *Container {
|
||||
s := NewContainer(svc, ctrl)
|
||||
v.Services = append(v.Services, s)
|
||||
v.Box.Add(s)
|
||||
return s
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
|
@ -44,19 +52,20 @@ type Container struct {
|
|||
header *header
|
||||
revealer *gtk.Revealer
|
||||
children *children
|
||||
rowctrl server.RowController
|
||||
rowctrl Controller
|
||||
}
|
||||
|
||||
func NewContainer(svc cchat.Service, rowctrl server.RowController) *Container {
|
||||
header := newHeader(svc)
|
||||
|
||||
func NewContainer(svc cchat.Service, ctrl Controller) *Container {
|
||||
children := newChildren()
|
||||
|
||||
chrev, _ := gtk.RevealerNew()
|
||||
chrev.SetRevealChild(false)
|
||||
chrev.SetRevealChild(true)
|
||||
chrev.Add(children)
|
||||
chrev.Show()
|
||||
|
||||
header := newHeader(svc)
|
||||
header.reveal.SetActive(chrev.GetRevealChild())
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
box.Show()
|
||||
box.PackStart(header, false, false, 0)
|
||||
|
@ -64,7 +73,7 @@ func NewContainer(svc cchat.Service, rowctrl server.RowController) *Container {
|
|||
|
||||
primitives.AddClass(box, "service")
|
||||
|
||||
var container = &Container{box, header, chrev, children, rowctrl}
|
||||
var container = &Container{box, header, chrev, children, ctrl}
|
||||
|
||||
// On click, toggle reveal.
|
||||
header.reveal.Connect("clicked", func() {
|
||||
|
@ -75,31 +84,41 @@ func NewContainer(svc cchat.Service, rowctrl server.RowController) *Container {
|
|||
|
||||
// On click, show the auth dialog.
|
||||
header.add.Connect("clicked", func() {
|
||||
auth.NewDialog(svc.Name(), svc.Authenticate(), container.addSession)
|
||||
ctrl.AuthenticateSession(container, svc)
|
||||
})
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
func (c *Container) addSession(ses cchat.Session) {
|
||||
func (c *Container) AddSession(ses cchat.Session) {
|
||||
srow := session.New(ses, c.rowctrl)
|
||||
c.children.addSessionRow(srow)
|
||||
}
|
||||
|
||||
func (c *Container) Sessions() []cchat.Session {
|
||||
var sessions = make([]cchat.Session, len(c.children.Sessions))
|
||||
for i, s := range c.children.Sessions {
|
||||
sessions[i] = s.Session
|
||||
}
|
||||
return sessions
|
||||
}
|
||||
|
||||
type header struct {
|
||||
*gtk.Box
|
||||
reveal *gtk.ToggleButton
|
||||
reveal *rich.ToggleButtonImage // no rich text here but it's left aligned
|
||||
add *gtk.Button
|
||||
}
|
||||
|
||||
func newHeader(svc cchat.Service) *header {
|
||||
reveal, _ := gtk.ToggleButtonNewWithLabel(svc.Name())
|
||||
primitives.BinLeftAlignLabel(reveal) // do this first
|
||||
|
||||
reveal := rich.NewToggleButtonImage(text.Rich{Content: svc.Name()}, "")
|
||||
reveal.Box.SetHAlign(gtk.ALIGN_START)
|
||||
reveal.SetRelief(gtk.RELIEF_NONE)
|
||||
reveal.SetMode(true)
|
||||
reveal.Show()
|
||||
|
||||
// Set a custom icon.
|
||||
primitives.SetImageIcon(&reveal.Image, "folder-remote-symbolic", IconSize)
|
||||
|
||||
add, _ := gtk.ButtonNewFromIconName("list-add-symbolic", gtk.ICON_SIZE_BUTTON)
|
||||
add.SetRelief(gtk.RELIEF_NONE)
|
||||
add.Show()
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const ChildrenMargin = 24
|
||||
|
||||
type RowController interface {
|
||||
MessageRowSelected(*Row, cchat.ServerMessage)
|
||||
}
|
||||
|
||||
type Row struct {
|
||||
*gtk.Box
|
||||
Button *gtk.Button
|
||||
Server cchat.Server
|
||||
|
||||
ctrl RowController
|
||||
|
||||
// enum 1
|
||||
message cchat.ServerMessage
|
||||
|
||||
// enum 2
|
||||
children *Children
|
||||
}
|
||||
|
||||
func New(server cchat.Server, ctrl RowController) *Row {
|
||||
name, err := server.Name()
|
||||
if err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get the server name"))
|
||||
name = "no name"
|
||||
}
|
||||
|
||||
button, _ := gtk.ButtonNewWithLabel(name)
|
||||
primitives.BinLeftAlignLabel(button)
|
||||
|
||||
button.SetRelief(gtk.RELIEF_NONE)
|
||||
button.Show()
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
box.PackStart(button, false, false, 0)
|
||||
box.Show()
|
||||
|
||||
primitives.AddClass(box, "server")
|
||||
|
||||
// TODO: images
|
||||
|
||||
var row = &Row{
|
||||
Box: box,
|
||||
Button: button,
|
||||
Server: server,
|
||||
ctrl: ctrl,
|
||||
}
|
||||
button.Connect("clicked", row.onClick)
|
||||
|
||||
switch server := server.(type) {
|
||||
case cchat.ServerList:
|
||||
row.children = NewChildren(server, ctrl)
|
||||
box.PackStart(row.children, false, false, 0)
|
||||
|
||||
primitives.AddClass(box, "server-list")
|
||||
|
||||
case cchat.ServerMessage:
|
||||
row.message = server
|
||||
|
||||
primitives.AddClass(box, "server-message")
|
||||
}
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
func (row *Row) onClick() {
|
||||
switch {
|
||||
case row.message != nil:
|
||||
row.ctrl.MessageRowSelected(row, row.message)
|
||||
case row.children != nil:
|
||||
row.children.SetRevealChild(!row.children.GetRevealChild())
|
||||
}
|
||||
}
|
||||
|
||||
type Children struct {
|
||||
*gtk.Revealer
|
||||
Main *gtk.Box
|
||||
List cchat.ServerList
|
||||
|
||||
Rows []*Row
|
||||
rowctrl RowController
|
||||
}
|
||||
|
||||
func NewChildren(list cchat.ServerList, ctrl RowController) *Children {
|
||||
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
main.SetMarginStart(ChildrenMargin)
|
||||
main.Show()
|
||||
|
||||
rev, _ := gtk.RevealerNew()
|
||||
rev.SetRevealChild(false)
|
||||
rev.Add(main)
|
||||
rev.Show()
|
||||
|
||||
children := &Children{
|
||||
Revealer: rev,
|
||||
Main: main,
|
||||
List: list,
|
||||
rowctrl: ctrl,
|
||||
}
|
||||
|
||||
if err := list.Servers(children); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get servers"))
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
func (c *Children) SetServers(servers []cchat.Server) {
|
||||
gts.ExecAsync(func() {
|
||||
for _, row := range c.Rows {
|
||||
c.Main.Remove(row)
|
||||
}
|
||||
|
||||
c.Rows = make([]*Row, len(servers))
|
||||
|
||||
for i, server := range servers {
|
||||
row := New(server, c.rowctrl)
|
||||
c.Rows[i] = row
|
||||
c.Main.Add(row)
|
||||
}
|
||||
})
|
||||
}
|
87
internal/ui/service/session/server/server_children.go
Normal file
87
internal/ui/service/session/server/server_children.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const ChildrenMargin = 24
|
||||
|
||||
type Controller interface {
|
||||
MessageRowSelected(*Row, cchat.ServerMessage)
|
||||
}
|
||||
|
||||
// Children is a children server with a reference to the parent.
|
||||
type Children struct {
|
||||
*gtk.Revealer
|
||||
Main *gtk.Box
|
||||
List cchat.ServerList
|
||||
|
||||
rowctrl Controller
|
||||
|
||||
Rows []*Row
|
||||
ParentRow *Row
|
||||
}
|
||||
|
||||
func NewChildren(parent *Row, list cchat.ServerList, ctrl Controller) *Children {
|
||||
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
main.SetMarginStart(ChildrenMargin)
|
||||
main.Show()
|
||||
|
||||
rev, _ := gtk.RevealerNew()
|
||||
rev.SetRevealChild(false)
|
||||
rev.Add(main)
|
||||
rev.Show()
|
||||
|
||||
children := &Children{
|
||||
Revealer: rev,
|
||||
Main: main,
|
||||
List: list,
|
||||
rowctrl: ctrl,
|
||||
ParentRow: parent,
|
||||
}
|
||||
|
||||
if err := list.Servers(children); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get servers"))
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
func (c *Children) SetServers(servers []cchat.Server) {
|
||||
gts.ExecAsync(func() {
|
||||
// Save the current state.
|
||||
var oldID string
|
||||
for _, row := range c.Rows {
|
||||
if row.GetActive() {
|
||||
oldID = row.Server.ID()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Update the server list.
|
||||
for _, row := range c.Rows {
|
||||
c.Main.Remove(row)
|
||||
}
|
||||
|
||||
c.Rows = make([]*Row, len(servers))
|
||||
|
||||
for i, server := range servers {
|
||||
row := NewRow(c, server, c.rowctrl)
|
||||
c.Rows[i] = row
|
||||
c.Main.Add(row)
|
||||
}
|
||||
|
||||
// Update parent reference? Only if it's activated.
|
||||
if oldID != "" {
|
||||
for _, row := range c.Rows {
|
||||
if row.Server.ID() == oldID {
|
||||
row.Button.SetActive(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
100
internal/ui/service/session/server/server_row.go
Normal file
100
internal/ui/service/session/server/server_row.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Row struct {
|
||||
*gtk.Box
|
||||
Button *rich.ToggleButtonImage
|
||||
Server cchat.Server
|
||||
Parent *Children
|
||||
|
||||
ctrl Controller
|
||||
|
||||
// enum 1
|
||||
message cchat.ServerMessage
|
||||
|
||||
// enum 2
|
||||
children *Children
|
||||
}
|
||||
|
||||
func NewRow(parent *Children, server cchat.Server, ctrl Controller) *Row {
|
||||
button := rich.NewToggleButtonImage(text.Rich{}, "")
|
||||
button.Box.SetHAlign(gtk.ALIGN_START)
|
||||
button.SetRelief(gtk.RELIEF_NONE)
|
||||
button.Show()
|
||||
|
||||
if err := server.Name(button); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get the server name"))
|
||||
button.SetLabel(text.Rich{Content: "Unknown"})
|
||||
}
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
box.PackStart(button, false, false, 0)
|
||||
box.Show()
|
||||
|
||||
primitives.AddClass(box, "server")
|
||||
|
||||
// TODO: images
|
||||
|
||||
var row = &Row{
|
||||
Box: box,
|
||||
Button: button,
|
||||
Server: server,
|
||||
Parent: parent,
|
||||
ctrl: ctrl,
|
||||
}
|
||||
|
||||
switch server := server.(type) {
|
||||
case cchat.ServerList:
|
||||
row.children = NewChildren(row, server, ctrl)
|
||||
box.PackStart(row.children, false, false, 0)
|
||||
|
||||
primitives.AddClass(box, "server-list")
|
||||
|
||||
case cchat.ServerMessage:
|
||||
row.message = server
|
||||
|
||||
primitives.AddClass(box, "server-message")
|
||||
}
|
||||
|
||||
button.Connect("clicked", row.onClick)
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
func (row *Row) GetActive() bool {
|
||||
return row.Button.GetActive()
|
||||
}
|
||||
|
||||
func (row *Row) onClick() {
|
||||
switch {
|
||||
|
||||
// If the server is a message server. We're only selected if the button is
|
||||
// pressed.
|
||||
case row.message != nil && row.GetActive():
|
||||
row.ctrl.MessageRowSelected(row, row.message)
|
||||
|
||||
// If the server is a list of smaller servers.
|
||||
case row.children != nil:
|
||||
row.children.SetRevealChild(!row.children.GetRevealChild())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Row) Breadcrumb() string {
|
||||
var label = r.Button.GetLabel().Content
|
||||
|
||||
// Does the row have a parent?
|
||||
if r.Parent != nil {
|
||||
return r.Parent.ParentRow.Breadcrumb() + "/" + label
|
||||
}
|
||||
|
||||
return label
|
||||
}
|
|
@ -4,53 +4,66 @@ import (
|
|||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const IconSize = 32
|
||||
|
||||
// Controller extends server.RowController to add session.
|
||||
type Controller interface {
|
||||
MessageRowSelected(*Row, *server.Row, cchat.ServerMessage)
|
||||
}
|
||||
|
||||
type Row struct {
|
||||
*gtk.Box
|
||||
Button *gtk.ToggleButton
|
||||
Button *rich.ToggleButtonImage
|
||||
Session cchat.Session
|
||||
|
||||
Servers *server.Children
|
||||
|
||||
ctrl Controller
|
||||
}
|
||||
|
||||
func New(ses cchat.Session, rowctrl server.RowController) *Row {
|
||||
n, err := ses.Name()
|
||||
if err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get the username"))
|
||||
n = "no name"
|
||||
func New(ses cchat.Session, ctrl Controller) *Row {
|
||||
row := &Row{
|
||||
Session: ses,
|
||||
ctrl: ctrl,
|
||||
}
|
||||
row.Servers = server.NewChildren(ses, row)
|
||||
|
||||
button, _ := gtk.ToggleButtonNewWithLabel(n)
|
||||
primitives.BinLeftAlignLabel(button)
|
||||
|
||||
button.SetRelief(gtk.RELIEF_NONE)
|
||||
button.Show()
|
||||
|
||||
servers := server.NewChildren(ses, rowctrl)
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
box.Show()
|
||||
box.SetMarginStart(server.ChildrenMargin)
|
||||
box.PackStart(button, false, false, 0)
|
||||
box.PackStart(servers, false, false, 0)
|
||||
|
||||
primitives.AddClass(box, "session")
|
||||
|
||||
row.Button = rich.NewToggleButtonImage(text.Rich{}, "")
|
||||
row.Button.Box.SetHAlign(gtk.ALIGN_START)
|
||||
row.Button.SetRelief(gtk.RELIEF_NONE)
|
||||
row.Button.Show()
|
||||
// On click, toggle reveal.
|
||||
button.Connect("clicked", func() {
|
||||
revealed := !servers.GetRevealChild()
|
||||
servers.SetRevealChild(revealed)
|
||||
button.SetActive(revealed)
|
||||
row.Button.Connect("clicked", func() {
|
||||
revealed := !row.Servers.GetRevealChild()
|
||||
row.Servers.SetRevealChild(revealed)
|
||||
row.Button.SetActive(revealed)
|
||||
})
|
||||
|
||||
return &Row{
|
||||
Box: box,
|
||||
Button: button,
|
||||
Session: ses,
|
||||
Servers: servers,
|
||||
primitives.SetImageIcon(&row.Button.Image, "user-available-symbolic", IconSize)
|
||||
|
||||
if err := ses.Name(row.Button); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get the username"))
|
||||
row.Button.SetLabel(text.Rich{Content: "Unknown"})
|
||||
}
|
||||
|
||||
row.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
row.Box.SetMarginStart(server.ChildrenMargin)
|
||||
row.Box.PackStart(row.Button, false, false, 0)
|
||||
row.Box.PackStart(row.Servers, false, false, 0)
|
||||
row.Box.Show()
|
||||
|
||||
primitives.AddClass(row.Box, "session")
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
func (r *Row) MessageRowSelected(server *server.Row, smsg cchat.ServerMessage) {
|
||||
r.ctrl.MessageRowSelected(r, server, smsg)
|
||||
}
|
||||
|
|
|
@ -3,25 +3,33 @@ package ui
|
|||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/keyring"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/auth"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
const LeftWidth = 220
|
||||
|
||||
type Application struct {
|
||||
type App struct {
|
||||
window *window
|
||||
header *header
|
||||
|
||||
// used to keep track of what row to highlight and unhighlight
|
||||
lastRowHighlighter func(bool)
|
||||
}
|
||||
|
||||
var (
|
||||
_ gts.Windower = (*Application)(nil)
|
||||
_ gts.Headerer = (*Application)(nil)
|
||||
_ server.RowController = (*Application)(nil)
|
||||
_ gts.Windower = (*App)(nil)
|
||||
_ gts.Headerer = (*App)(nil)
|
||||
_ service.Controller = (*App)(nil)
|
||||
)
|
||||
|
||||
func NewApplication() *Application {
|
||||
app := &Application{
|
||||
func NewApplication() *App {
|
||||
app := &App{
|
||||
window: newWindow(),
|
||||
header: newHeader(),
|
||||
}
|
||||
|
@ -29,18 +37,44 @@ func NewApplication() *Application {
|
|||
return app
|
||||
}
|
||||
|
||||
func (app *Application) AddService(svc cchat.Service) {
|
||||
app.window.Services.AddService(svc, app)
|
||||
func (app *App) AddService(svc cchat.Service) {
|
||||
var container = app.window.Services.AddService(svc, app)
|
||||
|
||||
// Attempt to restore sessions asynchronously.
|
||||
keyring.RestoreSessions(svc, container.AddSession)
|
||||
}
|
||||
|
||||
func (app *Application) MessageRowSelected(_ *server.Row, smsg cchat.ServerMessage) {
|
||||
app.window.MessageView.JoinServer(smsg)
|
||||
func (app *App) MessageRowSelected(ses *session.Row, srv *server.Row, smsg cchat.ServerMessage) {
|
||||
// Is there an old row that we should unhighlight?
|
||||
if app.lastRowHighlighter != nil {
|
||||
app.lastRowHighlighter(false)
|
||||
}
|
||||
|
||||
// Set the new row and highlight it.
|
||||
app.lastRowHighlighter = srv.Button.SetActive
|
||||
app.lastRowHighlighter(true)
|
||||
|
||||
log.Println("Breadcrumb:")
|
||||
|
||||
// Show the messages.
|
||||
app.window.MessageView.JoinServer(ses.Session, smsg)
|
||||
}
|
||||
|
||||
func (app *Application) Header() gtk.IWidget {
|
||||
func (app *App) AuthenticateSession(container *service.Container, svc cchat.Service) {
|
||||
auth.NewDialog(svc.Name(), svc.Authenticate(), func(ses cchat.Session) {
|
||||
container.AddSession(ses)
|
||||
|
||||
// Save all sessions.
|
||||
for _, err := range keyring.SaveSessions(svc, container.Sessions()) {
|
||||
log.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (app *App) Header() gtk.IWidget {
|
||||
return app.header
|
||||
}
|
||||
|
||||
func (app *Application) Window() gtk.IWidget {
|
||||
func (app *App) Window() gtk.IWidget {
|
||||
return app.window
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue