mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-01-24 19:06:43 +00:00
Refactored code for additional lazy loading and features
This commit is contained in:
parent
0d8d3609be
commit
be88670bb6
8
go.mod
8
go.mod
|
@ -3,16 +3,20 @@ module github.com/diamondburned/cchat-gtk
|
|||
go 1.14
|
||||
|
||||
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200612012846-9df87fea4f6d
|
||||
replace github.com/diamondburned/cchat-mock => ../cchat-mock/
|
||||
replace github.com/diamondburned/cchat-discord => ../cchat-discord/
|
||||
|
||||
require (
|
||||
github.com/Xuanwo/go-locale v0.2.0
|
||||
github.com/diamondburned/cchat v0.0.26
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200613233949-1e7651c8dd84
|
||||
github.com/diamondburned/cchat v0.0.28
|
||||
github.com/diamondburned/cchat-discord v0.0.0-00010101000000-000000000000
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d
|
||||
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64
|
||||
github.com/goodsign/monday v1.0.0
|
||||
github.com/google/btree v1.0.0 // indirect
|
||||
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
|
||||
github.com/ianlancetaylor/cgosymbolizer v0.0.0-20200424224625-be1b05b0b279
|
||||
github.com/markbates/pkger v0.17.0
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
|
|
23
go.sum
23
go.sum
|
@ -7,18 +7,27 @@ github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3E
|
|||
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/arikawa v0.8.7-0.20200522214036-530bff74a2c6/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
|
||||
github.com/diamondburned/arikawa v0.9.4 h1:Mrp0Vz9R2afbvhWS6m/oLIQy22/uxXb459LUv7qrZPA=
|
||||
github.com/diamondburned/arikawa v0.9.4/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
|
||||
github.com/diamondburned/cchat v0.0.25 h1:+kf2gQu5TQs1vD/gCaVlzKu5vOqZz/1Qw87xHdeFYj4=
|
||||
github.com/diamondburned/cchat v0.0.25/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.26 h1:QBt4d65uzUPJz3jF8b2pJ09Jz8LeBRyG2ol47FOy0g0=
|
||||
github.com/diamondburned/cchat v0.0.26/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.28 h1:+1VnltW0rl8/NZTUP+x89jVhi3YTTR+e6iLprZ7HcwM=
|
||||
github.com/diamondburned/cchat v0.0.28/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200613003444-b36f8f47debe h1:OoTLxpryxB9iQyu3bjw5N9N/3Bvu6FwklJ85X9erCAY=
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200613003444-b36f8f47debe/go.mod h1:vitBma+rd/ah+ujQsp6lPm/AfS2KtLKEh+Owxbv5BQM=
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200613233949-1e7651c8dd84 h1:NSuksZ9HiLiau93qAz4yNba6Xd7ExOFc956dumONDQ0=
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200613233949-1e7651c8dd84/go.mod h1:JxTay4MVEqmDisGqDGk8TG0UnKX7wDEImFywyoPfGjk=
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d h1:LkzARyvdGRvAsaKEPTV3XcqMHENH6J+KRAI+3sq41Qs=
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d/go.mod h1:SVTt5je4G+re8aSVJAFk/x8vvbRzXdpKgSKmVGoM1tg=
|
||||
github.com/diamondburned/gotk3 v0.0.0-20200612012846-9df87fea4f6d h1:NFTuwBU+CNZDB1iaGC3gDuBRf9FTd1h2WnIh6NF7elg=
|
||||
github.com/diamondburned/gotk3 v0.0.0-20200612012846-9df87fea4f6d/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64 h1:/ykUYHuYyj+NN/aaqe6lfaCZQc3EMZs93wAGVJTh5j0=
|
||||
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
|
||||
github.com/diamondburned/ningen v0.0.0-20200610212436-159f7105a2be h1:mUw8X/YzJGFSdL8y3Q/XqyzqPyIMNVSDyZGOP3JXgJA=
|
||||
github.com/diamondburned/ningen v0.0.0-20200610212436-159f7105a2be/go.mod h1:B2hq2B4va1MlnMmXuv9vXmyu9gscxJLmwrmcSB1Les8=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
|
@ -33,8 +42,14 @@ github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
|
|||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
|
||||
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/ianlancetaylor/cgosymbolizer v0.0.0-20200424224625-be1b05b0b279 h1:IpTHAzWv1pKDDWeJDY5VOHvqc2T9d3C8cPKEf2VPqHE=
|
||||
github.com/ianlancetaylor/cgosymbolizer v0.0.0-20200424224625-be1b05b0b279/go.mod h1:a5aratAVTWyz+nJMmDsN8O4XTfaLfdAsB1ysCmZX5Bw=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
|
@ -58,16 +73,24 @@ 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/twmb/murmur3 v1.1.3 h1:D83U0XYKcHRYwYIpBKf3Pks91Z0Byda/9SJ8B6EMRcA=
|
||||
github.com/twmb/murmur3 v1.1.3/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
|
||||
github.com/yuin/goldmark v1.1.30/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-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
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/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/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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=
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package gts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
|
@ -54,6 +56,7 @@ func Main(wfn func() WindowHeaderer) {
|
|||
App.Window, _ = gtk.ApplicationWindowNew(App.Application)
|
||||
App.Window.SetDefaultSize(1000, 500)
|
||||
App.Window.SetTitlebar(App.Header)
|
||||
App.Window.Connect("destroy", App.Application.Quit)
|
||||
App.Window.Show()
|
||||
|
||||
// Execute the function later, because we need it to run after
|
||||
|
@ -65,7 +68,9 @@ func Main(wfn func() WindowHeaderer) {
|
|||
|
||||
// Use a special function to run the application. Exit with the appropriate
|
||||
// exit code.
|
||||
os.Exit(App.Run(Args))
|
||||
if code := App.Run(Args); code > 0 {
|
||||
os.Exit(code)
|
||||
}
|
||||
}
|
||||
|
||||
// Async runs fn asynchronously, then runs the function it returns in the Gtk
|
||||
|
@ -79,7 +84,7 @@ func Async(fn func() (func(), error)) {
|
|||
|
||||
// Attempt to run the callback if it's there.
|
||||
if f != nil {
|
||||
glib.IdleAdd(f)
|
||||
ExecAsync(f)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -106,3 +111,87 @@ func EventIsRightClick(ev *gdk.Event) bool {
|
|||
keyev := gdk.EventButtonNewFromEvent(ev)
|
||||
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
|
||||
}
|
||||
|
||||
// Reuser is an interface for structs that inherit Reusable.
|
||||
type Reuser interface {
|
||||
Context() context.Context
|
||||
Acquire() int64
|
||||
Validate(int64) bool
|
||||
}
|
||||
|
||||
// AsyncUse is a handler for structs that implement the Reuser primitive. The
|
||||
// passed in function will be called asynchronously, but swap will be called in
|
||||
// the Gtk main thread.
|
||||
func AsyncUse(r Reuser, swap func(interface{}), fn func(context.Context) (interface{}, error)) {
|
||||
// Acquire an ID.
|
||||
id := r.Acquire()
|
||||
ctx := r.Context()
|
||||
|
||||
Async(func() (func(), error) {
|
||||
// Run the callback asynchronously.
|
||||
v, err := fn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func() {
|
||||
// Validate the ID. Cancel if it's invalid.
|
||||
if !r.Validate(id) {
|
||||
log.Println("Async function value dropped for reusable primitive.")
|
||||
return
|
||||
}
|
||||
|
||||
// Update the resource.
|
||||
swap(v)
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// Reusable is the synchronization primitive to provide a method for
|
||||
// asynchronous cancellation and reusability.
|
||||
//
|
||||
// It works by copying the ID (time) for each asynchronous operation. The
|
||||
// operation then completes, and the ID is then compared again before being
|
||||
// used. It provides a cancellation abstraction around the Gtk main thread.
|
||||
//
|
||||
// This struct is not thread-safe, as it relies on the Gtk main thread
|
||||
// synchronization.
|
||||
type Reusable struct {
|
||||
time int64 // creation time, used as ID
|
||||
ctx context.Context
|
||||
cancel func()
|
||||
}
|
||||
|
||||
func NewReusable() *Reusable {
|
||||
r := &Reusable{}
|
||||
r.Invalidate()
|
||||
return r
|
||||
}
|
||||
|
||||
// Invalidate generates a new ID for the primitive, which would render
|
||||
// asynchronously updating elements invalid.
|
||||
func (r *Reusable) Invalidate() {
|
||||
// Cancel the old context.
|
||||
if r.cancel != nil {
|
||||
r.cancel()
|
||||
}
|
||||
|
||||
// Reset.
|
||||
r.time = time.Now().UnixNano()
|
||||
r.ctx, r.cancel = context.WithCancel(context.Background())
|
||||
}
|
||||
|
||||
// Context returns the reusable's cancellable context. It never returns nil.
|
||||
func (r *Reusable) Context() context.Context {
|
||||
return r.ctx
|
||||
}
|
||||
|
||||
// Reusable checks the acquired ID against the current one.
|
||||
func (r *Reusable) Validate(acquired int64) (valid bool) {
|
||||
return r.time == acquired
|
||||
}
|
||||
|
||||
// Acquire lends the ID to be given to Reusable() after finishing.
|
||||
func (r *Reusable) Acquire() int64 {
|
||||
return r.time
|
||||
}
|
||||
|
|
|
@ -37,9 +37,17 @@ func AsyncImage(img ImageContainer, url string, procs ...imgutil.Processor) {
|
|||
ctx, cancel := context.WithCancel(context.Background())
|
||||
connectDestroyer(img, cancel)
|
||||
|
||||
go syncImageFn(ctx, img, url, procs, func(l *gdk.PixbufLoader, gif bool) {
|
||||
l.Connect("area-prepared", areaPreparedFn(ctx, img, gif))
|
||||
})
|
||||
gif := strings.Contains(url, ".gif")
|
||||
|
||||
l, err := gdk.PixbufLoaderNew()
|
||||
if err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to make pixbuf loader"))
|
||||
return
|
||||
}
|
||||
|
||||
l.Connect("area-prepared", areaPreparedFn(ctx, img, gif))
|
||||
|
||||
go syncImage(ctx, l, url, procs, gif)
|
||||
}
|
||||
|
||||
// AsyncImageSized resizes using GdkPixbuf. This method does not use the cache.
|
||||
|
@ -54,17 +62,25 @@ func AsyncImageSized(img ImageContainerSizer, url string, w, h int, procs ...img
|
|||
ctx, cancel := context.WithCancel(context.Background())
|
||||
connectDestroyer(img, cancel)
|
||||
|
||||
go syncImageFn(ctx, img, url, procs, func(l *gdk.PixbufLoader, gif bool) {
|
||||
l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) {
|
||||
w, h = imgutil.MaxSize(imgW, imgH, w, h)
|
||||
if w != imgW || h != imgH {
|
||||
l.SetSize(w, h)
|
||||
execIfCtx(ctx, func() { img.SetSizeRequest(w, h) })
|
||||
}
|
||||
})
|
||||
gif := strings.Contains(url, ".gif")
|
||||
|
||||
l.Connect("area-prepared", areaPreparedFn(ctx, img, gif))
|
||||
l, err := gdk.PixbufLoaderNew()
|
||||
if err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to make pixbuf loader"))
|
||||
return
|
||||
}
|
||||
|
||||
l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) {
|
||||
w, h = imgutil.MaxSize(imgW, imgH, w, h)
|
||||
if w != imgW || h != imgH {
|
||||
l.SetSize(w, h)
|
||||
execIfCtx(ctx, func() { img.SetSizeRequest(w, h) })
|
||||
}
|
||||
})
|
||||
|
||||
l.Connect("area-prepared", areaPreparedFn(ctx, img, gif))
|
||||
|
||||
go syncImage(ctx, l, url, procs, gif)
|
||||
}
|
||||
|
||||
type pbgetter interface {
|
||||
|
@ -110,13 +126,9 @@ func execIfCtx(ctx context.Context, fn func()) {
|
|||
})
|
||||
}
|
||||
|
||||
func syncImageFn(
|
||||
ctx context.Context,
|
||||
img ImageContainer,
|
||||
url string,
|
||||
procs []imgutil.Processor,
|
||||
middle func(l *gdk.PixbufLoader, gif bool),
|
||||
) {
|
||||
func syncImage(ctx context.Context, l io.WriteCloser, url string, p []imgutil.Processor, gif bool) {
|
||||
// Close at the end when done.
|
||||
defer l.Close()
|
||||
|
||||
r, err := get(ctx, url, true)
|
||||
if err != nil {
|
||||
|
@ -125,27 +137,12 @@ func syncImageFn(
|
|||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
l, err := gdk.PixbufLoaderNew()
|
||||
if err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to make pixbuf loader"))
|
||||
return
|
||||
}
|
||||
|
||||
gif := strings.Contains(url, ".gif")
|
||||
|
||||
// This is a very important signal, so we must do it synchronously. Gotk3's
|
||||
// callback implementation requires all connects to be synchronous to a
|
||||
// certain thread.
|
||||
<-gts.ExecSync(func() {
|
||||
middle(l, gif)
|
||||
})
|
||||
|
||||
// If we have processors, then write directly in there.
|
||||
if len(procs) > 0 {
|
||||
if len(p) > 0 {
|
||||
if !gif {
|
||||
err = imgutil.ProcessStream(l, r.Body, procs)
|
||||
err = imgutil.ProcessStream(l, r.Body, p)
|
||||
} else {
|
||||
err = imgutil.ProcessAnimationStream(l, r.Body, procs)
|
||||
err = imgutil.ProcessAnimationStream(l, r.Body, p)
|
||||
}
|
||||
} else {
|
||||
// Else, directly copy the body over.
|
||||
|
@ -156,8 +153,4 @@ func syncImageFn(
|
|||
log.Error(errors.Wrap(err, "Error processing image"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := l.Close(); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to close pixbuf"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,9 +13,7 @@ const AvatarSize = 20
|
|||
|
||||
type usernameContainer struct {
|
||||
*gtk.Revealer
|
||||
|
||||
main *gtk.Box
|
||||
|
||||
main *gtk.Box
|
||||
avatar *rich.Icon
|
||||
label *rich.Label
|
||||
}
|
||||
|
@ -76,7 +74,7 @@ func (u *usernameContainer) Update(session cchat.Session, sender cchat.ServerMes
|
|||
u.label.AsyncSetLabel(nicknamer.Nickname, "Error fetching server nickname")
|
||||
}
|
||||
|
||||
// Does session implement an icon? Update if so.
|
||||
// Does session implement an icon? Update if yes.
|
||||
if iconer, ok := session.(cchat.Icon); ok {
|
||||
u.avatar.AsyncSetIcon(iconer.Icon, "Error fetching session icon URL")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package messages
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/icons"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
|
@ -80,8 +82,8 @@ func NewView() *View {
|
|||
view := &View{}
|
||||
|
||||
// TODO: change
|
||||
// view.Container = compact.NewContainer()
|
||||
view.InputView = input.NewView(view)
|
||||
// view.Container = compact.NewContainer(view)
|
||||
view.Container = cozy.NewContainer(view)
|
||||
|
||||
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
|
@ -98,14 +100,14 @@ func NewView() *View {
|
|||
}
|
||||
|
||||
func (v *View) Reset() {
|
||||
v.state.Reset() // Reset the state variables.
|
||||
v.FaceView.Reset() // Switch back to the main screen.
|
||||
v.Container.Reset() // Clean all messages.
|
||||
v.InputView.Reset() // Reset the input.
|
||||
v.state.Reset() // Reset the state variables.
|
||||
}
|
||||
|
||||
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
|
||||
func (v *View) JoinServer(session cchat.Session, server ServerMessage) {
|
||||
func (v *View) JoinServer(session cchat.Session, server ServerMessage, done func()) {
|
||||
// Reset before setting.
|
||||
v.Reset()
|
||||
|
||||
|
@ -116,21 +118,28 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage) {
|
|||
v.state.bind(session, server)
|
||||
|
||||
gts.Async(func() (func(), error) {
|
||||
s, err := server.JoinServer(v.Container)
|
||||
// We can use a background context here, as the user can't go anywhere
|
||||
// that would require cancellation anyway. This is done in ui.go.
|
||||
s, err := server.JoinServer(context.Background(), v.Container)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "Failed to join server")
|
||||
return func() { v.SetError(err) }, err
|
||||
// Even if we're erroring out, we're running the done() callback
|
||||
// anyway.
|
||||
return func() { done(); v.SetError(err) }, err
|
||||
}
|
||||
|
||||
return func() {
|
||||
// Run the done() callback.
|
||||
done()
|
||||
|
||||
// Set the screen to the main one.
|
||||
v.FaceView.SetMain()
|
||||
|
||||
// Set the cancel handler.
|
||||
v.state.setcurrent(s)
|
||||
|
||||
// Skipping ok check because sender can be nil. Without the empty check, Go
|
||||
// will panic.
|
||||
// Skipping ok check because sender can be nil. Without the empty
|
||||
// check, Go will panic.
|
||||
sender, _ := server.(cchat.ServerMessageSender)
|
||||
v.InputView.SetSender(session, sender)
|
||||
}, nil
|
||||
|
@ -196,7 +205,7 @@ func (v *View) menuItemActivate(msgID string) func(m *gtk.MenuItem) {
|
|||
go func(action string) {
|
||||
// Run, get the error, and try to log it. The logger will ignore nil
|
||||
// errors.
|
||||
err := v.state.actioner.DoMessageAction(v.Container, action, msgID)
|
||||
err := v.state.actioner.DoMessageAction(action, msgID)
|
||||
log.Error(errors.Wrap(err, "Failed to do action "+action))
|
||||
}(m.GetLabel())
|
||||
}
|
||||
|
|
|
@ -141,7 +141,7 @@ type Connector interface {
|
|||
}
|
||||
|
||||
func BindMenu(connector Connector, menu *gtk.Menu) {
|
||||
connector.Connect("event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
|
||||
connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
|
||||
if gts.EventIsRightClick(ev) {
|
||||
menu.PopupAtPointer(ev)
|
||||
}
|
||||
|
@ -149,11 +149,16 @@ func BindMenu(connector Connector, menu *gtk.Menu) {
|
|||
}
|
||||
|
||||
func BindDynamicMenu(connector Connector, constr func(menu *gtk.Menu)) {
|
||||
connector.Connect("event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
|
||||
connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
|
||||
if gts.EventIsRightClick(ev) {
|
||||
menu, _ := gtk.MenuNew()
|
||||
constr(menu)
|
||||
menu.PopupAtPointer(ev)
|
||||
|
||||
// Only show the menu if the callback added any children into the
|
||||
// list.
|
||||
if menu.GetChildren().Length() > 0 {
|
||||
menu.PopupAtPointer(ev)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
package rich
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/imgutil"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Icon struct {
|
||||
|
@ -17,6 +17,9 @@ type Icon struct {
|
|||
Image *gtk.Image
|
||||
resizer imgutil.Processor
|
||||
procs []imgutil.Processor
|
||||
size int
|
||||
|
||||
r gts.Reusable
|
||||
|
||||
// state
|
||||
url string
|
||||
|
@ -41,12 +44,16 @@ func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
|
|||
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
|
||||
rev.SetTransitionDuration(50)
|
||||
|
||||
return &Icon{
|
||||
i := &Icon{
|
||||
Revealer: rev,
|
||||
Image: img,
|
||||
resizer: imgutil.Resize(sizepx, sizepx),
|
||||
procs: procs,
|
||||
|
||||
r: *gts.NewReusable(),
|
||||
}
|
||||
i.SetSize(sizepx)
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// Reset wipes the state to be just after construction.
|
||||
|
@ -61,6 +68,10 @@ func (i *Icon) URL() string {
|
|||
return i.url
|
||||
}
|
||||
|
||||
func (i *Icon) Size() int {
|
||||
return i.size
|
||||
}
|
||||
|
||||
func (i *Icon) CopyPixbuf(dst httputil.ImageContainer) {
|
||||
switch i.Image.GetStorageType() {
|
||||
case gtk.IMAGE_PIXBUF:
|
||||
|
@ -84,6 +95,7 @@ func (i *Icon) SetPlaceholderIcon(iconName string, iconSzPx int) {
|
|||
|
||||
// SetSize is not thread-safe.
|
||||
func (i *Icon) SetSize(szpx int) {
|
||||
i.size = szpx
|
||||
i.Image.SetSizeRequest(szpx, szpx)
|
||||
i.resizer = imgutil.Resize(szpx, szpx)
|
||||
}
|
||||
|
@ -95,17 +107,25 @@ func (i *Icon) AddProcessors(procs ...imgutil.Processor) {
|
|||
|
||||
// SetIcon is thread-safe.
|
||||
func (i *Icon) SetIcon(url string) {
|
||||
gts.ExecAsync(func() {
|
||||
i.SetIconUnsafe(url)
|
||||
gts.ExecAsync(func() { i.SetIconUnsafe(url) })
|
||||
}
|
||||
|
||||
func (i *Icon) swapResource(v interface{}) {
|
||||
i.SetIconUnsafe(v.(*nullIcon).url)
|
||||
}
|
||||
|
||||
func (i *Icon) AsyncSetIcon(fn func(context.Context, cchat.IconContainer) error, wrap string) {
|
||||
gts.AsyncUse(&i.r, i.swapResource, func(ctx context.Context) (interface{}, error) {
|
||||
var ni = &nullIcon{}
|
||||
return ni, fn(ctx, ni)
|
||||
})
|
||||
}
|
||||
|
||||
func (i *Icon) AsyncSetIcon(fn func(cchat.IconContainer) error, wrap string) {
|
||||
go func() {
|
||||
if err := fn(i); err != nil {
|
||||
log.Error(errors.Wrap(err, wrap))
|
||||
}
|
||||
}()
|
||||
func (i *Icon) AsyncSetIconer(iconer cchat.Icon, wrap string) {
|
||||
gts.AsyncUse(&i.r, i.swapResource, func(ctx context.Context) (interface{}, error) {
|
||||
var ni = &nullIcon{}
|
||||
return ni, iconer.Icon(ctx, ni)
|
||||
})
|
||||
}
|
||||
|
||||
// SetIconUnsafe is not thread-safe.
|
||||
|
@ -160,3 +180,8 @@ func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
|
|||
Box: box,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ToggleButtonImage) Reset() {
|
||||
t.Labeler.Reset()
|
||||
t.Image.Reset()
|
||||
}
|
||||
|
|
114
internal/ui/rich/label.go
Normal file
114
internal/ui/rich/label.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package rich
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
)
|
||||
|
||||
type Labeler interface {
|
||||
// thread-safe
|
||||
cchat.LabelContainer // thread-safe
|
||||
|
||||
// not thread-safe
|
||||
SetLabelUnsafe(text.Rich)
|
||||
GetLabel() text.Rich
|
||||
GetText() string
|
||||
Reset()
|
||||
}
|
||||
|
||||
type Label struct {
|
||||
gtk.Label
|
||||
current text.Rich
|
||||
|
||||
// Reusable primitive.
|
||||
r gts.Reusable
|
||||
}
|
||||
|
||||
var (
|
||||
_ gtk.IWidget = (*Label)(nil)
|
||||
_ Labeler = (*Label)(nil)
|
||||
)
|
||||
|
||||
func NewLabel(content text.Rich) *Label {
|
||||
label, _ := gtk.LabelNew("")
|
||||
label.SetMarkup(parser.RenderMarkup(content))
|
||||
label.SetXAlign(0) // left align
|
||||
label.SetEllipsize(pango.ELLIPSIZE_END)
|
||||
|
||||
return &Label{
|
||||
Label: *label,
|
||||
current: content,
|
||||
// reusable primitive, take reference
|
||||
r: *gts.NewReusable(),
|
||||
}
|
||||
}
|
||||
|
||||
// Reset wipes the state to be just after construction.
|
||||
func (l *Label) Reset() {
|
||||
l.current = text.Rich{}
|
||||
l.r.Invalidate()
|
||||
l.Label.SetText("")
|
||||
}
|
||||
|
||||
// swapResource is reserved for internal use only.
|
||||
func (l *Label) swapResource(v interface{}) {
|
||||
l.SetLabelUnsafe(v.(*nullLabel).Rich)
|
||||
}
|
||||
|
||||
func (l *Label) AsyncSetLabel(fn func(context.Context, cchat.LabelContainer) error, info string) {
|
||||
gts.AsyncUse(&l.r, l.swapResource, func(ctx context.Context) (interface{}, error) {
|
||||
var nl = &nullLabel{}
|
||||
return nl, fn(ctx, nl)
|
||||
})
|
||||
}
|
||||
|
||||
// SetLabel is thread-safe.
|
||||
func (l *Label) SetLabel(content text.Rich) {
|
||||
gts.ExecAsync(func() { l.SetLabelUnsafe(content) })
|
||||
}
|
||||
|
||||
// SetLabelUnsafe sets the label in the current thread, meaning it's not
|
||||
// thread-safe.
|
||||
func (l *Label) SetLabelUnsafe(content text.Rich) {
|
||||
l.current = content
|
||||
l.SetMarkup(parser.RenderMarkup(content))
|
||||
}
|
||||
|
||||
// GetLabel is NOT thread-safe.
|
||||
func (l *Label) GetLabel() text.Rich {
|
||||
return l.current
|
||||
}
|
||||
|
||||
// GetText is NOT thread-safe.
|
||||
func (l *Label) GetText() string {
|
||||
return l.current.Content
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
|
@ -3,15 +3,7 @@ package rich
|
|||
import (
|
||||
"html"
|
||||
|
||||
"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/diamondburned/cchat-gtk/internal/ui/rich/parser"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func Small(text string) string {
|
||||
|
@ -22,90 +14,12 @@ func MakeRed(content text.Rich) string {
|
|||
return `<span color="red">` + html.EscapeString(content.Content) + `</span>`
|
||||
}
|
||||
|
||||
type Labeler interface {
|
||||
// thread-safe
|
||||
cchat.LabelContainer // thread-safe
|
||||
// used for grabbing text without changing state
|
||||
type nullLabel struct{ text.Rich }
|
||||
|
||||
// not thread-safe
|
||||
SetLabelUnsafe(text.Rich)
|
||||
GetLabel() text.Rich
|
||||
GetText() string
|
||||
}
|
||||
func (n *nullLabel) SetLabel(t text.Rich) { n.Rich = t }
|
||||
|
||||
type Label struct {
|
||||
gtk.Label
|
||||
current text.Rich
|
||||
}
|
||||
// used for grabbing url without changing state
|
||||
type nullIcon struct{ url string }
|
||||
|
||||
var (
|
||||
_ gtk.IWidget = (*Label)(nil)
|
||||
_ Labeler = (*Label)(nil)
|
||||
)
|
||||
|
||||
func NewLabel(content text.Rich) *Label {
|
||||
label, _ := gtk.LabelNew("")
|
||||
label.SetMarkup(parser.RenderMarkup(content))
|
||||
label.SetXAlign(0) // left align
|
||||
label.SetEllipsize(pango.ELLIPSIZE_END)
|
||||
return &Label{*label, content}
|
||||
}
|
||||
|
||||
// Reset wipes the state to be just after construction.
|
||||
func (l *Label) Reset() {
|
||||
l.current = text.Rich{}
|
||||
l.Label.SetText("")
|
||||
}
|
||||
|
||||
func (l *Label) AsyncSetLabel(fn func(cchat.LabelContainer) error, info string) {
|
||||
go func() {
|
||||
if err := fn(l); err != nil {
|
||||
log.Error(errors.Wrap(err, info))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SetLabel is thread-safe.
|
||||
func (l *Label) SetLabel(content text.Rich) {
|
||||
gts.ExecAsync(func() {
|
||||
l.SetLabelUnsafe(content)
|
||||
})
|
||||
}
|
||||
|
||||
// SetLabelUnsafe sets the label in the current thread, meaning it's not
|
||||
// thread-safe.
|
||||
func (l *Label) SetLabelUnsafe(content text.Rich) {
|
||||
l.current = content
|
||||
l.SetMarkup(parser.RenderMarkup(content))
|
||||
}
|
||||
|
||||
// GetLabel is NOT thread-safe.
|
||||
func (l *Label) GetLabel() text.Rich {
|
||||
return l.current
|
||||
}
|
||||
|
||||
// GetText is NOT thread-safe.
|
||||
func (l *Label) GetText() string {
|
||||
return l.current.Content
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
func (i *nullIcon) SetIcon(url string) { i.url = url }
|
||||
|
|
113
internal/ui/service/button/button.go
Normal file
113
internal/ui/service/button/button.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package button
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
type ToggleButtonImage struct {
|
||||
rich.ToggleButtonImage
|
||||
|
||||
extraMenu []menu.Item
|
||||
menu *menu.LazyMenu
|
||||
|
||||
clicked func(bool)
|
||||
|
||||
err error
|
||||
icon bool // whether or not the button has an icon
|
||||
iconSz int
|
||||
}
|
||||
|
||||
var _ cchat.IconContainer = (*ToggleButtonImage)(nil)
|
||||
|
||||
func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
|
||||
b := rich.NewToggleButtonImage(content)
|
||||
b.Show()
|
||||
|
||||
tb := &ToggleButtonImage{
|
||||
ToggleButtonImage: *b,
|
||||
|
||||
clicked: func(bool) {},
|
||||
menu: menu.NewLazyMenu(b.ToggleButton),
|
||||
}
|
||||
|
||||
tb.Connect("clicked", func() {
|
||||
tb.clicked(tb.GetActive())
|
||||
})
|
||||
|
||||
return tb
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetClicked(clicked func(bool)) {
|
||||
b.clicked = clicked
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetClickedIfTrue(clickedIfTrue func()) {
|
||||
b.clicked = func(clicked bool) {
|
||||
if clicked {
|
||||
clickedIfTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetNormalExtraMenu(items []menu.Item) {
|
||||
b.extraMenu = items
|
||||
b.SetNormal()
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetNormal() {
|
||||
b.SetLabelUnsafe(b.GetLabel())
|
||||
b.menu.SetItems(b.extraMenu)
|
||||
|
||||
if b.icon {
|
||||
b.Image.SetPlaceholderIcon("user-available-symbolic", b.Image.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetLoading() {
|
||||
b.SetLabelUnsafe(b.GetLabel())
|
||||
|
||||
// Reset the menu.
|
||||
b.menu.SetItems(b.extraMenu)
|
||||
|
||||
if b.icon {
|
||||
b.Image.SetPlaceholderIcon("content-loading-symbolic", b.Image.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetFailed(err error, retry func()) {
|
||||
b.Label.SetMarkup(rich.MakeRed(b.GetLabel()))
|
||||
|
||||
// Add a retry button, if any.
|
||||
b.menu.Reset()
|
||||
b.menu.AddItems(menu.SimpleItem("Retry", retry))
|
||||
b.menu.AddItems(b.extraMenu...)
|
||||
|
||||
// If we have an icon set, then we can use the failed icon.
|
||||
if b.icon {
|
||||
b.Image.SetPlaceholderIcon("computer-fail-symbolic", b.Image.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetPlaceholderIcon(iconName string, iconSzPx int) {
|
||||
b.icon = true
|
||||
b.Image.SetPlaceholderIcon(iconName, iconSzPx)
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetIcon(url string) {
|
||||
gts.ExecAsync(func() { b.SetIconUnsafe(url) })
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetIconUnsafe(url string) {
|
||||
b.icon = true
|
||||
b.Image.SetIconUnsafe(url)
|
||||
}
|
||||
|
||||
// type Row struct {
|
||||
// gtk.Box
|
||||
// Button *ToggleButtonImage
|
||||
// Children *gtk.Box
|
||||
// }
|
|
@ -2,12 +2,10 @@ package service
|
|||
|
||||
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/imgutil"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const IconSize = 32
|
||||
|
@ -40,9 +38,7 @@ func newHeader(svc cchat.Service) *header {
|
|||
box.Show()
|
||||
|
||||
if iconer, ok := svc.(cchat.Icon); ok {
|
||||
if err := iconer.Icon(reveal); err != nil {
|
||||
log.Error(errors.Wrap(err, "Error getting session logo"))
|
||||
}
|
||||
reveal.Image.AsyncSetIconer(iconer, "Error getting session logo")
|
||||
}
|
||||
|
||||
// Spawn the menu on right click.
|
||||
|
|
69
internal/ui/service/menu/menu.go
Normal file
69
internal/ui/service/menu/menu.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package menu
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
// LazyMenu is a menu with lazy-loaded capabilities.
|
||||
type LazyMenu struct {
|
||||
items []Item
|
||||
}
|
||||
|
||||
func NewLazyMenu(bindTo primitives.Connector) *LazyMenu {
|
||||
l := &LazyMenu{}
|
||||
bindTo.Connect("button-press-event", l.popup)
|
||||
return l
|
||||
}
|
||||
|
||||
func (m *LazyMenu) SetItems(items []Item) {
|
||||
m.items = items
|
||||
}
|
||||
|
||||
func (m *LazyMenu) AddItems(items ...Item) {
|
||||
m.items = append(m.items, items...)
|
||||
}
|
||||
|
||||
func (m *LazyMenu) Reset() {
|
||||
m.items = nil
|
||||
}
|
||||
|
||||
func (m *LazyMenu) popup(w gtk.IWidget, ev *gdk.Event) {
|
||||
// Is this a right click? Exit if not.
|
||||
if !gts.EventIsRightClick(ev) {
|
||||
return
|
||||
}
|
||||
|
||||
// Do nothing if there are no menu items.
|
||||
if len(m.items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var menu, _ = gtk.MenuNew()
|
||||
|
||||
for _, item := range m.items {
|
||||
mb, _ := gtk.MenuItemNewWithLabel(item.Name)
|
||||
mb.Connect("activate", item.Func)
|
||||
mb.Show()
|
||||
|
||||
if item.Extra != nil {
|
||||
item.Extra(mb)
|
||||
}
|
||||
|
||||
menu.Append(mb)
|
||||
}
|
||||
|
||||
menu.PopupAtPointer(ev)
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Name string
|
||||
Func func()
|
||||
Extra func(*gtk.MenuItem)
|
||||
}
|
||||
|
||||
func SimpleItem(name string, fn func()) Item {
|
||||
return Item{Name: name, Func: fn}
|
||||
}
|
|
@ -50,8 +50,8 @@ func (v *View) AddService(svc cchat.Service, ctrl Controller) *Container {
|
|||
}
|
||||
|
||||
type Controller interface {
|
||||
// MessageRowSelected is wrapped around session's MessageRowSelected.
|
||||
MessageRowSelected(*session.Row, *server.Row, cchat.ServerMessage)
|
||||
// RowSelected is wrapped around session's MessageRowSelected.
|
||||
RowSelected(*session.Row, *server.ServerRow, cchat.ServerMessage)
|
||||
// AuthenticateSession is called to spawn the authentication dialog.
|
||||
AuthenticateSession(*Container, cchat.Service)
|
||||
// OnSessionRemove is called to remove a session. This should also clear out
|
||||
|
|
|
@ -3,11 +3,13 @@ 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/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/button"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/loading"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/imgutil"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -17,149 +19,272 @@ const ChildrenMargin = 24
|
|||
const IconSize = 18
|
||||
|
||||
type Controller interface {
|
||||
MessageRowSelected(*Row, cchat.ServerMessage)
|
||||
RowSelected(*ServerRow, cchat.ServerMessage)
|
||||
}
|
||||
|
||||
type Row struct {
|
||||
*gtk.Box
|
||||
Button *rich.ToggleButtonImage
|
||||
Server cchat.Server
|
||||
Parent breadcrumb.Breadcrumber
|
||||
Button *button.ToggleButtonImage
|
||||
|
||||
ctrl Controller
|
||||
parentcrumb breadcrumb.Breadcrumber
|
||||
|
||||
// enum 1
|
||||
message cchat.ServerMessage
|
||||
|
||||
// enum 2
|
||||
children *Children
|
||||
children *Children
|
||||
serverList cchat.ServerList
|
||||
loaded bool
|
||||
}
|
||||
|
||||
func NewRow(parent breadcrumb.Breadcrumber, server cchat.Server, ctrl Controller) *Row {
|
||||
button := rich.NewToggleButtonImage(server.Name())
|
||||
func NewRow(parent breadcrumb.Breadcrumber, name text.Rich) *Row {
|
||||
button := button.NewToggleButtonImage(name)
|
||||
button.Box.SetHAlign(gtk.ALIGN_START)
|
||||
button.Image.AddProcessors(imgutil.Round(true))
|
||||
button.Image.SetSize(IconSize)
|
||||
button.SetRelief(gtk.RELIEF_NONE)
|
||||
button.Show()
|
||||
|
||||
if iconer, ok := server.(cchat.Icon); ok {
|
||||
button.Image.AsyncSetIcon(iconer.Icon, "Error getting server icon URL")
|
||||
}
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
box.SetMarginStart(ChildrenMargin)
|
||||
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,
|
||||
row := &Row{
|
||||
Box: box,
|
||||
Button: button,
|
||||
parentcrumb: parent,
|
||||
}
|
||||
|
||||
switch server := server.(type) {
|
||||
case cchat.ServerList:
|
||||
row.children = NewChildren(row, ctrl)
|
||||
row.children.SetServerList(server)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Deactivate calls the disconnect function then sets the button to false. This
|
||||
// function is not thread-safe.
|
||||
func (row *Row) Deactivate() {
|
||||
row.Button.SetSensitive(true) // allow clicks again
|
||||
row.Button.SetActive(false) // stop highlighting
|
||||
func (r *Row) Breadcrumb() breadcrumb.Breadcrumb {
|
||||
return breadcrumb.Try(r.parentcrumb, r.Button.GetText())
|
||||
}
|
||||
|
||||
func (row *Row) GetActive() bool {
|
||||
return row.Button.GetActive()
|
||||
func (r *Row) SetLabelUnsafe(name text.Rich) {
|
||||
r.Button.SetLabelUnsafe(name)
|
||||
}
|
||||
|
||||
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.Button.SetSensitive(false) // prevent clicks from deactivating
|
||||
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) SetIconer(v interface{}) {
|
||||
if iconer, ok := v.(cchat.Icon); ok {
|
||||
r.Button.Image.AsyncSetIconer(iconer, "Error getting server icon URL")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Row) Breadcrumb() breadcrumb.Breadcrumb {
|
||||
return breadcrumb.Try(r.Parent, r.Button.GetText())
|
||||
// SetServerList sets the row to a server list.
|
||||
func (r *Row) SetServerList(list cchat.ServerList, ctrl Controller) {
|
||||
r.Button.SetClicked(func(active bool) {
|
||||
r.SetRevealChild(active)
|
||||
})
|
||||
|
||||
r.children = NewChildren(r, ctrl)
|
||||
r.children.Show()
|
||||
|
||||
r.Box.PackStart(r.children, false, false, 0)
|
||||
r.serverList = list
|
||||
}
|
||||
|
||||
// Reset clears off all children servers. It's a no-op if there are none.
|
||||
func (r *Row) Reset() {
|
||||
if r.children != nil {
|
||||
// Remove everything from the children container.
|
||||
r.children.Reset()
|
||||
|
||||
// Remove the children container itself.
|
||||
r.Box.Remove(r.children)
|
||||
|
||||
// Reset the state.
|
||||
r.loaded = false
|
||||
r.serverList = nil
|
||||
r.children = nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetLoading is called by the parent struct.
|
||||
func (r *Row) SetLoading() {
|
||||
r.Button.SetLoading()
|
||||
r.SetSensitive(false)
|
||||
}
|
||||
|
||||
// SetFailed is shared between the parent struct and the children list. This is
|
||||
// because both of those errors share the same appearance, just different
|
||||
// callbacks.
|
||||
func (r *Row) SetFailed(err error, retry func()) {
|
||||
r.SetSensitive(true)
|
||||
r.SetTooltipText(err.Error())
|
||||
r.Button.SetFailed(err, retry)
|
||||
r.Button.Label.SetMarkup(rich.MakeRed(r.Button.GetLabel()))
|
||||
}
|
||||
|
||||
// SetDone is shared between the parent struct and the children list. This is
|
||||
// because both will use the same SetFailed.
|
||||
func (r *Row) SetDone() {
|
||||
r.Button.SetNormal()
|
||||
r.SetSensitive(true)
|
||||
r.SetTooltipText("")
|
||||
}
|
||||
|
||||
func (r *Row) SetNormalExtraMenu(items []menu.Item) {
|
||||
r.Button.SetNormalExtraMenu(items)
|
||||
r.SetSensitive(true)
|
||||
r.SetTooltipText("")
|
||||
}
|
||||
|
||||
func (r *Row) childrenFailed(err error) {
|
||||
// If the user chooses to retry, the list will automatically expand.
|
||||
r.SetFailed(err, func() { r.SetRevealChild(true) })
|
||||
}
|
||||
|
||||
func (r *Row) childrenDone() {
|
||||
r.loaded = true
|
||||
r.SetDone()
|
||||
}
|
||||
|
||||
// SetSelected is used for highlighting the current message server.
|
||||
func (r *Row) SetSelected(selected bool) {
|
||||
// Set the clickability the opposite as the boolean.
|
||||
r.Button.SetSensitive(!selected)
|
||||
|
||||
// Some special edge case that I forgot.
|
||||
if !selected {
|
||||
r.Button.SetActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Row) GetActive() bool {
|
||||
return r.Button.GetActive()
|
||||
}
|
||||
|
||||
// SetRevealChild reveals the list of servers. It does nothing if there are no
|
||||
// servers, meaning if Row does not represent a ServerList.
|
||||
func (r *Row) SetRevealChild(reveal bool) {
|
||||
// Do the above noop check.
|
||||
if r.children == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Actually reveal the children.
|
||||
r.children.SetRevealChild(reveal)
|
||||
|
||||
// If this isn't a reveal, then we don't need to load.
|
||||
if !reveal {
|
||||
return
|
||||
}
|
||||
|
||||
// If we haven't loaded yet and we're still not loading, then load.
|
||||
if !r.loaded && r.children.load == nil {
|
||||
r.Load()
|
||||
}
|
||||
}
|
||||
|
||||
// Load loads the row without uncollapsing it.
|
||||
func (r *Row) Load() {
|
||||
// Safeguard.
|
||||
if r.children == nil || r.serverList == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Set that we're now loading.
|
||||
r.children.setLoading()
|
||||
r.SetSensitive(false)
|
||||
|
||||
// Load the list of servers if we're still in loading mode.
|
||||
go func() {
|
||||
err := r.serverList.Servers(r.children)
|
||||
gts.ExecAsync(func() {
|
||||
// We're not loading anymore, so remove the loading circle.
|
||||
r.children.setNotLoading()
|
||||
// Restore clickability.
|
||||
r.SetSensitive(true)
|
||||
|
||||
// Use the childrenX method instead of SetX.
|
||||
if err != nil {
|
||||
r.childrenFailed(errors.Wrap(err, "Failed to get servers"))
|
||||
} else {
|
||||
r.childrenDone()
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// GetRevealChild returns whether or not the server list is expanded, or always
|
||||
// false if there is no server list.
|
||||
func (r *Row) GetRevealChild() bool {
|
||||
if r.children != nil {
|
||||
return r.children.GetRevealChild()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ServerRow struct {
|
||||
*Row
|
||||
Server cchat.Server
|
||||
}
|
||||
|
||||
func NewServerRow(p breadcrumb.Breadcrumber, server cchat.Server, ctrl Controller) *ServerRow {
|
||||
row := NewRow(p, server.Name())
|
||||
row.Show()
|
||||
row.SetIconer(server)
|
||||
primitives.AddClass(row, "server")
|
||||
|
||||
var serverRow = &ServerRow{Row: row, Server: server}
|
||||
|
||||
switch server := server.(type) {
|
||||
case cchat.ServerList:
|
||||
row.SetServerList(server, ctrl)
|
||||
primitives.AddClass(row, "server-list")
|
||||
|
||||
case cchat.ServerMessage:
|
||||
row.Button.SetClickedIfTrue(func() { ctrl.RowSelected(serverRow, server) })
|
||||
primitives.AddClass(row, "server-message")
|
||||
}
|
||||
|
||||
return serverRow
|
||||
}
|
||||
|
||||
// Children is a children server with a reference to the parent.
|
||||
type Children struct {
|
||||
*gtk.Revealer
|
||||
Main *gtk.Box
|
||||
List cchat.ServerList
|
||||
|
||||
rowctrl Controller
|
||||
|
||||
load *loading.Button // nil after init
|
||||
Rows []*Row
|
||||
load *loading.Button // only not nil while loading
|
||||
|
||||
Rows []*ServerRow
|
||||
Parent breadcrumb.Breadcrumber
|
||||
}
|
||||
|
||||
func NewChildren(parent breadcrumb.Breadcrumber, ctrl Controller) *Children {
|
||||
func NewChildren(p breadcrumb.Breadcrumber, 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()
|
||||
|
||||
return &Children{
|
||||
Revealer: rev,
|
||||
Main: main,
|
||||
rowctrl: ctrl,
|
||||
Parent: parent,
|
||||
Parent: p,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Children) SetLoading() {
|
||||
// If we're already loading, then exit.
|
||||
// setLoading shows the loading circle as a list child.
|
||||
func (c *Children) setLoading() {
|
||||
// Exit if we're already loading.
|
||||
if c.load != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Clear everything.
|
||||
c.Reset()
|
||||
|
||||
// Set the loading circle and stuff.
|
||||
c.load = loading.NewButton()
|
||||
c.load.Show()
|
||||
c.Main.Add(c.load)
|
||||
}
|
||||
|
||||
func (c *Children) Reset() {
|
||||
// Do we have the spinning circle button? If yes, remove it.
|
||||
if c.load != nil {
|
||||
c.Main.Remove(c.load)
|
||||
c.load = nil
|
||||
}
|
||||
|
||||
// Remove old servers from the list.
|
||||
for _, row := range c.Rows {
|
||||
c.Main.Remove(row)
|
||||
|
@ -169,14 +294,15 @@ func (c *Children) Reset() {
|
|||
c.Rows = nil
|
||||
}
|
||||
|
||||
func (c *Children) SetServerList(list cchat.ServerList) {
|
||||
c.List = list
|
||||
|
||||
go func() {
|
||||
if err := list.Servers(c); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get servers"))
|
||||
}
|
||||
}()
|
||||
// setNotLoading removes the loading circle, if any. This is not in Reset()
|
||||
// anymore, since the backend may not necessarily call SetServers.
|
||||
func (c *Children) setNotLoading() {
|
||||
// Do we have the spinning circle button? If yes, remove it.
|
||||
if c.load != nil {
|
||||
// Stop the loading mode. The reset function should do everything for us.
|
||||
c.Main.Remove(c.load)
|
||||
c.load = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Children) SetServers(servers []cchat.Server) {
|
||||
|
@ -193,10 +319,10 @@ func (c *Children) SetServers(servers []cchat.Server) {
|
|||
// Reset before inserting new servers.
|
||||
c.Reset()
|
||||
|
||||
c.Rows = make([]*Row, len(servers))
|
||||
c.Rows = make([]*ServerRow, len(servers))
|
||||
|
||||
for i, server := range servers {
|
||||
row := NewRow(c, server, c.rowctrl)
|
||||
row := NewServerRow(c, server, c.rowctrl)
|
||||
c.Rows[i] = row
|
||||
c.Main.Add(row)
|
||||
}
|
||||
|
|
|
@ -6,13 +6,10 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/keyring"
|
||||
"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/breadcrumb"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/imgutil"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -23,9 +20,9 @@ type Controller interface {
|
|||
// OnSessionDisconnect is called before a session is disconnected. This
|
||||
// function is used for cleanups.
|
||||
OnSessionDisconnect(*Row)
|
||||
// MessageRowSelected is called when a server that can display messages (aka
|
||||
// RowSelected is called when a server that can display messages (aka
|
||||
// implements ServerMessage) is called.
|
||||
MessageRowSelected(*Row, *server.Row, cchat.ServerMessage)
|
||||
RowSelected(*Row, *server.ServerRow, cchat.ServerMessage)
|
||||
// RestoreSession is called with the session ID to ask the controller to
|
||||
// restore it from keyring information.
|
||||
RestoreSession(*Row, string) // ID string, async
|
||||
|
@ -40,78 +37,35 @@ type Controller interface {
|
|||
// Row represents a single session, including the button header and the
|
||||
// children servers.
|
||||
type Row struct {
|
||||
*gtk.Box
|
||||
Button *rich.ToggleButtonImage
|
||||
Session cchat.Session
|
||||
Servers *server.Children
|
||||
*server.Row
|
||||
Session cchat.Session
|
||||
sessionID string // used for reconnection
|
||||
|
||||
ctrl Controller
|
||||
parent breadcrumb.Breadcrumber
|
||||
menuconstr func(*gtk.Menu)
|
||||
sessionID string // used for reconnection
|
||||
|
||||
// nil after calling SetSession()
|
||||
// krs keyring.Session
|
||||
ctrl Controller
|
||||
}
|
||||
|
||||
func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Row {
|
||||
row := newRow(parent, ctrl)
|
||||
row := newRow(parent, text.Rich{}, ctrl)
|
||||
row.SetSession(ses)
|
||||
return row
|
||||
}
|
||||
|
||||
func NewLoading(parent breadcrumb.Breadcrumber, id, name string, ctrl Controller) *Row {
|
||||
row := newRow(parent, ctrl)
|
||||
row := newRow(parent, text.Rich{Content: name}, ctrl)
|
||||
row.sessionID = id
|
||||
row.Button.SetLabelUnsafe(text.Rich{Content: name})
|
||||
row.setLoading()
|
||||
|
||||
row.Row.SetLoading()
|
||||
return row
|
||||
}
|
||||
|
||||
var dragEntries = []gtk.TargetEntry{
|
||||
primitives.NewTargetEntry("GTK_TOGGLE_BUTTON"),
|
||||
}
|
||||
var dragAtom = gdk.GdkAtomIntern("GTK_TOGGLE_BUTTON", true)
|
||||
func newRow(parent breadcrumb.Breadcrumber, name text.Rich, ctrl Controller) *Row {
|
||||
// Bind the row to .session in CSS.
|
||||
row := server.NewRow(parent, name)
|
||||
row.Button.SetPlaceholderIcon("user-invisible-symbolic", IconSize)
|
||||
row.Show()
|
||||
primitives.AddClass(row, "session")
|
||||
primitives.AddClass(row, "server-list")
|
||||
|
||||
func newRow(parent breadcrumb.Breadcrumber, ctrl Controller) *Row {
|
||||
row := &Row{
|
||||
ctrl: ctrl,
|
||||
parent: parent,
|
||||
}
|
||||
row.Servers = server.NewChildren(parent, row)
|
||||
row.Servers.SetLoading()
|
||||
|
||||
row.Button = rich.NewToggleButtonImage(text.Rich{})
|
||||
row.Button.Box.SetHAlign(gtk.ALIGN_START)
|
||||
row.Button.Image.AddProcessors(imgutil.Round(true))
|
||||
// Set the loading icon.
|
||||
row.Button.SetRelief(gtk.RELIEF_NONE)
|
||||
row.Button.Show()
|
||||
|
||||
// On click, toggle reveal.
|
||||
row.Button.Connect("clicked", func() {
|
||||
revealed := !row.Servers.GetRevealChild()
|
||||
row.Servers.SetRevealChild(revealed)
|
||||
row.Button.SetActive(revealed)
|
||||
})
|
||||
|
||||
row.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
row.Box.SetMarginStart(server.ChildrenMargin)
|
||||
row.Box.PackStart(row.Button, false, false, 0)
|
||||
row.Box.Show()
|
||||
|
||||
// Bind the box to .session in CSS.
|
||||
primitives.AddClass(row.Box, "session")
|
||||
// Bind the button to create a new menu.
|
||||
primitives.BindDynamicMenu(row.Button, func(menu *gtk.Menu) {
|
||||
row.menuconstr(menu)
|
||||
})
|
||||
|
||||
// noop, empty menu
|
||||
row.menuconstr = func(menu *gtk.Menu) {}
|
||||
|
||||
return row
|
||||
return &Row{Row: row, ctrl: ctrl}
|
||||
}
|
||||
|
||||
// RemoveSession removes itself from the session list.
|
||||
|
@ -130,12 +84,15 @@ func (r *Row) RemoveSession() {
|
|||
// ReconnectSession tries to reconnect with the keyring data. This is a slow
|
||||
// method but it's also a very cold path.
|
||||
func (r *Row) ReconnectSession() {
|
||||
// If we haven't ever connected:
|
||||
// If we haven't ever connected, then don't run. In a legitimate case, this
|
||||
// shouldn't happen.
|
||||
if r.sessionID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
r.setLoading()
|
||||
// Set the row as loading.
|
||||
r.Row.SetLoading()
|
||||
// Try to restore the session.
|
||||
r.ctrl.RestoreSession(r, r.sessionID)
|
||||
}
|
||||
|
||||
|
@ -145,8 +102,7 @@ func (r *Row) DisconnectSession() {
|
|||
r.ctrl.OnSessionDisconnect(r)
|
||||
|
||||
// Show visually that we're disconnected first by wiping all servers.
|
||||
r.Box.Remove(r.Servers)
|
||||
r.Servers.Reset()
|
||||
r.Reset()
|
||||
|
||||
// Set the offline icon to the button.
|
||||
r.Button.Image.SetPlaceholderIcon("user-invisible-symbolic", IconSize)
|
||||
|
@ -162,33 +118,18 @@ func (r *Row) DisconnectSession() {
|
|||
// Disconnect and wrap the error if any. Wrap works with a nil error.
|
||||
err := errors.Wrap(r.Session.Disconnect(), "Failed to disconnect.")
|
||||
return func() {
|
||||
// allow access to the menu
|
||||
// Allow access to the menu
|
||||
r.SetSensitive(true)
|
||||
|
||||
// set the menu to allow disconnection.
|
||||
r.menuconstr = func(menu *gtk.Menu) {
|
||||
primitives.AppendMenuItems(menu, []gtk.IMenuItem{
|
||||
primitives.MenuItem("Connect", r.ReconnectSession),
|
||||
primitives.MenuItem("Remove", r.RemoveSession),
|
||||
})
|
||||
}
|
||||
// Set the menu to allow disconnection.
|
||||
r.Button.SetNormalExtraMenu([]menu.Item{
|
||||
menu.SimpleItem("Connect", r.ReconnectSession),
|
||||
menu.SimpleItem("Remove", r.RemoveSession),
|
||||
})
|
||||
}, err
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Row) setLoading() {
|
||||
// set the loading icon
|
||||
r.Button.Image.SetPlaceholderIcon("content-loading-symbolic", IconSize)
|
||||
// set the loading icon in the servers list
|
||||
r.Servers.SetLoading()
|
||||
// restore the old label's color
|
||||
r.Button.SetLabelUnsafe(r.Button.GetLabel())
|
||||
// clear the tooltip
|
||||
r.SetTooltipText("")
|
||||
// blur - set the color darker
|
||||
r.SetSensitive(false)
|
||||
}
|
||||
|
||||
// KeyringSession returns a keyring session, or nil if the session cannot be
|
||||
// saved.
|
||||
func (r *Row) KeyringSession() *keyring.Session {
|
||||
|
@ -200,56 +141,35 @@ func (r *Row) ID() string {
|
|||
return r.sessionID
|
||||
}
|
||||
|
||||
// SetFailed sets the initial connect status to failed. Do note that session can
|
||||
// have 2 types of loading: loading the session and loading the server list.
|
||||
// This one sets the former.
|
||||
func (r *Row) SetFailed(err error) {
|
||||
// SetFailed, but also add the callback to retry.
|
||||
r.Row.SetFailed(err, r.ReconnectSession)
|
||||
}
|
||||
|
||||
// SetSession binds the session and marks the row as ready. It extends SetDone.
|
||||
func (r *Row) SetSession(ses cchat.Session) {
|
||||
r.Session = ses
|
||||
r.sessionID = ses.ID()
|
||||
r.SetLabelUnsafe(ses.Name())
|
||||
r.SetIconer(ses)
|
||||
|
||||
r.Servers.SetServerList(ses)
|
||||
r.Box.PackStart(r.Servers, false, false, 0)
|
||||
// Bind extra menu items before loading. These items won't be clickable
|
||||
// during loading.
|
||||
r.SetNormalExtraMenu([]menu.Item{
|
||||
menu.SimpleItem("Disconnect", r.DisconnectSession),
|
||||
menu.SimpleItem("Remove", r.RemoveSession),
|
||||
})
|
||||
|
||||
r.Button.SetLabelUnsafe(ses.Name())
|
||||
r.Button.Image.SetPlaceholderIcon("user-available-symbolic", IconSize)
|
||||
|
||||
r.SetSensitive(true)
|
||||
r.SetTooltipText("") // reset
|
||||
|
||||
// Try and set the session's icon.
|
||||
if iconer, ok := ses.(cchat.Icon); ok {
|
||||
r.Button.Image.AsyncSetIcon(iconer.Icon, "Error fetching session icon URL")
|
||||
}
|
||||
|
||||
// Set the menu with the disconnect button.
|
||||
r.menuconstr = func(menu *gtk.Menu) {
|
||||
primitives.AppendMenuItems(menu, []gtk.IMenuItem{
|
||||
primitives.MenuItem("Disconnect", r.DisconnectSession),
|
||||
primitives.MenuItem("Remove", r.RemoveSession),
|
||||
})
|
||||
}
|
||||
// Preload now.
|
||||
r.SetServerList(ses, r)
|
||||
r.Load()
|
||||
}
|
||||
|
||||
func (r *Row) SetFailed(err error) {
|
||||
// Allow the retry button to be pressed.
|
||||
r.menuconstr = func(menu *gtk.Menu) {
|
||||
primitives.AppendMenuItems(menu, []gtk.IMenuItem{
|
||||
primitives.MenuItem("Retry", r.ReconnectSession),
|
||||
primitives.MenuItem("Remove", r.RemoveSession),
|
||||
})
|
||||
}
|
||||
|
||||
r.SetSensitive(true)
|
||||
r.SetTooltipText(err.Error())
|
||||
// Intentional side-effect of not changing the actual label state.
|
||||
r.Button.Label.SetMarkup(rich.MakeRed(r.Button.GetLabel()))
|
||||
// Set the icon to a failed one.
|
||||
r.Button.Image.SetPlaceholderIcon("computer-fail-symbolic", IconSize)
|
||||
}
|
||||
|
||||
func (r *Row) MessageRowSelected(server *server.Row, smsg cchat.ServerMessage) {
|
||||
r.ctrl.MessageRowSelected(r, server, smsg)
|
||||
}
|
||||
|
||||
func (r *Row) Breadcrumb() breadcrumb.Breadcrumb {
|
||||
return breadcrumb.Try(r.parent, r.Button.GetLabel().Content)
|
||||
func (r *Row) RowSelected(server *server.ServerRow, smsg cchat.ServerMessage) {
|
||||
r.ctrl.RowSelected(r, server, smsg)
|
||||
}
|
||||
|
||||
// BindMover binds with the ID stored in the parent container to be used in the
|
||||
|
|
|
@ -40,7 +40,7 @@ type App struct {
|
|||
header *header
|
||||
|
||||
// used to keep track of what row to disconnect before switching
|
||||
lastDeactivator func()
|
||||
lastSelector func(bool)
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -85,18 +85,27 @@ func (app *App) OnSessionDisconnect(id string) {
|
|||
app.OnSessionRemove(id)
|
||||
}
|
||||
|
||||
func (app *App) MessageRowSelected(ses *session.Row, srv *server.Row, smsg cchat.ServerMessage) {
|
||||
func (app *App) RowSelected(ses *session.Row, srv *server.ServerRow, smsg cchat.ServerMessage) {
|
||||
// Is there an old row that we should deactivate?
|
||||
if app.lastDeactivator != nil {
|
||||
app.lastDeactivator()
|
||||
if app.lastSelector != nil {
|
||||
app.lastSelector(false)
|
||||
}
|
||||
|
||||
// Set the new row.
|
||||
app.lastDeactivator = srv.Deactivate
|
||||
app.lastSelector = srv.SetSelected
|
||||
app.lastSelector(true)
|
||||
|
||||
app.header.SetBreadcrumb(srv.Breadcrumb())
|
||||
|
||||
// Disable the server list because we don't want the user to switch around
|
||||
// while we're loading.
|
||||
app.window.Services.SetSensitive(false)
|
||||
|
||||
// Assert that server is also a list, then join the server.
|
||||
app.window.MessageView.JoinServer(ses.Session, smsg.(messages.ServerMessage))
|
||||
app.window.MessageView.JoinServer(ses.Session, smsg.(messages.ServerMessage), func() {
|
||||
// Re-enable the server list.
|
||||
app.window.Services.SetSensitive(true)
|
||||
})
|
||||
}
|
||||
|
||||
func (app *App) AuthenticateSession(container *service.Container, svc cchat.Service) {
|
||||
|
|
5
main.go
5
main.go
|
@ -6,9 +6,12 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/ui"
|
||||
"github.com/diamondburned/cchat/services"
|
||||
|
||||
_ "github.com/diamondburned/cchat-discord"
|
||||
_ "github.com/diamondburned/cchat-mock"
|
||||
)
|
||||
|
||||
var destructor = func() {}
|
||||
|
||||
func main() {
|
||||
gts.Main(func() gts.WindowHeaderer {
|
||||
var app = ui.NewApplication()
|
||||
|
@ -28,4 +31,6 @@ func main() {
|
|||
|
||||
return app
|
||||
})
|
||||
|
||||
destructor()
|
||||
}
|
||||
|
|
30
profile.go
30
profile.go
|
@ -3,18 +3,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
_ "net/http/pprof"
|
||||
|
||||
_ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
go func() {
|
||||
if err := http.ListenAndServe("localhost:42069", nil); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to start profiling HTTP server"))
|
||||
// go func() {
|
||||
// if err := http.ListenAndServe("localhost:42069", nil); err != nil {
|
||||
// log.Error(errors.Wrap(err, "Failed to start profiling HTTP server"))
|
||||
// }
|
||||
// }()
|
||||
|
||||
runtime.SetBlockProfileRate(1)
|
||||
|
||||
f, _ := os.Create("/tmp/cchat.pprof")
|
||||
p := pprof.Lookup("block")
|
||||
|
||||
destructor = func() {
|
||||
log.Println("==destructor==")
|
||||
|
||||
if err := p.WriteTo(f, 2); err != nil {
|
||||
log.Println("Profile writeTo error:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue