488 lines
12 KiB
Go
488 lines
12 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
|
|
"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/primitives/actions"
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/savepath"
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/button"
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/commander"
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
|
"github.com/gotk3/gotk3/gdk"
|
|
"github.com/gotk3/gotk3/gtk"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const ChildrenMargin = 0 // refer to style.css
|
|
|
|
func AssertUnhollow(hollower interface{ IsHollow() bool }) {
|
|
if hollower.IsHollow() {
|
|
panic("Server is hollow, but a normal method was called.")
|
|
}
|
|
}
|
|
|
|
// ParentController controls ServerRow's container, which is the Children
|
|
// struct.
|
|
type ParentController interface {
|
|
Controller
|
|
ForceIcons()
|
|
}
|
|
|
|
type ServerRow struct {
|
|
*gtk.Box
|
|
Button *button.ToggleButton
|
|
ActionsMenu *actions.Menu
|
|
|
|
Server cchat.Server
|
|
name rich.NameContainer
|
|
ctrl ParentController
|
|
|
|
parentcrumb traverse.Breadcrumber
|
|
|
|
cmder *commander.Buffer
|
|
|
|
// non-nil if server list and the function returns error
|
|
childrenErr error
|
|
|
|
childrev *gtk.Revealer
|
|
children *Children
|
|
serverList cchat.Lister
|
|
serverStop func()
|
|
|
|
// State that's updated even when stale. Initializations will use these.
|
|
unread bool
|
|
mentioned bool
|
|
showLabel bool
|
|
|
|
UnreadIndicator cchat.UnreadIndicator
|
|
// callback to cancel unread indicator
|
|
cancelUnread func()
|
|
}
|
|
|
|
var serverCSS = primitives.PrepareClassCSS("server", `
|
|
/* Ignore first child because .server-children already covers this */
|
|
.server:not(:first-child) {
|
|
margin: 0;
|
|
border-radius: 0;
|
|
}
|
|
|
|
.server.active-column {
|
|
background-color: mix(@theme_bg_color, @theme_selected_bg_color, 0.25);
|
|
}
|
|
`)
|
|
|
|
// NewHollowServer creates a new hollow ServerRow. It will automatically create
|
|
// hollow children containers and rows for the given server.
|
|
func NewHollowServer(p traverse.Breadcrumber, sv cchat.Server, ctrl ParentController) *ServerRow {
|
|
serverRow := ServerRow{
|
|
parentcrumb: p,
|
|
ctrl: ctrl,
|
|
Server: sv,
|
|
cancelUnread: func() {},
|
|
}
|
|
|
|
serverRow.name.QueueNamer(context.Background(), sv)
|
|
|
|
lister := sv.AsLister()
|
|
messenger := sv.AsMessenger()
|
|
|
|
switch {
|
|
case lister != nil:
|
|
serverRow.SetHollowServerList(lister, ctrl)
|
|
serverRow.children.SetUnreadHandler(serverRow.SetUnreadUnsafe)
|
|
|
|
case messenger != nil:
|
|
serverRow.UnreadIndicator = messenger.AsUnreadIndicator()
|
|
if serverRow.UnreadIndicator != nil {
|
|
gts.Async(func() (func(), error) {
|
|
c, err := serverRow.UnreadIndicator.UnreadIndicate(&serverRow)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Failed to use unread indicator")
|
|
}
|
|
|
|
return func() { serverRow.cancelUnread = c }, nil
|
|
})
|
|
}
|
|
}
|
|
|
|
return &serverRow
|
|
}
|
|
|
|
// Init brings the row out of the hollow state. It loads the children (if any),
|
|
// but this process does not make more widgets.
|
|
func (r *ServerRow) Init() {
|
|
if !r.IsHollow() {
|
|
return
|
|
}
|
|
|
|
// Initialize the row, which would fill up the button and others as well.
|
|
|
|
r.Button = button.NewToggleButton(&r.name)
|
|
r.Button.SetShowLabel(r.showLabel)
|
|
r.Button.Show()
|
|
|
|
r.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
|
r.Box.SetHAlign(gtk.ALIGN_FILL)
|
|
r.Box.PackStart(r.Button, false, false, 0)
|
|
serverCSS(r.Box)
|
|
|
|
// Make the Actions menu.
|
|
r.ActionsMenu = actions.NewMenu("server")
|
|
r.ActionsMenu.InsertActionGroup(r)
|
|
|
|
// Ensure errors are displayed.
|
|
r.childrenSetErr(r.childrenErr)
|
|
|
|
// Connect the destroyer, if any.
|
|
r.Connect("destroy", func(interface{}) { r.cancelUnread() })
|
|
|
|
// Restore the read state.
|
|
r.Button.SetUnreadUnsafe(r.unread, r.mentioned) // update with state
|
|
|
|
if cmder := r.Server.AsCommander(); cmder != nil {
|
|
r.cmder = commander.NewBuffer(&r.name, cmder)
|
|
r.ActionsMenu.AddAction("Command Prompt", r.cmder.ShowDialog)
|
|
}
|
|
|
|
// Bind right clicks and show a popover menu on such event.
|
|
r.Button.Connect("button-press-event", func(_ gtk.IWidget, ev *gdk.Event) {
|
|
if gts.EventIsRightClick(ev) {
|
|
r.ActionsMenu.Popup(r)
|
|
}
|
|
})
|
|
|
|
// Bring up the icons of all the current level's rows if we have one.
|
|
r.name.OnUpdate(func() {
|
|
if r.name.Image().HasImage() {
|
|
r.ctrl.ForceIcons()
|
|
}
|
|
})
|
|
|
|
var (
|
|
lister = r.Server.AsLister()
|
|
columnate = lister != nil && lister.Columnate()
|
|
messenger = r.Server.AsMessenger()
|
|
)
|
|
|
|
switch {
|
|
case lister != nil && !columnate:
|
|
primitives.AddClass(r, "server-list")
|
|
r.children.Init()
|
|
r.children.Show()
|
|
|
|
r.childrev, _ = gtk.RevealerNew()
|
|
r.childrev.SetRevealChild(false)
|
|
r.childrev.Add(r.children)
|
|
r.childrev.Show()
|
|
|
|
r.Box.PackStart(r.childrev, false, false, 0)
|
|
r.Button.SetClicked(r.SetRevealChild)
|
|
|
|
case lister != nil && columnate:
|
|
primitives.AddClass(r, "server-list")
|
|
primitives.AddClass(r, "server-columnate")
|
|
r.Button.SetClicked(func(active bool) {
|
|
if active {
|
|
r.ctrl.SelectColumnatedLister(r, lister)
|
|
} else {
|
|
r.ctrl.SelectColumnatedLister(r, nil)
|
|
}
|
|
})
|
|
|
|
case messenger != nil:
|
|
primitives.AddClass(r, "server-message")
|
|
r.Button.SetClicked(func(active bool) {
|
|
if active {
|
|
r.ctrl.MessengerSelected(r)
|
|
} else {
|
|
r.ctrl.ClearMessenger()
|
|
}
|
|
})
|
|
}
|
|
|
|
// Restore the label visibility state.
|
|
r.SetShowLabel(r.showLabel)
|
|
}
|
|
|
|
// IsActiveServerMessage returns true if the row is currently selected AND it
|
|
// is a message row.
|
|
func (r *ServerRow) IsActiveServerMessage() bool {
|
|
// If the button is nil, then that probably means we're still in a hollow
|
|
// state. This obviously means nothing is being selected.
|
|
if r.Button == nil {
|
|
return false
|
|
}
|
|
|
|
return r.children == nil && r.Button.GetActive()
|
|
}
|
|
|
|
// SetUnread is thread-safe.
|
|
func (r *ServerRow) SetUnread(unread, mentioned bool) {
|
|
gts.ExecAsync(func() { r.SetUnreadUnsafe(unread, mentioned) })
|
|
}
|
|
|
|
func (r *ServerRow) SetUnreadUnsafe(unread, mentioned bool) {
|
|
// We're never unread if we're reading this current server.
|
|
if r.IsActiveServerMessage() {
|
|
unread, mentioned = false, false
|
|
}
|
|
|
|
// Update the local state.
|
|
r.unread = unread
|
|
r.mentioned = mentioned
|
|
|
|
// Button is nil if we're still in a hollow state. A nil check should tell
|
|
// us that.
|
|
if r.Button != nil {
|
|
r.Button.SetUnreadUnsafe(r.unread, r.mentioned)
|
|
}
|
|
|
|
// Still update the parent's state even if we're hollow.
|
|
traverse.TrySetUnread(r.parentcrumb, r.Server.ID(), r.unread, r.mentioned)
|
|
}
|
|
|
|
func (r *ServerRow) IsHollow() bool {
|
|
return r.Box == nil
|
|
}
|
|
|
|
// SetHollowServerList sets the row to a hollow server list (children) and
|
|
// recursively create
|
|
func (r *ServerRow) SetHollowServerList(list cchat.Lister, ctrl Controller) {
|
|
r.serverList = list
|
|
r.children = NewHollowChildren(r, ctrl)
|
|
r.load(func(error) {})
|
|
}
|
|
|
|
// load calls finish if the server list is not loaded. If it is, finish is
|
|
// called with a nil immediately.
|
|
func (r *ServerRow) load(finish func(error)) {
|
|
if r.children.Rows != nil {
|
|
finish(nil)
|
|
return
|
|
}
|
|
|
|
list := r.serverList
|
|
children := r.children
|
|
children.setLoading()
|
|
|
|
if !r.IsHollow() {
|
|
r.SetSensitive(false)
|
|
}
|
|
|
|
go func() {
|
|
stop, err := list.Servers(children)
|
|
if err != nil {
|
|
log.Error(errors.Wrap(err, "Failed to get servers"))
|
|
}
|
|
|
|
gts.ExecAsync(func() {
|
|
r.serverStop = stop
|
|
|
|
// Announce that we're not loading anymore.
|
|
r.children.setNotLoading()
|
|
|
|
if !r.IsHollow() {
|
|
// Restore clickability.
|
|
r.SetSensitive(true)
|
|
}
|
|
|
|
// Use the childrenX method instead of SetX. We can wrap nil
|
|
// errors.
|
|
r.childrenSetErr(errors.Wrap(err, "Failed to get servers"))
|
|
|
|
finish(err)
|
|
})
|
|
}()
|
|
}
|
|
|
|
// Reset clears off all children servers. It's a no-op if there are none.
|
|
func (r *ServerRow) Reset() {
|
|
if r.children != nil {
|
|
r.children.Reset()
|
|
r.children.Destroy()
|
|
}
|
|
|
|
if r.serverStop != nil {
|
|
r.serverStop()
|
|
r.serverStop = nil
|
|
}
|
|
|
|
// Reset the state.
|
|
r.ActionsMenu.Reset()
|
|
r.serverList = nil
|
|
r.children = nil
|
|
}
|
|
|
|
func (r *ServerRow) childrenSetErr(err error) {
|
|
// Update the state and only use this state field.
|
|
r.childrenErr = err
|
|
|
|
// Only call this if we're not hollow. If we are, then Init() will read the
|
|
// state field above and render the failed button.
|
|
if !r.IsHollow() {
|
|
if err != nil {
|
|
// If the user chooses to retry, the list will automatically expand.
|
|
r.SetFailed(err, func() { r.SetRevealChild(true) })
|
|
} else {
|
|
r.SetDone()
|
|
}
|
|
}
|
|
}
|
|
|
|
// UseEmptyIcon forces the row to show a placeholder icon.
|
|
func (r *ServerRow) UseEmptyIcon() {
|
|
AssertUnhollow(r)
|
|
r.Button.UseEmptyIcon()
|
|
}
|
|
|
|
// HasIcon returns true if the current row has an icon.
|
|
func (r *ServerRow) HasIcon() bool {
|
|
return !r.IsHollow() && r.Button.Image != nil
|
|
}
|
|
|
|
// SetShowLabel sets whether or not to show the button's (and its children's, if
|
|
// any)'s icons.
|
|
func (r *ServerRow) SetShowLabel(showLabel bool) {
|
|
r.showLabel = showLabel
|
|
|
|
if r.IsHollow() {
|
|
return
|
|
}
|
|
|
|
r.Button.SetShowLabel(showLabel)
|
|
|
|
// We'd want the button to be wide if we're showing the label. Otherwise,
|
|
// it can be small.
|
|
if r.Button.GetShowLabel() {
|
|
r.Box.SetHAlign(gtk.ALIGN_FILL)
|
|
} else {
|
|
r.Box.SetHAlign(gtk.ALIGN_START)
|
|
}
|
|
|
|
if r.children != nil && !r.children.IsHollow() {
|
|
r.children.SetExpand(showLabel)
|
|
}
|
|
}
|
|
|
|
func (r *ServerRow) ParentBreadcrumb() traverse.Breadcrumber {
|
|
return r.parentcrumb
|
|
}
|
|
|
|
func (r *ServerRow) Breadcrumb() string {
|
|
if r.IsHollow() {
|
|
return ""
|
|
}
|
|
|
|
return r.name.String()
|
|
}
|
|
|
|
// ID returns the server ID.
|
|
func (r *ServerRow) ID() cchat.ID {
|
|
return r.Server.ID()
|
|
}
|
|
|
|
// Name returns the name state.
|
|
func (r *ServerRow) Name() rich.LabelStateStorer {
|
|
return &r.name
|
|
}
|
|
|
|
// SetLoading is called by the parent struct.
|
|
func (r *ServerRow) SetLoading() {
|
|
AssertUnhollow(r)
|
|
|
|
r.SetSensitive(false)
|
|
r.Button.SetLoading()
|
|
}
|
|
|
|
// 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 *ServerRow) SetFailed(err error, retry func()) {
|
|
AssertUnhollow(r)
|
|
|
|
r.SetSensitive(true)
|
|
r.SetTooltipText(err.Error())
|
|
r.Button.SetFailed(err, retry)
|
|
r.ActionsMenu.Reset()
|
|
r.ActionsMenu.AddAction("Retry", retry)
|
|
}
|
|
|
|
// SetDone is shared between the parent struct and the children list. This is
|
|
// because both will use the same SetFailed.
|
|
func (r *ServerRow) SetDone() {
|
|
AssertUnhollow(r)
|
|
|
|
r.Button.SetNormal()
|
|
r.SetSensitive(true)
|
|
r.SetTooltipText("")
|
|
}
|
|
|
|
// SetSelected is used for highlighting the current message server.
|
|
func (r *ServerRow) SetSelected(selected bool) {
|
|
AssertUnhollow(r)
|
|
|
|
r.Button.SetSelected(selected)
|
|
}
|
|
|
|
func (r *ServerRow) GetActive() bool {
|
|
if !r.IsHollow() {
|
|
return r.Button.GetActive()
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// SetRevealChild reveals the list of servers. It does nothing if there are no
|
|
// servers, meaning if Row does not represent a ServerList.
|
|
func (r *ServerRow) SetRevealChild(reveal bool) {
|
|
AssertUnhollow(r)
|
|
|
|
// Do the above noop check.
|
|
if r.children == nil {
|
|
return
|
|
}
|
|
|
|
// Actually reveal the children.
|
|
r.childrev.SetRevealChild(reveal)
|
|
|
|
// Save the path.
|
|
savepath.Update(r, reveal)
|
|
|
|
if reveal {
|
|
primitives.AddClass(r, "expanded")
|
|
} else {
|
|
primitives.RemoveClass(r, "expanded")
|
|
}
|
|
|
|
// If this isn't a reveal, then we don't need to load.
|
|
if !reveal {
|
|
return
|
|
}
|
|
|
|
// Ensure that we have successfully loaded the server.
|
|
r.load(func(err error) {
|
|
if err == nil {
|
|
// Load the list of servers if we're still in loading mode. Before,
|
|
// we have to call Servers on this. Now, we already know that there
|
|
// are hollow servers in the children container.
|
|
r.children.LoadAll()
|
|
}
|
|
})
|
|
}
|
|
|
|
// GetRevealChild returns whether or not the server list is expanded, or always
|
|
// false if there is no server list.
|
|
func (r *ServerRow) GetRevealChild() bool {
|
|
AssertUnhollow(r)
|
|
|
|
if r.childrev != nil {
|
|
return r.childrev.GetRevealChild()
|
|
}
|
|
return false
|
|
}
|