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") } }