mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2024-10-31 19:44:23 +00:00
fixed scaling with image API refactor, minor tweaks
This commit is contained in:
parent
296346bbe7
commit
d3048fe08c
|
@ -182,13 +182,28 @@ func ExecSync(fn func()) <-chan struct{} {
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DoAfter calls f after the given duration in the Gtk main loop.
|
||||||
|
func DoAfter(d time.Duration, f func()) {
|
||||||
|
DoAfterMs(uint(d.Milliseconds()), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoAfterMs calls f after the given ms in the Gtk main loop.
|
||||||
|
func DoAfterMs(ms uint, f func()) {
|
||||||
|
_, err := glib.TimeoutAdd(ms, f)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk
|
// AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk
|
||||||
// main loop.
|
// main loop.
|
||||||
func AfterFunc(d time.Duration, f func()) (stop func()) {
|
func AfterFunc(d time.Duration, f func()) (stop func()) {
|
||||||
h, err := glib.TimeoutAdd(
|
return AfterMsFunc(uint(d.Milliseconds()), f)
|
||||||
uint(d.Milliseconds()),
|
}
|
||||||
func() bool { f(); return true },
|
|
||||||
)
|
// AfterMsFunc is similar to AfterFunc but takes in milliseconds instead.
|
||||||
|
func AfterMsFunc(ms uint, f func()) (stop func()) {
|
||||||
|
h, err := glib.TimeoutAdd(ms, func() bool { f(); return true })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,41 +7,31 @@ import (
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
"github.com/diamondburned/imgutil"
|
"github.com/diamondburned/imgutil"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
"github.com/gotk3/gotk3/glib"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
|
|
||||||
type ImageContainer interface {
|
type ImageContainer interface {
|
||||||
|
primitives.Connector
|
||||||
|
|
||||||
SetFromPixbuf(*gdk.Pixbuf)
|
SetFromPixbuf(*gdk.Pixbuf)
|
||||||
SetFromAnimation(*gdk.PixbufAnimation)
|
SetFromAnimation(*gdk.PixbufAnimation)
|
||||||
GetSizeRequest() (w, h int)
|
GetSizeRequest() (w, h int)
|
||||||
Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsyncImage loads an image. This method uses the cache.
|
// AsyncImage loads an image. This method uses the cache.
|
||||||
func AsyncImage(img ImageContainer, url string, procs ...imgutil.Processor) {
|
func AsyncImage(ctx context.Context,
|
||||||
asyncImage(img, url, procs)
|
img ImageContainer, url string, procs ...imgutil.Processor) {
|
||||||
}
|
|
||||||
|
|
||||||
// AsyncImageSized resizes using GdkPixbuf. This method uses the cache.
|
|
||||||
func AsyncImageSized(img ImageContainer, url string, procs ...imgutil.Processor) {
|
|
||||||
asyncImage(img, url, procs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func asyncImage(img ImageContainer, url string, procs []imgutil.Processor) {
|
|
||||||
if url == "" {
|
if url == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Add a processor to resize.
|
ctx = primitives.HandleDestroyCtx(ctx, img)
|
||||||
// procs = append(procs, imgutil.Resize(w, h))
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
connectDestroyer(img, cancel)
|
|
||||||
|
|
||||||
gif := strings.Contains(url, ".gif")
|
gif := strings.Contains(url, ".gif")
|
||||||
|
|
||||||
|
@ -65,12 +55,12 @@ func asyncImage(img ImageContainer, url string, procs []imgutil.Processor) {
|
||||||
go syncImage(ctx, l, url, procs, gif)
|
go syncImage(ctx, l, url, procs, gif)
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectDestroyer(img ImageContainer, cancel func()) {
|
// func connectDestroyer(img ImageContainer, cancel func()) {
|
||||||
img.Connect("destroy", func() {
|
// img.Connect("destroy", func() {
|
||||||
cancel()
|
// cancel()
|
||||||
img.SetFromPixbuf(nil)
|
// img.SetFromPixbuf(nil)
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
func areaPreparedFn(ctx context.Context, img ImageContainer, gif bool) func(l *gdk.PixbufLoader) {
|
func areaPreparedFn(ctx context.Context, img ImageContainer, gif bool) func(l *gdk.PixbufLoader) {
|
||||||
return func(l *gdk.PixbufLoader) {
|
return func(l *gdk.PixbufLoader) {
|
||||||
|
|
|
@ -187,21 +187,22 @@ func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage {
|
||||||
|
|
||||||
type Avatar struct {
|
type Avatar struct {
|
||||||
roundimage.Button
|
roundimage.Button
|
||||||
|
image *roundimage.StaticImage
|
||||||
url string
|
url string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAvatar() *Avatar {
|
func NewAvatar() *Avatar {
|
||||||
img, _ := roundimage.NewStaticImage(nil, 0)
|
img, _ := roundimage.NewStaticImage(nil, 0)
|
||||||
|
img.SetSizeRequest(AvatarSize, AvatarSize)
|
||||||
img.Show()
|
img.Show()
|
||||||
|
|
||||||
avatar, _ := roundimage.NewCustomButton(img)
|
avatar, _ := roundimage.NewCustomButton(img)
|
||||||
avatar.SetVAlign(gtk.ALIGN_START)
|
avatar.SetVAlign(gtk.ALIGN_START)
|
||||||
avatar.Image.SetSizeRequest(AvatarSize, AvatarSize)
|
|
||||||
|
|
||||||
// Default icon.
|
// Default icon.
|
||||||
primitives.SetImageIcon(img, "user-available-symbolic", AvatarSize)
|
primitives.SetImageIcon(img, "user-available-symbolic", AvatarSize)
|
||||||
|
|
||||||
return &Avatar{*avatar, ""}
|
return &Avatar{*avatar, img, ""}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetURL updates the Avatar to be that URL. It does nothing if URL is empty or
|
// SetURL updates the Avatar to be that URL. It does nothing if URL is empty or
|
||||||
|
@ -215,7 +216,7 @@ func (a *Avatar) SetURL(url string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
a.url = url
|
a.url = url
|
||||||
httputil.AsyncImageSized(a.Image, url)
|
a.image.SetImageURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ManuallySetURL sets the URL without downloading the image. It assumes the
|
// ManuallySetURL sets the URL without downloading the image. It assumes the
|
||||||
|
|
|
@ -40,24 +40,13 @@ var usernameCSS = primitives.PrepareCSS(`
|
||||||
`)
|
`)
|
||||||
|
|
||||||
func NewContainer() *Container {
|
func NewContainer() *Container {
|
||||||
avatar := rich.NewIcon(AvatarSize)
|
|
||||||
avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize)
|
|
||||||
avatar.Show()
|
|
||||||
|
|
||||||
label := rich.NewLabel(text.Rich{})
|
|
||||||
label.SetMaxWidthChars(35)
|
|
||||||
label.Show()
|
|
||||||
|
|
||||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
|
||||||
box.PackStart(avatar, false, false, 0)
|
|
||||||
box.PackStart(label, false, false, 0)
|
|
||||||
box.Show()
|
box.Show()
|
||||||
|
|
||||||
primitives.AddClass(box, "username-view")
|
primitives.AddClass(box, "username-view")
|
||||||
primitives.AttachCSS(box, usernameCSS)
|
primitives.AttachCSS(box, usernameCSS)
|
||||||
|
|
||||||
rev, _ := gtk.RevealerNew()
|
rev, _ := gtk.RevealerNew()
|
||||||
rev.SetRevealChild(false)
|
|
||||||
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
|
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
|
||||||
rev.SetTransitionDuration(50)
|
rev.SetTransitionDuration(50)
|
||||||
rev.Add(box)
|
rev.Add(box)
|
||||||
|
@ -67,12 +56,13 @@ func NewContainer() *Container {
|
||||||
// thread.
|
// thread.
|
||||||
currentRevealer = rev.SetRevealChild
|
currentRevealer = rev.SetRevealChild
|
||||||
|
|
||||||
return &Container{
|
container := Container{
|
||||||
Revealer: rev,
|
Revealer: rev,
|
||||||
main: box,
|
main: box,
|
||||||
avatar: avatar,
|
|
||||||
label: label,
|
|
||||||
}
|
}
|
||||||
|
container.Reset()
|
||||||
|
|
||||||
|
return &container
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Container) SetRevealChild(reveal bool) {
|
func (u *Container) SetRevealChild(reveal bool) {
|
||||||
|
@ -87,8 +77,18 @@ func (u *Container) shouldReveal() bool {
|
||||||
|
|
||||||
func (u *Container) Reset() {
|
func (u *Container) Reset() {
|
||||||
u.SetRevealChild(false)
|
u.SetRevealChild(false)
|
||||||
u.avatar.Reset()
|
|
||||||
u.label.Reset()
|
u.avatar = rich.NewIcon(AvatarSize)
|
||||||
|
u.avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize)
|
||||||
|
u.avatar.Show()
|
||||||
|
|
||||||
|
u.label = rich.NewLabel(text.Rich{})
|
||||||
|
u.label.SetMaxWidthChars(35)
|
||||||
|
u.label.Show()
|
||||||
|
|
||||||
|
primitives.RemoveChildren(u.main)
|
||||||
|
u.main.PackStart(u.avatar, false, false, 0)
|
||||||
|
u.main.PackStart(u.label, false, false, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update is not thread-safe.
|
// Update is not thread-safe.
|
||||||
|
|
|
@ -29,6 +29,7 @@ func New(parent gtk.IWidget, placeholder gtk.IWidget) *FaceView {
|
||||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
|
||||||
stack, _ := gtk.StackNew()
|
stack, _ := gtk.StackNew()
|
||||||
|
stack.SetTransitionDuration(55)
|
||||||
stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
|
stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
|
||||||
stack.AddNamed(parent, "main")
|
stack.AddNamed(parent, "main")
|
||||||
stack.AddNamed(placeholder, "placeholder")
|
stack.AddNamed(placeholder, "placeholder")
|
||||||
|
|
|
@ -278,13 +278,21 @@ func (v *View) MemberListUpdated(c *memberlist.Container) {
|
||||||
|
|
||||||
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
|
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
|
||||||
func (v *View) JoinServer(session cchat.Session, server cchat.Server, bc traverse.Breadcrumber) {
|
func (v *View) JoinServer(session cchat.Session, server cchat.Server, bc traverse.Breadcrumber) {
|
||||||
// Reset before setting.
|
|
||||||
v.Reset()
|
|
||||||
|
|
||||||
// Set the screen to loading.
|
// Set the screen to loading.
|
||||||
v.FaceView.SetLoading()
|
v.FaceView.SetLoading()
|
||||||
v.ctrl.OnMessageBusy()
|
v.ctrl.OnMessageBusy()
|
||||||
|
|
||||||
|
// We can be dumb. Reset afterwards so the animation goes smoother.
|
||||||
|
gts.DoAfterMs(
|
||||||
|
v.FaceView.GetTransitionDuration(),
|
||||||
|
func() { v.joinServer(session, server, bc) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) joinServer(session cchat.Session, server cchat.Server, bc traverse.Breadcrumber) {
|
||||||
|
// Reset before setting.
|
||||||
|
v.Reset()
|
||||||
|
|
||||||
// Get the messenger once.
|
// Get the messenger once.
|
||||||
var messenger = server.AsMessenger()
|
var messenger = server.AsMessenger()
|
||||||
// Exit if this server is not a messenger.
|
// Exit if this server is not a messenger.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package completion
|
package completion
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
|
@ -234,7 +235,7 @@ func (c *Completer) update() []gtk.IWidget {
|
||||||
pps = ppIcon
|
pps = ppIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
httputil.AsyncImageSized(img, entry.IconURL, pps...)
|
httputil.AsyncImage(context.Background(), img, entry.IconURL, pps...)
|
||||||
}
|
}
|
||||||
|
|
||||||
widgets[i] = b
|
widgets[i] = b
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package primitives
|
package primitives
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
|
@ -168,6 +169,12 @@ type Connector interface {
|
||||||
Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error)
|
Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HandleDestroyCtx(ctx context.Context, connector Connector) context.Context {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
connector.Connect("destroy", cancel)
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
func BindMenu(connector Connector, menu *gtk.Menu) {
|
func BindMenu(connector Connector, menu *gtk.Menu) {
|
||||||
connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
|
connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
|
||||||
if gts.EventIsRightClick(ev) {
|
if gts.EventIsRightClick(ev) {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package roundimage
|
package roundimage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||||
"github.com/diamondburned/handy"
|
"github.com/diamondburned/handy"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
@ -24,6 +26,7 @@ func TrySetText(imager Imager, text string) {
|
||||||
type Avatar struct {
|
type Avatar struct {
|
||||||
handy.Avatar
|
handy.Avatar
|
||||||
pixbuf *gdk.Pixbuf
|
pixbuf *gdk.Pixbuf
|
||||||
|
url string
|
||||||
size int
|
size int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,33 +60,48 @@ func (a *Avatar) SetSizeRequest(w, h int) {
|
||||||
min = h
|
min = h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.size = min
|
||||||
a.Avatar.SetSize(min)
|
a.Avatar.SetSize(min)
|
||||||
a.Avatar.SetSizeRequest(w, h)
|
a.Avatar.SetSizeRequest(w, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Avatar) loadFunc(size int) *gdk.Pixbuf {
|
func (a *Avatar) loadFunc(size int) *gdk.Pixbuf {
|
||||||
if a.pixbuf == nil {
|
// No URL, draw nothing.
|
||||||
a.size = size
|
if a.url == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.size != size {
|
if a.pixbuf != nil && a.size == size {
|
||||||
a.size = size
|
return a.pixbuf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refetch and rescale.
|
||||||
|
a.size = size
|
||||||
|
// Technically, this will recurse. However, we're changing the size, so
|
||||||
|
// eventually it should stop.
|
||||||
|
httputil.AsyncImage(context.Background(), a, a.url)
|
||||||
|
|
||||||
|
if a.pixbuf == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporarily resize for now.
|
||||||
p, err := a.pixbuf.ScaleSimple(size, size, gdk.INTERP_HYPER)
|
p, err := a.pixbuf.ScaleSimple(size, size, gdk.INTERP_HYPER)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return a.pixbuf
|
p = a.pixbuf
|
||||||
}
|
}
|
||||||
|
|
||||||
a.pixbuf = p
|
return p
|
||||||
}
|
|
||||||
|
|
||||||
return a.pixbuf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRadius is a no-op.
|
// SetRadius is a no-op.
|
||||||
func (a *Avatar) SetRadius(float64) {}
|
func (a *Avatar) SetRadius(float64) {}
|
||||||
|
|
||||||
|
func (a *Avatar) SetImageURL(url string) {
|
||||||
|
a.url = url
|
||||||
|
a.Avatar.SetImageLoadFunc(a.loadFunc)
|
||||||
|
}
|
||||||
|
|
||||||
// SetFromPixbuf sets the pixbuf.
|
// SetFromPixbuf sets the pixbuf.
|
||||||
func (a *Avatar) SetFromPixbuf(pb *gdk.Pixbuf) {
|
func (a *Avatar) SetFromPixbuf(pb *gdk.Pixbuf) {
|
||||||
a.pixbuf = pb
|
a.pixbuf = pb
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package roundimage
|
package roundimage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
"github.com/diamondburned/imgutil"
|
||||||
"github.com/gotk3/gotk3/cairo"
|
"github.com/gotk3/gotk3/cairo"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
@ -40,6 +42,7 @@ type Imager interface {
|
||||||
type Image struct {
|
type Image struct {
|
||||||
*gtk.Image
|
*gtk.Image
|
||||||
Radius float64
|
Radius float64
|
||||||
|
procs []imgutil.Processor
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Imager = (*Image)(nil)
|
var _ Imager = (*Image)(nil)
|
||||||
|
@ -60,10 +63,19 @@ func NewImage(radius float64) (*Image, error) {
|
||||||
return image, nil
|
return image, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Image) AddProcessor(procs ...imgutil.Processor) {
|
||||||
|
i.procs = append(i.procs, procs...)
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Image) GetImage() *gtk.Image {
|
func (i *Image) GetImage() *gtk.Image {
|
||||||
return i.Image
|
return i.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Image) SetImageURL(url string) {
|
||||||
|
// No dynamic sizing support; yolo.
|
||||||
|
httputil.AsyncImage(context.Background(), i, url, i.procs...)
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Image) SetRadius(r float64) {
|
func (i *Image) SetRadius(r float64) {
|
||||||
i.Radius = r
|
i.Radius = r
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package roundimage
|
package roundimage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
@ -9,6 +11,7 @@ import (
|
||||||
// StaticImage is an image that only plays a GIF if it's hovered on top of.
|
// StaticImage is an image that only plays a GIF if it's hovered on top of.
|
||||||
type StaticImage struct {
|
type StaticImage struct {
|
||||||
*Image
|
*Image
|
||||||
|
animating bool
|
||||||
animation *gdk.PixbufAnimation
|
animation *gdk.PixbufAnimation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +27,7 @@ func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var s = &StaticImage{i, nil}
|
var s = &StaticImage{i, false, nil}
|
||||||
if parent != nil {
|
if parent != nil {
|
||||||
s.ConnectHandlers(parent)
|
s.ConnectHandlers(parent)
|
||||||
}
|
}
|
||||||
|
@ -34,17 +37,24 @@ func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage,
|
||||||
|
|
||||||
func (s *StaticImage) ConnectHandlers(connector primitives.Connector) {
|
func (s *StaticImage) ConnectHandlers(connector primitives.Connector) {
|
||||||
connector.Connect("enter-notify-event", func() {
|
connector.Connect("enter-notify-event", func() {
|
||||||
if s.animation != nil {
|
if s.animation != nil && !s.animating {
|
||||||
|
s.animating = true
|
||||||
s.Image.SetFromAnimation(s.animation)
|
s.Image.SetFromAnimation(s.animation)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
connector.Connect("leave-notify-event", func() {
|
connector.Connect("leave-notify-event", func() {
|
||||||
if s.animation != nil {
|
if s.animation != nil && s.animating {
|
||||||
|
s.animating = false
|
||||||
s.Image.SetFromPixbuf(s.animation.GetStaticImage())
|
s.Image.SetFromPixbuf(s.animation.GetStaticImage())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StaticImage) SetImageURL(url string) {
|
||||||
|
// No dynamic sizing support; yolo.
|
||||||
|
httputil.AsyncImage(context.Background(), s, url, s.Image.procs...)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *StaticImage) SetFromPixbuf(pb *gdk.Pixbuf) {
|
func (s *StaticImage) SetFromPixbuf(pb *gdk.Pixbuf) {
|
||||||
s.animation = nil
|
s.animation = nil
|
||||||
s.Image.SetFromPixbuf(pb)
|
s.Image.SetFromPixbuf(pb)
|
||||||
|
|
|
@ -3,8 +3,6 @@ package rich
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"reflect"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
)
|
)
|
||||||
|
@ -46,63 +44,3 @@ func AsyncUse(r Reuser, fn AsyncUser) {
|
||||||
}, nil
|
}, 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()
|
|
||||||
|
|
||||||
swapfn reflect.Value // reflect fn
|
|
||||||
arg1type reflect.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Reuser = (*Reusable)(nil)
|
|
||||||
|
|
||||||
func NewReusable(swapperFn interface{}) *Reusable {
|
|
||||||
r := Reusable{}
|
|
||||||
r.swapfn = reflect.ValueOf(swapperFn)
|
|
||||||
r.arg1type = r.swapfn.Type().In(0)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reusable) SwapResource(v interface{}, cancel func()) {
|
|
||||||
r.swapfn.Call([]reflect.Value{reflect.ValueOf(v)})
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/diamondburned/imgutil"
|
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -19,10 +18,12 @@ type IconerFn = func(context.Context, cchat.IconContainer) (func(), error)
|
||||||
|
|
||||||
type RoundIconContainer interface {
|
type RoundIconContainer interface {
|
||||||
gtk.IWidget
|
gtk.IWidget
|
||||||
httputil.ImageContainer
|
|
||||||
primitives.ImageIconSetter
|
primitives.ImageIconSetter
|
||||||
roundimage.RadiusSetter
|
roundimage.RadiusSetter
|
||||||
|
|
||||||
|
SetImageURL(url string)
|
||||||
|
|
||||||
GetStorageType() gtk.ImageType
|
GetStorageType() gtk.ImageType
|
||||||
GetPixbuf() *gdk.Pixbuf
|
GetPixbuf() *gdk.Pixbuf
|
||||||
GetAnimation() *gdk.PixbufAnimation
|
GetAnimation() *gdk.PixbufAnimation
|
||||||
|
@ -35,11 +36,9 @@ var (
|
||||||
|
|
||||||
// Icon represents a rounded image container.
|
// Icon represents a rounded image container.
|
||||||
type Icon struct {
|
type Icon struct {
|
||||||
*gtk.Revealer
|
*gtk.Revealer // TODO move out
|
||||||
Image RoundIconContainer
|
|
||||||
procs []imgutil.Processor
|
|
||||||
|
|
||||||
r *Reusable
|
Image RoundIconContainer
|
||||||
|
|
||||||
// state
|
// state
|
||||||
url string
|
url string
|
||||||
|
@ -49,13 +48,13 @@ const DefaultIconSize = 16
|
||||||
|
|
||||||
var _ cchat.IconContainer = (*Icon)(nil)
|
var _ cchat.IconContainer = (*Icon)(nil)
|
||||||
|
|
||||||
func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
|
func NewIcon(sizepx int) *Icon {
|
||||||
img, _ := roundimage.NewImage(0)
|
img, _ := roundimage.NewImage(0)
|
||||||
img.Show()
|
img.Show()
|
||||||
return NewCustomIcon(img, sizepx, procs...)
|
return NewCustomIcon(img, sizepx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCustomIcon(img RoundIconContainer, sizepx int, procs ...imgutil.Processor) *Icon {
|
func NewCustomIcon(img RoundIconContainer, sizepx int) *Icon {
|
||||||
if sizepx == 0 {
|
if sizepx == 0 {
|
||||||
sizepx = DefaultIconSize
|
sizepx = DefaultIconSize
|
||||||
}
|
}
|
||||||
|
@ -69,24 +68,12 @@ func NewCustomIcon(img RoundIconContainer, sizepx int, procs ...imgutil.Processo
|
||||||
i := &Icon{
|
i := &Icon{
|
||||||
Revealer: rev,
|
Revealer: rev,
|
||||||
Image: img,
|
Image: img,
|
||||||
procs: procs,
|
|
||||||
}
|
}
|
||||||
i.SetSize(sizepx)
|
i.SetSize(sizepx)
|
||||||
i.r = NewReusable(func(ni *nullIcon) {
|
|
||||||
i.SetIconUnsafe(ni.url)
|
|
||||||
})
|
|
||||||
|
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset wipes the state to be just after construction.
|
|
||||||
func (i *Icon) Reset() {
|
|
||||||
i.url = ""
|
|
||||||
i.r.Invalidate() // invalidate async fetching images
|
|
||||||
i.Revealer.SetRevealChild(false)
|
|
||||||
i.Image.SetFromPixbuf(nil) // destroy old pb
|
|
||||||
}
|
|
||||||
|
|
||||||
// URL is not thread-safe.
|
// URL is not thread-safe.
|
||||||
func (i *Icon) URL() string {
|
func (i *Icon) URL() string {
|
||||||
return i.url
|
return i.url
|
||||||
|
@ -127,11 +114,6 @@ func (i *Icon) Size() int {
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddProcessors is not thread-safe.
|
|
||||||
func (i *Icon) AddProcessors(procs ...imgutil.Processor) {
|
|
||||||
i.procs = append(i.procs, procs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetIcon is thread-safe.
|
// SetIcon is thread-safe.
|
||||||
func (i *Icon) SetIcon(url string) {
|
func (i *Icon) SetIcon(url string) {
|
||||||
gts.ExecAsync(func() { i.SetIconUnsafe(url) })
|
gts.ExecAsync(func() { i.SetIconUnsafe(url) })
|
||||||
|
@ -141,45 +123,48 @@ func (i *Icon) AsyncSetIconer(iconer cchat.Iconer, errwrap string) {
|
||||||
// Reveal to show the placeholder.
|
// Reveal to show the placeholder.
|
||||||
i.SetRevealChild(true)
|
i.SetRevealChild(true)
|
||||||
|
|
||||||
AsyncUse(i.r, func(ctx context.Context) (interface{}, func(), error) {
|
// I have a hunch this will never work; as long as Go keeps a reference with
|
||||||
ni := &nullIcon{}
|
// iconer.Icon, then destroy will never be triggered.
|
||||||
f, err := iconer.Icon(ctx, ni)
|
ctx := primitives.HandleDestroyCtx(context.Background(), i)
|
||||||
return ni, f, errors.Wrap(err, errwrap)
|
gts.Async(func() (func(), error) {
|
||||||
|
f, err := iconer.Icon(ctx, i)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to load iconer")
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() { i.Connect("destroy", f) }, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetIconUnsafe is not thread-safe.
|
// SetIconUnsafe is not thread-safe.
|
||||||
func (i *Icon) SetIconUnsafe(url string) {
|
func (i *Icon) SetIconUnsafe(url string) {
|
||||||
i.Image.SetRadius(0) // round
|
// Setting the radius here since we resetted it for a placeholder icon.
|
||||||
|
i.Image.SetRadius(0)
|
||||||
i.SetRevealChild(true)
|
i.SetRevealChild(true)
|
||||||
i.url = url
|
i.url = url
|
||||||
i.updateAsync()
|
i.Image.SetImageURL(i.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Icon) updateAsync() {
|
// type EventIcon struct {
|
||||||
httputil.AsyncImageSized(i.Image, i.url, i.procs...)
|
// *gtk.EventBox
|
||||||
}
|
// Icon *Icon
|
||||||
|
// }
|
||||||
|
|
||||||
type EventIcon struct {
|
// func NewEventIcon(sizepx int) *EventIcon {
|
||||||
*gtk.EventBox
|
// icn := NewIcon(sizepx)
|
||||||
Icon *Icon
|
// return WrapEventIcon(icn)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func NewEventIcon(sizepx int, pp ...imgutil.Processor) *EventIcon {
|
// func WrapEventIcon(icn *Icon) *EventIcon {
|
||||||
icn := NewIcon(sizepx, pp...)
|
// icn.Show()
|
||||||
return WrapEventIcon(icn)
|
// evb, _ := gtk.EventBoxNew()
|
||||||
}
|
// evb.Add(icn)
|
||||||
|
|
||||||
func WrapEventIcon(icn *Icon) *EventIcon {
|
// return &EventIcon{
|
||||||
icn.Show()
|
// EventBox: evb,
|
||||||
evb, _ := gtk.EventBoxNew()
|
// Icon: icn,
|
||||||
evb.Add(icn)
|
// }
|
||||||
|
// }
|
||||||
return &EventIcon{
|
|
||||||
EventBox: evb,
|
|
||||||
Icon: icn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToggleButtonImage struct {
|
type ToggleButtonImage struct {
|
||||||
gtk.ToggleButton
|
gtk.ToggleButton
|
||||||
|
@ -232,8 +217,3 @@ func NewCustomToggleButtonImage(img RoundIconContainer, content text.Rich) *Togg
|
||||||
Box: box,
|
Box: box,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ToggleButtonImage) Reset() {
|
|
||||||
t.Labeler.Reset()
|
|
||||||
t.Image.Reset()
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/gotk3/gotk3/pango"
|
"github.com/gotk3/gotk3/pango"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Labeler interface {
|
type Labeler interface {
|
||||||
|
@ -20,13 +21,11 @@ type Labeler interface {
|
||||||
SetLabelUnsafe(text.Rich)
|
SetLabelUnsafe(text.Rich)
|
||||||
GetLabel() text.Rich
|
GetLabel() text.Rich
|
||||||
GetText() string
|
GetText() string
|
||||||
Reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuperLabeler represents a label that inherits the current labeler.
|
// SuperLabeler represents a label that inherits the current labeler.
|
||||||
type SuperLabeler interface {
|
type SuperLabeler interface {
|
||||||
SetLabelUnsafe(text.Rich)
|
SetLabelUnsafe(text.Rich)
|
||||||
Reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LabelerFn = func(context.Context, cchat.LabelContainer) (func(), error)
|
type LabelerFn = func(context.Context, cchat.LabelContainer) (func(), error)
|
||||||
|
@ -35,9 +34,6 @@ type Label struct {
|
||||||
gtk.Label
|
gtk.Label
|
||||||
Current text.Rich
|
Current text.Rich
|
||||||
|
|
||||||
// Reusable primitive.
|
|
||||||
r *Reusable
|
|
||||||
|
|
||||||
// super unexported field for inheritance
|
// super unexported field for inheritance
|
||||||
super SuperLabeler
|
super SuperLabeler
|
||||||
}
|
}
|
||||||
|
@ -58,11 +54,6 @@ func NewLabel(content text.Rich) *Label {
|
||||||
Current: content,
|
Current: content,
|
||||||
}
|
}
|
||||||
|
|
||||||
// reusable primitive
|
|
||||||
l.r = NewReusable(func(nl *nullLabel) {
|
|
||||||
l.SetLabelUnsafe(nl.Rich)
|
|
||||||
})
|
|
||||||
|
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,23 +71,15 @@ func (l *Label) validsuper() bool {
|
||||||
return !ok && l.super != nil
|
return !ok && l.super != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset wipes the state to be just after construction. If super is not nil,
|
|
||||||
// then it's reset as well.
|
|
||||||
func (l *Label) Reset() {
|
|
||||||
l.Current = text.Rich{}
|
|
||||||
l.r.Invalidate()
|
|
||||||
l.Label.SetText("")
|
|
||||||
|
|
||||||
if l.validsuper() {
|
|
||||||
l.super.Reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Label) AsyncSetLabel(fn LabelerFn, info string) {
|
func (l *Label) AsyncSetLabel(fn LabelerFn, info string) {
|
||||||
AsyncUse(l.r, func(ctx context.Context) (interface{}, func(), error) {
|
ctx := primitives.HandleDestroyCtx(context.Background(), l)
|
||||||
nl := &nullLabel{}
|
gts.Async(func() (func(), error) {
|
||||||
f, err := fn(ctx, nl)
|
f, err := fn(ctx, l)
|
||||||
return nl, f, err
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to load iconer")
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() { l.Connect("destroy", f) }, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package labeluri
|
package labeluri
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -211,7 +212,7 @@ func popoverImg(url string, round bool) gtk.IWidget {
|
||||||
img.SetHAlign(gtk.ALIGN_CENTER)
|
img.SetHAlign(gtk.ALIGN_CENTER)
|
||||||
img.Show()
|
img.Show()
|
||||||
|
|
||||||
httputil.AsyncImageSized(idl, url)
|
httputil.AsyncImage(context.Background(), idl, url)
|
||||||
|
|
||||||
btn.SetHAlign(gtk.ALIGN_CENTER)
|
btn.SetHAlign(gtk.ALIGN_CENTER)
|
||||||
btn.SetRelief(gtk.RELIEF_NONE)
|
btn.SetRelief(gtk.RELIEF_NONE)
|
||||||
|
@ -268,7 +269,7 @@ func bind(connector WidgetConnector, activator func(uri string, r gdk.Rectangle)
|
||||||
img.Show()
|
img.Show()
|
||||||
|
|
||||||
// Asynchronously fetch the image.
|
// Asynchronously fetch the image.
|
||||||
httputil.AsyncImageSized(img, uri)
|
httputil.AsyncImage(context.Background(), img, uri)
|
||||||
|
|
||||||
btn, _ := gtk.ButtonNew()
|
btn, _ := gtk.ButtonNew()
|
||||||
btn.Add(img)
|
btn.Add(img)
|
||||||
|
|
|
@ -20,10 +20,3 @@ type nullLabel struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *nullLabel) SetLabel(t text.Rich) { n.Rich = t }
|
func (n *nullLabel) SetLabel(t text.Rich) { n.Rich = t }
|
||||||
|
|
||||||
// used for grabbing url without changing state
|
|
||||||
type nullIcon struct {
|
|
||||||
url string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *nullIcon) SetIcon(url string) { i.url = url }
|
|
||||||
|
|
|
@ -22,9 +22,6 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const IconSize = 48
|
|
||||||
const IconName = "face-plain-symbolic"
|
|
||||||
|
|
||||||
// Servicer extends server.RowController to add session.
|
// Servicer extends server.RowController to add session.
|
||||||
type Servicer interface {
|
type Servicer interface {
|
||||||
// Service asks the controller for its service.
|
// Service asks the controller for its service.
|
||||||
|
@ -53,7 +50,8 @@ type Servicer interface {
|
||||||
type Row struct {
|
type Row struct {
|
||||||
*gtk.ListBoxRow
|
*gtk.ListBoxRow
|
||||||
avatar *roundimage.Avatar
|
avatar *roundimage.Avatar
|
||||||
icon *rich.EventIcon // nilable
|
iconBox *gtk.EventBox
|
||||||
|
icon *rich.Icon // nillable
|
||||||
|
|
||||||
parentcrumb traverse.Breadcrumber
|
parentcrumb traverse.Breadcrumber
|
||||||
|
|
||||||
|
@ -107,6 +105,10 @@ var rowCSS = primitives.PrepareClassCSS("session-row",
|
||||||
0.65
|
0.65
|
||||||
), 0.85);
|
), 0.85);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-row.failed {
|
||||||
|
background-color: alpha(red, 0.45);
|
||||||
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
var rowIconCSS = primitives.PrepareClassCSS("session-icon", `
|
var rowIconCSS = primitives.PrepareClassCSS("session-icon", `
|
||||||
|
@ -114,12 +116,19 @@ var rowIconCSS = primitives.PrepareClassCSS("session-icon", `
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-icon.failed {
|
|
||||||
background-color: alpha(red, 0.45);
|
|
||||||
}
|
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
const IconSize = 48
|
||||||
|
const IconName = "face-plain-symbolic"
|
||||||
|
|
||||||
|
func newIcon(img rich.RoundIconContainer) *rich.Icon {
|
||||||
|
icon := rich.NewCustomIcon(img, IconSize)
|
||||||
|
icon.SetPlaceholderIcon(IconName, IconSize)
|
||||||
|
icon.ShowAll()
|
||||||
|
rowIconCSS(icon)
|
||||||
|
return icon
|
||||||
|
}
|
||||||
|
|
||||||
func New(parent traverse.Breadcrumber, ses cchat.Session, ctrl Servicer) *Row {
|
func New(parent traverse.Breadcrumber, ses cchat.Session, ctrl Servicer) *Row {
|
||||||
row := newRow(parent, text.Rich{}, ctrl)
|
row := newRow(parent, text.Rich{}, ctrl)
|
||||||
row.SetSession(ses)
|
row.SetSession(ses)
|
||||||
|
@ -143,13 +152,8 @@ func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Servicer) *Row {
|
||||||
row.avatar.SetText(name.Content)
|
row.avatar.SetText(name.Content)
|
||||||
row.avatar.Show()
|
row.avatar.Show()
|
||||||
|
|
||||||
icon := rich.NewCustomIcon(row.avatar, IconSize)
|
row.iconBox, _ = gtk.EventBoxNew()
|
||||||
icon.Show()
|
row.iconBox.Show()
|
||||||
|
|
||||||
row.icon = rich.WrapEventIcon(icon)
|
|
||||||
row.icon.Icon.SetPlaceholderIcon(IconName, IconSize)
|
|
||||||
row.icon.Show()
|
|
||||||
rowIconCSS(row.icon.Icon)
|
|
||||||
|
|
||||||
row.ListBoxRow, _ = gtk.ListBoxRowNew()
|
row.ListBoxRow, _ = gtk.ListBoxRowNew()
|
||||||
row.ListBoxRow.Show()
|
row.ListBoxRow.Show()
|
||||||
|
@ -165,7 +169,7 @@ func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Servicer) *Row {
|
||||||
row.ActionsMenu.InsertActionGroup(row)
|
row.ActionsMenu.InsertActionGroup(row)
|
||||||
|
|
||||||
// Bind right clicks and show a popover menu on such event.
|
// Bind right clicks and show a popover menu on such event.
|
||||||
row.icon.Connect("button-press-event", func(_ gtk.IWidget, ev *gdk.Event) {
|
row.iconBox.Connect("button-press-event", func(_ gtk.IWidget, ev *gdk.Event) {
|
||||||
if gts.EventIsRightClick(ev) {
|
if gts.EventIsRightClick(ev) {
|
||||||
row.ActionsMenu.Popup(row)
|
row.ActionsMenu.Popup(row)
|
||||||
}
|
}
|
||||||
|
@ -215,8 +219,13 @@ func (r *Row) Reset() {
|
||||||
r.ActionsMenu.Reset() // wipe menu items
|
r.ActionsMenu.Reset() // wipe menu items
|
||||||
r.ActionsMenu.AddAction("Remove", r.RemoveSession)
|
r.ActionsMenu.AddAction("Remove", r.RemoveSession)
|
||||||
|
|
||||||
|
if r.icon == nil {
|
||||||
|
r.icon = newIcon(r.avatar)
|
||||||
|
r.iconBox.Add(r.icon)
|
||||||
|
}
|
||||||
|
|
||||||
// Set a lame placeholder icon.
|
// Set a lame placeholder icon.
|
||||||
r.icon.Icon.SetPlaceholderIcon("folder-remote-symbolic", IconSize)
|
r.icon.SetPlaceholderIcon("folder-remote-symbolic", IconSize)
|
||||||
|
|
||||||
r.Session = nil
|
r.Session = nil
|
||||||
r.cmder = nil
|
r.cmder = nil
|
||||||
|
@ -258,13 +267,14 @@ func (r *Row) SetLoading() {
|
||||||
r.Session = nil
|
r.Session = nil
|
||||||
|
|
||||||
// Reset the icon.
|
// Reset the icon.
|
||||||
r.icon.Icon.Reset()
|
primitives.RemoveChildren(r.iconBox)
|
||||||
|
r.icon = nil
|
||||||
|
|
||||||
// Remove everything from the row, including the icon.
|
// Remove everything from the row, including the icon.
|
||||||
primitives.RemoveChildren(r)
|
primitives.RemoveChildren(r)
|
||||||
|
|
||||||
// Remove the failed class.
|
// Remove the failed class.
|
||||||
primitives.RemoveClass(r.icon.Icon, "failed")
|
primitives.RemoveClass(r, "failed")
|
||||||
|
|
||||||
// Add a loading circle.
|
// Add a loading circle.
|
||||||
spin := spinner.New()
|
spin := spinner.New()
|
||||||
|
@ -287,15 +297,18 @@ func (r *Row) SetFailed(err error) {
|
||||||
r.SetSensitive(true)
|
r.SetSensitive(true)
|
||||||
// Remove everything off the row.
|
// Remove everything off the row.
|
||||||
primitives.RemoveChildren(r)
|
primitives.RemoveChildren(r)
|
||||||
// Add the icon.
|
// Mark the row as failed.
|
||||||
r.Add(r.icon)
|
primitives.AddClass(r, "failed")
|
||||||
// Set the button to a retry icon.
|
|
||||||
r.icon.Icon.SetPlaceholderIcon("view-refresh-symbolic", IconSize)
|
|
||||||
// Mark the icon as failed.
|
|
||||||
primitives.AddClass(r.icon.Icon, "failed")
|
|
||||||
|
|
||||||
// SetFailed, but also add the callback to retry.
|
if r.icon == nil {
|
||||||
// r.Row.SetFailed(err, r.ReconnectSession)
|
r.icon = newIcon(r.avatar)
|
||||||
|
r.iconBox.Add(r.icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the icon.
|
||||||
|
r.Add(r.iconBox)
|
||||||
|
// Set the button to a retry icon.
|
||||||
|
r.icon.SetPlaceholderIcon("view-refresh-symbolic", IconSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Row) RestoreSession(res cchat.SessionRestorer, k keyring.Session) {
|
func (r *Row) RestoreSession(res cchat.SessionRestorer, k keyring.Session) {
|
||||||
|
@ -318,18 +331,24 @@ func (r *Row) SetSession(ses cchat.Session) {
|
||||||
r.Session = ses
|
r.Session = ses
|
||||||
r.sessionID = ses.ID()
|
r.sessionID = ses.ID()
|
||||||
r.SetTooltipMarkup(markup.Render(ses.Name()))
|
r.SetTooltipMarkup(markup.Render(ses.Name()))
|
||||||
r.icon.Icon.SetPlaceholderIcon(IconName, IconSize)
|
|
||||||
r.avatar.SetText(ses.Name().Content)
|
r.avatar.SetText(ses.Name().Content)
|
||||||
|
|
||||||
|
if r.icon == nil {
|
||||||
|
r.icon = newIcon(r.avatar)
|
||||||
|
r.iconBox.Add(r.icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.icon.SetPlaceholderIcon(IconName, IconSize)
|
||||||
|
|
||||||
// If the session has an icon, then use it.
|
// If the session has an icon, then use it.
|
||||||
if iconer := ses.AsIconer(); iconer != nil {
|
if iconer := ses.AsIconer(); iconer != nil {
|
||||||
r.icon.Icon.AsyncSetIconer(iconer, "failed to set session icon")
|
r.icon.AsyncSetIconer(iconer, "failed to set session icon")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update to indicate that we're done.
|
// Update to indicate that we're done.
|
||||||
primitives.RemoveChildren(r)
|
primitives.RemoveChildren(r)
|
||||||
r.SetSensitive(true)
|
r.SetSensitive(true)
|
||||||
r.Add(r.icon)
|
r.Add(r.iconBox)
|
||||||
|
|
||||||
// Bind extra menu items before loading. These items won't be clickable
|
// Bind extra menu items before loading. These items won't be clickable
|
||||||
// during loading.
|
// during loading.
|
||||||
|
|
Loading…
Reference in a new issue