From 0d8d3609be9cea7bd8ce4f7024198ccd7731b1ec Mon Sep 17 00:00:00 2001 From: "diamondburned (Forefront)" Date: Sun, 14 Jun 2020 11:19:06 -0700 Subject: [PATCH] Minor graphical tweaks, added Disconnection --- go.mod | 4 +- go.sum | 4 + internal/keyring/keyring.go | 12 +- internal/ui/messages/sadface/sadface.go | 8 +- internal/ui/primitives/primitives.go | 26 ++- internal/ui/service/header.go | 2 +- internal/ui/service/service.go | 30 +++- internal/ui/service/session/server/server.go | 46 ++++-- internal/ui/service/session/session.go | 165 ++++++++++++++----- internal/ui/ui.go | 6 + 10 files changed, 230 insertions(+), 73 deletions(-) diff --git a/go.mod b/go.mod index 33f3911..d4b7011 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200612 require ( github.com/Xuanwo/go-locale v0.2.0 - github.com/diamondburned/cchat v0.0.25 - github.com/diamondburned/cchat-mock v0.0.0-20200613003444-b36f8f47debe + github.com/diamondburned/cchat v0.0.26 + github.com/diamondburned/cchat-mock v0.0.0-20200613233949-1e7651c8dd84 github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64 github.com/goodsign/monday v1.0.0 github.com/google/btree v1.0.0 // indirect diff --git a/go.sum b/go.sum index d1a4ffb..3068904 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/diamondburned/cchat v0.0.25 h1:+kf2gQu5TQs1vD/gCaVlzKu5vOqZz/1Qw87xHdeFYj4= github.com/diamondburned/cchat v0.0.25/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= +github.com/diamondburned/cchat v0.0.26 h1:QBt4d65uzUPJz3jF8b2pJ09Jz8LeBRyG2ol47FOy0g0= +github.com/diamondburned/cchat v0.0.26/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= github.com/diamondburned/cchat-mock v0.0.0-20200613003444-b36f8f47debe h1:OoTLxpryxB9iQyu3bjw5N9N/3Bvu6FwklJ85X9erCAY= github.com/diamondburned/cchat-mock v0.0.0-20200613003444-b36f8f47debe/go.mod h1:vitBma+rd/ah+ujQsp6lPm/AfS2KtLKEh+Owxbv5BQM= +github.com/diamondburned/cchat-mock v0.0.0-20200613233949-1e7651c8dd84 h1:NSuksZ9HiLiau93qAz4yNba6Xd7ExOFc956dumONDQ0= +github.com/diamondburned/cchat-mock v0.0.0-20200613233949-1e7651c8dd84/go.mod h1:JxTay4MVEqmDisGqDGk8TG0UnKX7wDEImFywyoPfGjk= github.com/diamondburned/gotk3 v0.0.0-20200612012846-9df87fea4f6d h1:NFTuwBU+CNZDB1iaGC3gDuBRf9FTd1h2WnIh6NF7elg= github.com/diamondburned/gotk3 v0.0.0-20200612012846-9df87fea4f6d/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64 h1:/ykUYHuYyj+NN/aaqe6lfaCZQc3EMZs93wAGVJTh5j0= diff --git a/internal/keyring/keyring.go b/internal/keyring/keyring.go index f9d5ee6..fd6ebab 100644 --- a/internal/keyring/keyring.go +++ b/internal/keyring/keyring.go @@ -39,7 +39,7 @@ type Session struct { Data map[string]string } -func GetSession(ses cchat.Session, name string) *Session { +func ConvertSession(ses cchat.Session, name string) *Session { saver, ok := ses.(cchat.SessionSaver) if !ok { return nil @@ -79,3 +79,13 @@ func RestoreSessions(serviceName text.Rich) (sessions []Session) { } return } + +func RestoreSession(serviceName text.Rich, id string) *Session { + var sessions = RestoreSessions(serviceName) + for _, session := range sessions { + if session.ID == id { + return &session + } + } + return nil +} diff --git a/internal/ui/messages/sadface/sadface.go b/internal/ui/messages/sadface/sadface.go index 37eda6d..2a6eb98 100644 --- a/internal/ui/messages/sadface/sadface.go +++ b/internal/ui/messages/sadface/sadface.go @@ -50,15 +50,15 @@ func New(parent gtk.IWidget, placeholder WidgetUnreferencer) *FaceView { // Reset brings the view to an empty box. func (v *FaceView) Reset() { + v.Stack.SetVisibleChildName("empty") v.ensurePlaceholderDestroyed() v.Loading.Spinner.Stop() - v.Stack.SetVisibleChildName("empty") } func (v *FaceView) SetMain() { + v.Stack.SetVisibleChildName("main") v.ensurePlaceholderDestroyed() v.Loading.Spinner.Stop() - v.Stack.SetVisibleChildName("main") } func (v *FaceView) SetLoading() { @@ -68,10 +68,10 @@ func (v *FaceView) SetLoading() { } func (v *FaceView) SetError(err error) { + v.Face.SetError(err) + v.Stack.SetVisibleChildName("face") v.ensurePlaceholderDestroyed() v.Loading.Spinner.Stop() - v.Stack.SetVisibleChildName("face") - v.Face.SetError(err) } func (v *FaceView) ensurePlaceholderDestroyed() { diff --git a/internal/ui/primitives/primitives.go b/internal/ui/primitives/primitives.go index 34eda05..66c5700 100644 --- a/internal/ui/primitives/primitives.go +++ b/internal/ui/primitives/primitives.go @@ -106,7 +106,13 @@ func SetImageIcon(img *gtk.Image, icon string, sizepx int) { img.SetSizeRequest(sizepx, sizepx) } -func AppendMenuItems(menu interface{ Append(gtk.IMenuItem) }, items []*gtk.MenuItem) { +func PrependMenuItems(menu interface{ Prepend(gtk.IMenuItem) }, items []gtk.IMenuItem) { + for i := len(items) - 1; i >= 0; i-- { + menu.Prepend(items[i]) + } +} + +func AppendMenuItems(menu interface{ Append(gtk.IMenuItem) }, items []gtk.IMenuItem) { for _, item := range items { menu.Append(item) } @@ -118,6 +124,12 @@ func HiddenMenuItem(label string, fn interface{}) *gtk.MenuItem { return mb } +func HiddenDisabledMenuItem(label string, fn interface{}) *gtk.MenuItem { + mb := HiddenMenuItem(label, fn) + mb.SetSensitive(false) + return mb +} + func MenuItem(label string, fn interface{}) *gtk.MenuItem { menuitem := HiddenMenuItem(label, fn) menuitem.Show() @@ -128,7 +140,7 @@ type Connector interface { Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error) } -func BindMenu(menu *gtk.Menu, connector Connector) { +func BindMenu(connector Connector, menu *gtk.Menu) { connector.Connect("event", func(_ *gtk.ToggleButton, ev *gdk.Event) { if gts.EventIsRightClick(ev) { menu.PopupAtPointer(ev) @@ -136,6 +148,16 @@ func BindMenu(menu *gtk.Menu, connector Connector) { }) } +func BindDynamicMenu(connector Connector, constr func(menu *gtk.Menu)) { + connector.Connect("event", func(_ *gtk.ToggleButton, ev *gdk.Event) { + if gts.EventIsRightClick(ev) { + menu, _ := gtk.MenuNew() + constr(menu) + menu.PopupAtPointer(ev) + } + }) +} + func NewTargetEntry(target string) gtk.TargetEntry { e, _ := gtk.TargetEntryNew(target, gtk.TARGET_SAME_APP, 0) return *e diff --git a/internal/ui/service/header.go b/internal/ui/service/header.go index 3245626..ea92306 100644 --- a/internal/ui/service/header.go +++ b/internal/ui/service/header.go @@ -47,7 +47,7 @@ func newHeader(svc cchat.Service) *header { // Spawn the menu on right click. menu, _ := gtk.MenuNew() - primitives.BindMenu(menu, reveal) + primitives.BindMenu(reveal, menu) return &header{box, reveal, add, menu} } diff --git a/internal/ui/service/service.go b/internal/ui/service/service.go index 36687f7..34707d3 100644 --- a/internal/ui/service/service.go +++ b/internal/ui/service/service.go @@ -1,6 +1,8 @@ package service import ( + "fmt" + "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/keyring" @@ -55,6 +57,8 @@ type Controller interface { // OnSessionRemove is called to remove a session. This should also clear out // the message view in the parent package. OnSessionRemove(id string) + // OnSessionDisconnect is here to satisfy session's controller. + OnSessionDisconnect(id string) } // Container represents a single service, including the button header and the @@ -114,7 +118,7 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container { }) // Make menu items. - primitives.AppendMenuItems(header.Menu, []*gtk.MenuItem{ + primitives.AppendMenuItems(header.Menu, []gtk.IMenuItem{ primitives.MenuItem("Save Sessions", func() { container.SaveAllSessions() }), @@ -131,7 +135,7 @@ func (c *Container) AddSession(ses cchat.Session) *session.Row { } func (c *Container) AddLoadingSession(id, name string) *session.Row { - srow := session.NewLoading(c, name, c) + srow := session.NewLoading(c, id, name, c) c.children.AddSessionRow(id, srow) return srow } @@ -149,15 +153,31 @@ func (c *Container) MoveSession(rowID, beneathRowID string) { c.SaveAllSessions() } +func (c *Container) OnSessionDisconnect(ses *session.Row) { + c.Controller.OnSessionDisconnect(ses.ID()) +} + // RestoreSession tries to restore sessions asynchronously. This satisfies // session.Controller. -func (c *Container) RestoreSession(row *session.Row, krs keyring.Session) { +func (c *Container) RestoreSession(row *session.Row, id string) { // Can this session be restored? If not, exit. restorer, ok := c.Service.(cchat.SessionRestorer) if !ok { return } - c.restoreSession(row, restorer, krs) + + // Do we even have a session stored? + krs := keyring.RestoreSession(c.Service.Name(), id) + if krs == nil { + log.Error(fmt.Errorf( + "Missing keyring for service %s, session ID %s", + c.Service.Name().Content, id, + )) + + return + } + + c.restoreSession(row, restorer, *krs) } // internal method called on AddService. @@ -186,7 +206,7 @@ func (c *Container) restoreSession(r *session.Row, res cchat.SessionRestorer, k err = errors.Wrapf(err, "Failed to restore session %s (%s)", k.ID, k.Name) log.Error(err) - gts.ExecAsync(func() { r.SetFailed(k, err) }) + gts.ExecAsync(func() { r.SetFailed(err) }) } else { gts.ExecAsync(func() { r.SetSession(s) }) } diff --git a/internal/ui/service/session/server/server.go b/internal/ui/service/session/server/server.go index 9d31c6f..f2d6e64 100644 --- a/internal/ui/service/session/server/server.go +++ b/internal/ui/service/session/server/server.go @@ -115,21 +115,17 @@ func (r *Row) Breadcrumb() breadcrumb.Breadcrumb { type Children struct { *gtk.Revealer Main *gtk.Box - load *loading.Button // nil after init List cchat.ServerList rowctrl Controller + load *loading.Button // nil after init Rows []*Row Parent breadcrumb.Breadcrumber } func NewChildren(parent breadcrumb.Breadcrumber, ctrl Controller) *Children { - load := loading.NewButton() - load.Show() - main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) - main.Add(load) main.SetMarginStart(ChildrenMargin) main.Show() @@ -141,12 +137,38 @@ func NewChildren(parent breadcrumb.Breadcrumber, ctrl Controller) *Children { return &Children{ Revealer: rev, Main: main, - load: load, rowctrl: ctrl, Parent: parent, } } +func (c *Children) SetLoading() { + // If we're already loading, then exit. + if c.load != nil { + return + } + + c.load = loading.NewButton() + c.load.Show() + c.Main.Add(c.load) +} + +func (c *Children) Reset() { + // Do we have the spinning circle button? If yes, remove it. + if c.load != nil { + c.Main.Remove(c.load) + c.load = nil + } + + // Remove old servers from the list. + for _, row := range c.Rows { + c.Main.Remove(row) + } + + // Wipe the list empty. + c.Rows = nil +} + func (c *Children) SetServerList(list cchat.ServerList) { c.List = list @@ -159,12 +181,6 @@ func (c *Children) SetServerList(list cchat.ServerList) { func (c *Children) SetServers(servers []cchat.Server) { gts.ExecAsync(func() { - // Do we have the spinning circle button? If yes, remove it. - if c.load != nil { - c.Main.Remove(c.load) - c.load = nil - } - // Save the current state. var oldID string for _, row := range c.Rows { @@ -174,10 +190,8 @@ func (c *Children) SetServers(servers []cchat.Server) { } } - // Update the server list. - for _, row := range c.Rows { - c.Main.Remove(row) - } + // Reset before inserting new servers. + c.Reset() c.Rows = make([]*Row, len(servers)) diff --git a/internal/ui/service/session/session.go b/internal/ui/service/session/session.go index 0c0536d..ea6722f 100644 --- a/internal/ui/service/session/session.go +++ b/internal/ui/service/session/session.go @@ -2,7 +2,9 @@ package session import ( "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/keyring" + "github.com/diamondburned/cchat-gtk/internal/log" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/rich" "github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb" @@ -11,15 +13,27 @@ import ( "github.com/diamondburned/imgutil" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gtk" + "github.com/pkg/errors" ) const IconSize = 32 // Controller extends server.RowController to add session. type Controller interface { + // OnSessionDisconnect is called before a session is disconnected. This + // function is used for cleanups. + OnSessionDisconnect(*Row) + // MessageRowSelected is called when a server that can display messages (aka + // implements ServerMessage) is called. MessageRowSelected(*Row, *server.Row, cchat.ServerMessage) - RestoreSession(*Row, keyring.Session) // async + // RestoreSession is called with the session ID to ask the controller to + // restore it from keyring information. + RestoreSession(*Row, string) // ID string, async + // RemoveSession is called to ask the controller to remove the session from + // the list of sessions. RemoveSession(*Row) + // MoveSession is called to ask the controller to move the session to + // somewhere else in the list of sessions. MoveSession(id, movingID string) } @@ -31,24 +45,24 @@ type Row struct { Session cchat.Session Servers *server.Children - menu *gtk.Menu - retry *gtk.MenuItem - - ctrl Controller - parent breadcrumb.Breadcrumber + ctrl Controller + parent breadcrumb.Breadcrumber + menuconstr func(*gtk.Menu) + sessionID string // used for reconnection // nil after calling SetSession() - krs keyring.Session + // krs keyring.Session } func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Row { - row := new(parent, ctrl) + row := newRow(parent, ctrl) row.SetSession(ses) return row } -func NewLoading(parent breadcrumb.Breadcrumber, name string, ctrl Controller) *Row { - row := new(parent, ctrl) +func NewLoading(parent breadcrumb.Breadcrumber, id, name string, ctrl Controller) *Row { + row := newRow(parent, ctrl) + row.sessionID = id row.Button.SetLabelUnsafe(text.Rich{Content: name}) row.setLoading() @@ -60,12 +74,13 @@ var dragEntries = []gtk.TargetEntry{ } var dragAtom = gdk.GdkAtomIntern("GTK_TOGGLE_BUTTON", true) -func new(parent breadcrumb.Breadcrumber, ctrl Controller) *Row { +func newRow(parent breadcrumb.Breadcrumber, ctrl Controller) *Row { row := &Row{ ctrl: ctrl, parent: parent, } row.Servers = server.NewChildren(parent, row) + row.Servers.SetLoading() row.Button = rich.NewToggleButtonImage(text.Rich{}) row.Button.Box.SetHAlign(gtk.ALIGN_START) @@ -86,33 +101,86 @@ func new(parent breadcrumb.Breadcrumber, ctrl Controller) *Row { row.Box.PackStart(row.Button, false, false, 0) row.Box.Show() + // Bind the box to .session in CSS. primitives.AddClass(row.Box, "session") - - row.menu, _ = gtk.MenuNew() - primitives.BindMenu(row.menu, row.Button) - - row.retry = primitives.HiddenMenuItem("Retry", func() { - // Show the loading stuff. - row.setLoading() - // Reuse the failed keyring session provided. As this variable is reset - // after a success, it relies of the button not triggering. - ctrl.RestoreSession(row, row.krs) + // Bind the button to create a new menu. + primitives.BindDynamicMenu(row.Button, func(menu *gtk.Menu) { + row.menuconstr(menu) }) - row.retry.SetSensitive(false) - primitives.AppendMenuItems(row.menu, []*gtk.MenuItem{ - row.retry, - primitives.MenuItem("Remove", func() { - ctrl.RemoveSession(row) - }), - }) + // noop, empty menu + row.menuconstr = func(menu *gtk.Menu) {} return row } +// RemoveSession removes itself from the session list. +func (r *Row) RemoveSession() { + // Remove the session off the list. + r.ctrl.RemoveSession(r) + + // Asynchrously disconnect. + go func() { + if err := r.Session.Disconnect(); err != nil { + log.Error(errors.Wrap(err, "Non-fatal, failed to disconnect removed session")) + } + }() +} + +// ReconnectSession tries to reconnect with the keyring data. This is a slow +// method but it's also a very cold path. +func (r *Row) ReconnectSession() { + // If we haven't ever connected: + if r.sessionID == "" { + return + } + + r.setLoading() + r.ctrl.RestoreSession(r, r.sessionID) +} + +// DisconnectSession disconnects the current session. +func (r *Row) DisconnectSession() { + // Call the disconnect function from the controller first. + r.ctrl.OnSessionDisconnect(r) + + // Show visually that we're disconnected first by wiping all servers. + r.Box.Remove(r.Servers) + r.Servers.Reset() + + // Set the offline icon to the button. + r.Button.Image.SetPlaceholderIcon("user-invisible-symbolic", IconSize) + // Also unselect the button. + r.Button.SetActive(false) + + // Disable the button because we're busy disconnecting. We'll re-enable them + // once we're done reconnecting. + r.SetSensitive(false) + + // Try and disconnect asynchronously. + gts.Async(func() (func(), error) { + // Disconnect and wrap the error if any. Wrap works with a nil error. + err := errors.Wrap(r.Session.Disconnect(), "Failed to disconnect.") + return func() { + // allow access to the menu + r.SetSensitive(true) + + // set the menu to allow disconnection. + r.menuconstr = func(menu *gtk.Menu) { + primitives.AppendMenuItems(menu, []gtk.IMenuItem{ + primitives.MenuItem("Connect", r.ReconnectSession), + primitives.MenuItem("Remove", r.RemoveSession), + }) + } + }, err + }) +} + func (r *Row) setLoading() { // set the loading icon r.Button.Image.SetPlaceholderIcon("content-loading-symbolic", IconSize) + // set the loading icon in the servers list + r.Servers.SetLoading() // restore the old label's color r.Button.SetLabelUnsafe(r.Button.GetLabel()) // clear the tooltip @@ -124,19 +192,24 @@ func (r *Row) setLoading() { // KeyringSession returns a keyring session, or nil if the session cannot be // saved. func (r *Row) KeyringSession() *keyring.Session { - return keyring.GetSession(r.Session, r.Button.GetText()) + return keyring.ConvertSession(r.Session, r.Button.GetText()) +} + +// ID returns the session ID. +func (r *Row) ID() string { + return r.sessionID } func (r *Row) SetSession(ses cchat.Session) { - // Disable the retry button. - r.retry.SetSensitive(false) - r.retry.Hide() - r.Session = ses + r.sessionID = ses.ID() + r.Servers.SetServerList(ses) + r.Box.PackStart(r.Servers, false, false, 0) + r.Button.SetLabelUnsafe(ses.Name()) r.Button.Image.SetPlaceholderIcon("user-available-symbolic", IconSize) - r.Box.PackStart(r.Servers, false, false, 0) + r.SetSensitive(true) r.SetTooltipText("") // reset @@ -145,22 +218,30 @@ func (r *Row) SetSession(ses cchat.Session) { r.Button.Image.AsyncSetIcon(iconer.Icon, "Error fetching session icon URL") } - // Wipe the keyring session off. - r.krs = keyring.Session{} + // Set the menu with the disconnect button. + r.menuconstr = func(menu *gtk.Menu) { + primitives.AppendMenuItems(menu, []gtk.IMenuItem{ + primitives.MenuItem("Disconnect", r.DisconnectSession), + primitives.MenuItem("Remove", r.RemoveSession), + }) + } } -func (r *Row) SetFailed(krs keyring.Session, err error) { - // Set the failed keyring session. - r.krs = krs - +func (r *Row) SetFailed(err error) { // Allow the retry button to be pressed. - r.retry.SetSensitive(true) - r.retry.Show() + r.menuconstr = func(menu *gtk.Menu) { + primitives.AppendMenuItems(menu, []gtk.IMenuItem{ + primitives.MenuItem("Retry", r.ReconnectSession), + primitives.MenuItem("Remove", r.RemoveSession), + }) + } r.SetSensitive(true) r.SetTooltipText(err.Error()) // Intentional side-effect of not changing the actual label state. r.Button.Label.SetMarkup(rich.MakeRed(r.Button.GetLabel())) + // Set the icon to a failed one. + r.Button.Image.SetPlaceholderIcon("computer-fail-symbolic", IconSize) } func (r *Row) MessageRowSelected(server *server.Row, smsg cchat.ServerMessage) { diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 20bf073..396d2fb 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -79,6 +79,12 @@ func (app *App) OnSessionRemove(id string) { } } +func (app *App) OnSessionDisconnect(id string) { + // We're basically doing the same thing as removing a session. Check + // OnSessionRemove above. + app.OnSessionRemove(id) +} + func (app *App) MessageRowSelected(ses *session.Row, srv *server.Row, smsg cchat.ServerMessage) { // Is there an old row that we should deactivate? if app.lastDeactivator != nil {