cchat-gtk/internal/ui/service/session/servers.go

269 lines
6.4 KiB
Go

package session
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/humanize"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/spinner"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/serverpane"
"github.com/diamondburned/handy"
"github.com/gotk3/gotk3/gtk"
)
const FaceSize = 48 // gtk.ICON_SIZE_DIALOG
const ListWidth = 200
// SessionController extends server.Controller to add needed methods that the
// specific top-level servers container needs.
type SessionController interface {
ClearMessenger()
MessengerSelected(*server.ServerRow)
}
// Servers wraps around a list of servers inherited from Children to display a
// Lister in its own box instead of as a nested list. It's the container that's
// displayed on the right of the service sidebar.
type Servers struct {
gtk.Stack
SessionController
spinner *spinner.Boxed
// Main is the horizontal box containing the current struct's list of
// servers columnated with the same level. The second item in the box should
// be the selected server.
Main *serverpane.Paned
// Lister is the current lister belonging to this server.
Lister cchat.Lister
stopLs func()
// Children is main's lhs.
Children *server.Children
// NextColumn is main's rhs.
NextColumn *Servers // nil
detachNext func()
}
var toplevelCSS = primitives.PrepareClassCSS("top-level", `
.top-level {
}
`)
// NewServers creates a new Servers instance that holds only the given column
// number and its children. Any servers with a different columnate ID will be in
// the children pane.
func NewServers(p traverse.Breadcrumber, ctrl SessionController) *Servers {
servers := Servers{
SessionController: ctrl,
}
servers.Children = server.NewChildren(p, &servers)
servers.Children.SetVExpand(true)
servers.Children.Show()
toplevelCSS(servers.Children)
servers.Main = serverpane.NewPaned(servers.Children, gtk.ORIENTATION_VERTICAL)
servers.Main.Show()
stack, _ := gtk.StackNew()
servers.Stack = *stack
servers.Stack.SetVAlign(gtk.ALIGN_START)
servers.Stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
servers.Stack.SetTransitionDuration(75)
servers.Stack.AddNamed(servers.Main, "main")
servers.Stack.Show()
return &servers
}
// Destroy destroys and invalidates this instance permanently.
func (s *Servers) Destroy() {
s.Reset()
s.Stack.Destroy()
}
func (s *Servers) Reset() {
// Reset isn't necessarily called while loading, so we do a check.
if s.spinner != nil {
s.spinner.Destroy()
s.spinner = nil
}
// Close the right server column if any.
if s.NextColumn != nil {
if s.detachNext != nil {
s.detachNext()
}
s.Main.Remove(s.NextColumn)
s.NextColumn.Destroy()
s.NextColumn = nil
}
// Call the destructor if any.
if s.stopLs != nil {
s.stopLs()
s.stopLs = nil
}
// Reset the state.
s.Lister = nil
// Reset the children container.
s.Children.Reset()
s.Stack.SetVisibleChild(s.Main)
}
// IsLoading returns true if the servers container is loading.
func (s *Servers) IsLoading() bool {
return s.spinner != nil
}
// SetList indicates that the server list has been loaded. Unlike
// server.Children, this method will load immediately.
func (s *Servers) SetList(slist cchat.Lister) {
if s.stopLs != nil {
s.stopLs()
s.stopLs = nil
}
s.Lister = slist
s.load()
}
func (s *Servers) load() {
if s.Lister == nil || s.IsLoading() {
return
}
// Mark the servers list as loading.
s.setLoading()
lister := s.Lister
go func() {
stop, err := lister.Servers(s)
gts.ExecAsync(func() {
if err != nil {
s.setFailed(err)
} else {
s.stopLs = stop
s.setDone()
}
})
}()
}
// SetServers is reserved for cchat.ServersContainer.
func (s *Servers) SetServers(servers []cchat.Server) {
gts.ExecAsync(func() {
s.Children.SetServersUnsafe(servers)
if servers == nil {
s.ClearMessenger()
return
}
// Reload all top-level nodes.
s.Children.LoadAll()
})
}
// SetServers is reserved for cchat.ServersContainer.
func (s *Servers) UpdateServer(update cchat.ServerUpdate) {
gts.ExecAsync(func() { s.Children.UpdateServerUnsafe(update) })
}
// setDone changes the view to show the servers.
func (s *Servers) setDone() {
s.SetVisibleChild(s.Main)
// stop the spinner.
if s.spinner != nil {
s.spinner.Destroy()
s.spinner = nil
}
}
// setLoading shows a loading spinner. Use this after the session row is
// connected.
func (s *Servers) setLoading() {
s.spinner = spinner.New()
s.spinner.SetSizeRequest(FaceSize, FaceSize)
s.spinner.Show()
s.spinner.Start()
s.AddNamed(s.spinner, "spinner")
s.SetVisibleChildName("spinner")
}
// setFailed shows a sad face with the error. Use this when the session row has
// failed to load.
func (s *Servers) setFailed(err error) {
// stop the spinner. Let this SEGFAULT if nil.
s.spinner.Destroy()
s.spinner = nil
// Remove existing error widgets.
w, err := s.Stack.GetChildByName("error")
if err == nil {
s.Stack.Remove(w)
}
// Create a retry button.
btn, _ := gtk.ButtonNewFromIconName("view-refresh-symbolic", gtk.ICON_SIZE_BUTTON)
btn.SetLabel("Retry")
btn.Connect("clicked", s.load)
btn.Show()
page := handy.StatusPageNew()
page.SetTitle("Error")
page.SetIconName("dialog-error")
page.SetTooltipText(err.Error())
page.SetDescription(humanize.Error(err))
page.Add(btn)
s.Stack.AddNamed(page, "error")
s.Stack.SetVisibleChildName("error")
}
// SelectColumnatedLister is called by children servers to open up a server list
// on the right.
func (s *Servers) SelectColumnatedLister(srv *server.ServerRow, lst cchat.Lister) {
if s.detachNext != nil {
s.Main.Remove(s.NextColumn) // run the deconstructor
s.detachNext()
s.NextColumn.Destroy()
}
if lst == nil {
return
}
s.NextColumn = NewServers(srv, s)
s.NextColumn.SetList(lst)
s.Main.AddSide(s.NextColumn)
update := func(box *gtk.Box) {
a := box.GetAllocation()
// Align the next column to the selected item.
s.NextColumn.SetMarginTop(a.GetY())
}
s.Children.SetExpand(false)
primitives.AddClass(srv, "active-column")
update(srv.Box)
sizeHandle := srv.Box.Connect("size-allocate", update)
s.detachNext = func() {
srv.Box.HandlerDisconnect(sizeHandle)
s.Children.SetExpand(true)
primitives.RemoveClass(srv, "active-column")
}
}