Minor graphical tweaks, added Disconnection

This commit is contained in:
diamondburned (Forefront) 2020-06-14 11:19:06 -07:00
parent adeffc7717
commit 0d8d3609be
10 changed files with 230 additions and 73 deletions

4
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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
}

View File

@ -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() {

View File

@ -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

View File

@ -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}
}

View File

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

View File

@ -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))

View File

@ -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) {

View File

@ -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 {