From 89c2fd34568fce926b003279f0c745d79787f567 Mon Sep 17 00:00:00 2001 From: diamondburned Date: Wed, 21 Oct 2020 22:23:01 -0700 Subject: [PATCH] workaround for member popovers disappearing --- internal/ui/messages/memberlist/eventqueue.go | 71 +++++++++++++++++++ internal/ui/messages/memberlist/memberlist.go | 30 +++++--- 2 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 internal/ui/messages/memberlist/eventqueue.go diff --git a/internal/ui/messages/memberlist/eventqueue.go b/internal/ui/messages/memberlist/eventqueue.go new file mode 100644 index 0000000..649d30f --- /dev/null +++ b/internal/ui/messages/memberlist/eventqueue.go @@ -0,0 +1,71 @@ +package memberlist + +import ( + "sync" + + "github.com/diamondburned/cchat-gtk/internal/gts" +) + +type EventQueuer interface { + Activate() + Deactivate() +} + +// eventQueue is a rough unbounded event queue. A zero-value instance is valid. +type eventQueue struct { + mutex sync.Mutex + activated bool + // idleQueue contains the incoming callbacks to update events. This is a + // temporary hack to the issue of popovers disappearing when its parent + // widget, that is the list box, changes. This slice should then contain all + // those events to be executed only when the Popover is popped down. + idleQueue []func() +} + +func (evq *eventQueue) Add(fn func()) { + evq.mutex.Lock() + defer evq.mutex.Unlock() + + if evq.activated { + evq.idleQueue = append(evq.idleQueue, fn) + } else { + gts.ExecAsync(fn) + } +} + +func (evq *eventQueue) Activate() { + evq.mutex.Lock() + defer evq.mutex.Unlock() + + evq.activated = true +} + +func (evq *eventQueue) pop() []func() { + evq.mutex.Lock() + defer evq.mutex.Unlock() + + popped := evq.idleQueue + evq.idleQueue = nil + evq.activated = false + + return popped +} + +func (evq *eventQueue) Deactivate() { + var popped = evq.pop() + + // We shouldn't try and run more than a certain amount of callbacks within a + // single loop, as it will freeze up the UI. + if len(popped) > 25 { + for _, fn := range popped { + gts.ExecAsync(fn) + } + return + } + + gts.ExecAsync(func() { + for _, fn := range popped { + fn() + } + }) +} diff --git a/internal/ui/messages/memberlist/memberlist.go b/internal/ui/messages/memberlist/memberlist.go index a7849a3..492b205 100644 --- a/internal/ui/messages/memberlist/memberlist.go +++ b/internal/ui/messages/memberlist/memberlist.go @@ -36,6 +36,8 @@ type Container struct { // map id -> *Section Sections map[string]*Section stop func() + + eventQueue eventQueue } var memberListCSS = primitives.PrepareClassCSS("member-list", ` @@ -110,15 +112,20 @@ func (c *Container) TryAsyncList(server cchat.Messenger) { } func (c *Container) SetSections(sections []cchat.MemberSection) { - gts.ExecAsync(func() { c.SetSectionsUnsafe(sections) }) + c.eventQueue.Add(func() { c.SetSectionsUnsafe(sections) }) } func (c *Container) SetMember(sectionID string, member cchat.ListMember) { - gts.ExecAsync(func() { c.SetMemberUnsafe(sectionID, member) }) + c.eventQueue.Add(func() { c.SetMemberUnsafe(sectionID, member) }) } func (c *Container) RemoveMember(sectionID string, id string) { - gts.ExecAsync(func() { c.RemoveMemberUnsafe(sectionID, id) }) + c.eventQueue.Add(func() { c.RemoveMemberUnsafe(sectionID, id) }) +} + +type sectionInsert struct { + front *Section + sections []*Section } func (c *Container) SetSectionsUnsafe(sections []cchat.MemberSection) { @@ -127,7 +134,7 @@ func (c *Container) SetSectionsUnsafe(sections []cchat.MemberSection) { for i, section := range sections { sc, ok := c.Sections[section.ID()] if !ok { - sc = NewSection(section) + sc = NewSection(section, &c.eventQueue) } else { sc.Update(section.Name(), section.Total()) } @@ -191,7 +198,7 @@ var sectionBodyCSS = primitives.PrepareClassCSS("section-body", ` } `) -func NewSection(sect cchat.MemberSection) *Section { +func NewSection(sect cchat.MemberSection, evq EventQueuer) *Section { header := rich.NewLabel(text.Rich{}) header.Show() sectionHeaderCSS(header) @@ -216,8 +223,7 @@ func NewSection(sect cchat.MemberSection) *Section { // Cold path; we can afford searching in the map. for _, member := range members { if member.ListBoxRow.GetIndex() == i { - member.Popup() - return + member.Popup(evq) } } }) @@ -291,6 +297,8 @@ type Member struct { Avatar *rich.Icon Name *gtk.Label output markup.RenderOutput + + parent *gtk.ListBox } const AvatarSize = 34 @@ -382,12 +390,18 @@ func (m *Member) Update(member cchat.ListMember) { } // Popup pops up the mention popover if any. -func (m *Member) Popup() { +func (m *Member) Popup(evq EventQueuer) { if len(m.output.Mentions) > 0 { p := labeluri.NewPopoverMentioner(m, m.output.Input, m.output.Mentions[0]) p.Ref() // prevent the popover from closing itself p.SetPosition(gtk.POS_LEFT) p.Connect("closed", p.Unref) + + // Unbounded concurrency is kind of bad. We should deal with + // this in the future. + evq.Activate() + p.Connect("closed", evq.Deactivate) + p.Popup() } }