From af3f3ec178dd53e1592756352b7ae9215dc9b41e Mon Sep 17 00:00:00 2001 From: diamondburned Date: Thu, 16 Jul 2020 17:21:14 -0700 Subject: [PATCH] Added drag-and-drop; minor tweaks and bug fixes --- go.mod | 4 +- go.sum | 4 + icons/icons.go | 31 +-- internal/gts/css.go | 17 -- internal/gts/gts.go | 52 +++- internal/ui/header.go | 66 ++++- internal/ui/messages/container/cozy/cozy.go | 2 +- .../messages/container/cozy/message_full.go | 35 ++- internal/ui/messages/message/message.go | 4 +- internal/ui/messages/view.go | 2 +- internal/ui/primitives/drag/drag.go | 116 +++++++++ internal/ui/primitives/primitives.go | 50 +--- .../ui/primitives/roundimage/roundimage.go | 161 +++++++----- internal/ui/primitives/spinner/spinner.go | 4 + internal/ui/rich/labeluri/labeluri.go | 39 ++- internal/ui/rich/parser/markup/markup.go | 25 +- internal/ui/service/list.go | 240 ++---------------- internal/ui/service/service.go | 45 +--- internal/ui/service/session/list.go | 23 +- .../ui/service/session/server/children.go | 6 +- internal/ui/service/session/server/server.go | 8 +- internal/ui/service/session/session.go | 209 +-------------- internal/ui/service/view.go | 19 +- internal/ui/ui.go | 22 +- 24 files changed, 471 insertions(+), 713 deletions(-) create mode 100644 internal/ui/primitives/drag/drag.go diff --git a/go.mod b/go.mod index 450d4d6..dfab41c 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,13 @@ go 1.14 replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d +replace github.com/diamondburned/cchat-discord => ../cchat-discord/ + require ( github.com/Xuanwo/go-locale v0.2.0 github.com/alecthomas/chroma v0.7.3 github.com/diamondburned/cchat v0.0.43 - github.com/diamondburned/cchat-discord v0.0.0-20200715034853-d1e516c919c4 + github.com/diamondburned/cchat-discord v0.0.0-20200716062508-093bedb3048e github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 github.com/disintegration/imaging v1.6.2 diff --git a/go.sum b/go.sum index a26b6de..05b26d5 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ github.com/diamondburned/cchat v0.0.43 h1:HetAujSaUSdnQgAUZgprNLARjf/MSWXpCfWdvX github.com/diamondburned/cchat v0.0.43/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= github.com/diamondburned/cchat-discord v0.0.0-20200715034853-d1e516c919c4 h1:aealHwitg0XO8Nfgitq73FRsNCETeKX7M9Rqnwm32No= github.com/diamondburned/cchat-discord v0.0.0-20200715034853-d1e516c919c4/go.mod h1:ZmeZUjT3TVEItYUTi274ICTi+xDf2CCimD2yXRCaWdo= +github.com/diamondburned/cchat-discord v0.0.0-20200716062508-093bedb3048e h1:VeRejmVomD2fHsFfhkEdg0FWocd8Gz00gI38cCRyGRw= +github.com/diamondburned/cchat-discord v0.0.0-20200716062508-093bedb3048e/go.mod h1:cX6rGfvIv2rfNPrhfcRx88bfNxyL7eFmiYZLCWGfchw= github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b h1:sq0MXjJc3yAOZvuolRxOpKQNvpMLyTmsECxQqdYgF5E= github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b/go.mod h1:+bAf0m2o5qH54DmYJ/lR1HeITV53ol0JaoKyFFx3m3E= github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d h1:Ha/I6PMKi+B4hpWclwlXj0tUMehR7Q0TNxPczzBwzPI= @@ -58,6 +60,8 @@ github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 h1:OWxllHbUp github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ= github.com/diamondburned/ningen v0.1.1-0.20200715034121-61de7138eb56 h1:DAk3bEwJZycjfZu4OXDYrR/nmpy3ZS/dfUF0rskfVj0= github.com/diamondburned/ningen v0.1.1-0.20200715034121-61de7138eb56/go.mod h1:SKPY3387RHCbMrnefex9D+zlrA2yB+LCtaaQAgatAuc= +github.com/diamondburned/ningen v0.1.1-0.20200715040340-2395a0dbd0fa h1:ntHcz6GNzxn3TovtYZVwOBvL3xn7Iq1luaV/KEIEXrk= +github.com/diamondburned/ningen v0.1.1-0.20200715040340-2395a0dbd0fa/go.mod h1:SKPY3387RHCbMrnefex9D+zlrA2yB+LCtaaQAgatAuc= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= diff --git a/icons/icons.go b/icons/icons.go index db947d0..43869f9 100644 --- a/icons/icons.go +++ b/icons/icons.go @@ -1,30 +1,32 @@ package icons import ( - "bytes" "log" "github.com/gotk3/gotk3/gdk" - "github.com/markbates/pkger" ) // static assets var assets = map[string]*gdk.Pixbuf{} -func Logo256Variant2() *gdk.Pixbuf { - return loadPixbuf(__cchat_variant2_256) +func Logo256Variant2(sz int) *gdk.Pixbuf { + return loadPixbuf(__cchat_variant2_256, sz) } -func Logo256() *gdk.Pixbuf { - return loadPixbuf(__cchat_256) +func Logo256(sz int) *gdk.Pixbuf { + return loadPixbuf(__cchat_256, sz) } -func loadPixbuf(data []byte) *gdk.Pixbuf { +func loadPixbuf(data []byte, sz int) *gdk.Pixbuf { l, err := gdk.PixbufLoaderNew() if err != nil { log.Fatalln("Failed to create a pixbuf loader for icons:", err) } + if sz > 0 { + l.Connect("size-prepared", func() { l.SetSize(sz, sz) }) + } + p, err := l.WriteAndReturnPixbuf(data) if err != nil { log.Fatalln("Failed to write and return pixbuf:", err) @@ -32,18 +34,3 @@ func loadPixbuf(data []byte) *gdk.Pixbuf { return p } - -func readFile(name string) []byte { - f, err := pkger.Open(name) - if err != nil { - log.Fatalln("Failed to open pkger file:", err) - } - defer f.Close() - - var buf bytes.Buffer - if _, err := buf.ReadFrom(f); err != nil { - log.Fatalln("Failed to read from pkger file:", err) - } - - return buf.Bytes() -} diff --git a/internal/gts/css.go b/internal/gts/css.go index a8d3496..935ca06 100644 --- a/internal/gts/css.go +++ b/internal/gts/css.go @@ -1,12 +1,9 @@ package gts import ( - "bytes" - "github.com/diamondburned/cchat-gtk/internal/log" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gtk" - "github.com/markbates/pkger" "github.com/pkg/errors" ) @@ -38,17 +35,3 @@ func LoadCSS(name, css string) { cssRepos[name] = prov } - -func readFile(buf *bytes.Buffer, file string) error { - f, err := pkger.Open(file) - if err != nil { - return errors.Wrap(err, "Failed to load a CSS file") - } - defer f.Close() - - if _, err := buf.ReadFrom(f); err != nil { - return errors.Wrap(err, "Failed to read file") - } - - return nil -} diff --git a/internal/gts/gts.go b/internal/gts/gts.go index c4d6835..cf6d750 100644 --- a/internal/gts/gts.go +++ b/internal/gts/gts.go @@ -75,6 +75,29 @@ func init() { App.Throttler = throttler.Bind(App.Application) } +// // AppMenuWidget returns the box that holds the app menu. +// func AppMenuWidget() (widget *gtk.Widget) { +// App.Header.For().Foreach(func(v interface{}) { +// // If we've already found the widget, then stop finding. +// if widget != nil { +// return +// } + +// // Cast the interface to a widget. +// curr := v.(gtk.IWidget).ToWidget() + +// log.Println("testing") + +// // Check if the widget has a class named "left". +// if sctx, _ := curr.GetStyleContext(); sctx.HasClass("left") { +// log.Println("has class .left") +// widget = curr +// } +// }) + +// return +// } + type Window interface { Window() gtk.IWidget Header() gtk.IWidget @@ -88,28 +111,30 @@ func Main(wfn func() Window) { // Load all CSS onto the default screen. loadProviders(getDefaultScreen()) + App.Header, _ = gtk.HeaderBarNew() + // Right buttons only. + App.Header.SetDecorationLayout(":minimize,close") + App.Header.SetShowCloseButton(true) + App.Header.SetProperty("spacing", 0) + + b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) + App.Header.SetCustomTitle(b) + + App.Window, _ = gtk.ApplicationWindowNew(App.Application) + App.Window.SetDefaultSize(1000, 500) + App.Window.SetTitlebar(App.Header) + // Execute the function later, because we need it to run after // initialization. w := wfn() App.Application.SetAppMenu(w.Menu()) - App.Header, _ = gtk.HeaderBarNew() - // Right buttons only. - App.Header.SetDecorationLayout("menu:minimize,close") - App.Header.SetShowCloseButton(true) - App.Header.Show() - - // b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) - // App.Header.SetCustomTitle(b) - - App.Window, _ = gtk.ApplicationWindowNew(App.Application) - App.Window.SetDefaultSize(1000, 500) - App.Window.SetTitlebar(App.Header) App.Window.SetIcon(w.Icon()) + App.Window.Add(w.Window()) App.Window.Show() - App.Window.Add(w.Window()) App.Header.Add(w.Header()) + App.Header.Show() // Connect extra actions. AddAppAction("quit", App.Window.Destroy) @@ -129,7 +154,6 @@ func Main(wfn func() Window) { w.Close() }) }) - }) // Use a special function to run the application. Exit with the appropriate diff --git a/internal/ui/header.go b/internal/ui/header.go index c53eda5..7285af8 100644 --- a/internal/ui/header.go +++ b/internal/ui/header.go @@ -4,9 +4,12 @@ import ( "html" "strings" + "github.com/diamondburned/cchat-gtk/icons" + "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/actions" "github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb" "github.com/diamondburned/cchat-gtk/internal/ui/service/session" + "github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/gtk" ) @@ -14,6 +17,7 @@ type header struct { *gtk.Box left *headerLeft // middle-ish right *headerRight + menu *glib.Menu } func newHeader() *header { @@ -32,15 +36,22 @@ func newHeader() *header { box.PackStart(right, true, true, 0) box.Show() + menu := glib.MenuNew() + menu.Append("Preferences", "app.preferences") + menu.Append("Quit", "app.quit") + + left.appmenu.SetMenuModel(&menu.MenuModel) + // TODO return &header{ box, left, right, + menu, } } -const BreadcrumbSlash = `/` +const BreadcrumbSlash = `/` func (h *header) SetBreadcrumber(b breadcrumb.Breadcrumber) { if b == nil { @@ -59,24 +70,63 @@ func (h *header) SetBreadcrumber(b breadcrumb.Breadcrumber) { } func (h *header) SetSessionMenu(s *session.Row) { - h.left.openmenu.Bind(s.ActionsMenu) + h.left.sesmenu.Bind(s.ActionsMenu) +} + +type appMenu struct { + *gtk.MenuButton +} + +func newAppMenu() *appMenu { + img, _ := gtk.ImageNew() + img.SetFromPixbuf(icons.Logo256(24)) + img.Show() + + appmenu, _ := gtk.MenuButtonNew() + appmenu.SetImage(img) + appmenu.SetUsePopover(true) + appmenu.SetHAlign(gtk.ALIGN_CENTER) + appmenu.SetMarginStart(8) + appmenu.SetMarginEnd(8) + + return &appMenu{appmenu} +} + +func (a *appMenu) SetSizeRequest(w, h int) { + // Subtract the margin size. + if w -= 8 * 2; w < 0 { + w = 0 + } + + a.MenuButton.SetSizeRequest(w, h) } type headerLeft struct { *gtk.Box - openmenu *actions.MenuButton + appmenu *appMenu + sesmenu *actions.MenuButton } func newHeaderLeft() *headerLeft { - openmenu := actions.NewMenuButton() - openmenu.Show() + appmenu := newAppMenu() + appmenu.Show() + + sep, _ := gtk.SeparatorNew(gtk.ORIENTATION_VERTICAL) + sep.Show() + primitives.AddClass(sep, "titlebutton") + + sesmenu := actions.NewMenuButton() + sesmenu.Show() box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) - box.PackStart(openmenu, false, false, 5) + box.PackStart(appmenu, false, false, 0) + box.PackStart(sep, false, false, 0) + box.PackStart(sesmenu, false, false, 5) return &headerLeft{ - Box: box, - openmenu: openmenu, + Box: box, + appmenu: appmenu, + sesmenu: sesmenu, } } diff --git a/internal/ui/messages/container/cozy/cozy.go b/internal/ui/messages/container/cozy/cozy.go index 686ee74..e1572cd 100644 --- a/internal/ui/messages/container/cozy/cozy.go +++ b/internal/ui/messages/container/cozy/cozy.go @@ -112,7 +112,7 @@ func (c *Container) reuseAvatar(authorID, avatarURL string, full *FullMessage) { // Borrow the avatar pixbuf, but only if the avatar URL is the same. p, ok := lastAuthorMsg.(AvatarPixbufCopier) if ok && lastAuthorMsg.AvatarURL() == avatarURL { - p.CopyAvatarPixbuf(full.Avatar) + p.CopyAvatarPixbuf(full.Avatar.Image) full.Avatar.ManuallySetURL(avatarURL) } else { // We can't borrow, so we need to fetch it anew. diff --git a/internal/ui/messages/container/cozy/message_full.go b/internal/ui/messages/container/cozy/message_full.go index f4e5732..aa2882f 100644 --- a/internal/ui/messages/container/cozy/message_full.go +++ b/internal/ui/messages/container/cozy/message_full.go @@ -10,8 +10,9 @@ import ( "github.com/diamondburned/cchat-gtk/internal/ui/messages/input" "github.com/diamondburned/cchat-gtk/internal/ui/messages/message" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" - "github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu" + "github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage" + "github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri" "github.com/gotk3/gotk3/gtk" ) @@ -43,6 +44,13 @@ var boldCSS = primitives.PrepareCSS(` * { font-weight: 600; } `) +var avatarCSS = primitives.PrepareClassCSS("cozy-avatar", ` + /* Slightly dip down on click */ + .cozy-avatar:active { + margin-top: 1px; + } +`) + func NewFullMessage(msg cchat.MessageCreate) *FullMessage { msgc := WrapFullMessage(message.NewContainer(msg)) // Don't update the avatar. NewMessage in controller will try and reuse the @@ -57,6 +65,11 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage { avatar := NewAvatar() avatar.SetMarginTop(TopFullMargin) avatar.SetMarginStart(container.ColumnSpacing * 2) + avatar.Connect("clicked", func() { + if output := gc.Username.Output(); len(output.Mentions) > 0 { + labeluri.PopoverMentioner(avatar, output.Mentions[0]) + } + }) // We don't call avatar.Show(). That's called in Attach. // Style the timestamp accordingly. @@ -64,8 +77,8 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage { gc.Timestamp.SetVAlign(gtk.ALIGN_END) // bottom-align gc.Timestamp.SetMarginStart(0) // clear margins - // Attach the class for the left avatar. - primitives.AddClass(avatar, "cozy-avatar") + // Attach the class and CSS for the left avatar. + avatarCSS(avatar) // Attach the username style provider. primitives.AttachCSS(gc.Username, boldCSS) @@ -123,11 +136,11 @@ func (m *FullMessage) UpdateAuthor(author cchat.MessageAuthor) { // CopyAvatarPixbuf sets the pixbuf into the given container. This shares the // same pixbuf, but gtk.Image should take its own reference from the pixbuf. func (m *FullMessage) CopyAvatarPixbuf(dst httputil.ImageContainer) { - switch m.Avatar.GetStorageType() { + switch img := m.Avatar.Image; img.GetStorageType() { case gtk.IMAGE_PIXBUF: - dst.SetFromPixbuf(m.Avatar.GetPixbuf()) + dst.SetFromPixbuf(img.GetPixbuf()) case gtk.IMAGE_ANIMATION: - dst.SetFromAnimation(m.Avatar.GetAnimation()) + dst.SetFromAnimation(img.GetAnimation()) } } @@ -165,17 +178,19 @@ func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage { } type Avatar struct { - roundimage.Image + roundimage.Button url string } func NewAvatar() *Avatar { - avatar, _ := roundimage.NewImage(0) + avatar, _ := roundimage.NewButton() avatar.SetSizeRequest(AvatarSize, AvatarSize) avatar.SetVAlign(gtk.ALIGN_START) // Default icon. - primitives.SetImageIcon(avatar.Image, "user-available-symbolic", AvatarSize) + primitives.SetImageIcon( + avatar.Image.Image, "user-available-symbolic", AvatarSize, + ) return &Avatar{*avatar, ""} } @@ -191,7 +206,7 @@ func (a *Avatar) SetURL(url string) { } a.url = url - httputil.AsyncImageSized(a, url, AvatarSize, AvatarSize) + httputil.AsyncImageSized(a.Image, url, AvatarSize, AvatarSize) } // ManuallySetURL sets the URL without downloading the image. It assumes the diff --git a/internal/ui/messages/message/message.go b/internal/ui/messages/message/message.go index 36e6e2e..e78be3d 100644 --- a/internal/ui/messages/message/message.go +++ b/internal/ui/messages/message/message.go @@ -6,9 +6,9 @@ import ( "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/humanize" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" + "github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu" "github.com/diamondburned/cchat-gtk/internal/ui/rich" "github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri" - "github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu" "github.com/diamondburned/cchat/text" "github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/pango" @@ -100,6 +100,7 @@ func NewEmptyContainer() *GenericContainer { user.SetLineWrapMode(pango.WRAP_WORD_CHAR) user.SetXAlign(1) // right align user.SetVAlign(gtk.ALIGN_START) + user.SetTrackVisitedLinks(false) user.Show() ctbody := labeluri.NewLabel(text.Rich{}) @@ -108,6 +109,7 @@ func NewEmptyContainer() *GenericContainer { ctbody.SetLineWrapMode(pango.WRAP_WORD_CHAR) ctbody.SetXAlign(0) // left align ctbody.SetSelectable(true) + ctbody.SetTrackVisitedLinks(false) ctbody.Show() // Wrap the content label inside a content box. diff --git a/internal/ui/messages/view.go b/internal/ui/messages/view.go index 5d8ff50..1d2c79b 100644 --- a/internal/ui/messages/view.go +++ b/internal/ui/messages/view.go @@ -85,7 +85,7 @@ func NewView() *View { primitives.AddClass(view.Box, "message-view") // placeholder logo - logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256Variant2()) + logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256Variant2(128)) logo.Show() view.FaceView = sadface.New(view.Box, logo) diff --git a/internal/ui/primitives/drag/drag.go b/internal/ui/primitives/drag/drag.go new file mode 100644 index 0000000..a7255a1 --- /dev/null +++ b/internal/ui/primitives/drag/drag.go @@ -0,0 +1,116 @@ +package drag + +import ( + "github.com/diamondburned/cchat-gtk/internal/ui/primitives" + "github.com/gotk3/gotk3/gdk" + "github.com/gotk3/gotk3/gtk" +) + +func NewTargetEntry(target string) gtk.TargetEntry { + e, _ := gtk.TargetEntryNew(target, gtk.TARGET_SAME_APP, 0) + return *e +} + +// Find searches the given container for the draggable widget with the given +// name. +func Find(w primitives.Container, id string) int { + var index = -1 // not found default + + primitives.EachChildren(w, func(i int, v interface{}) bool { + if primitives.GetName(v.(primitives.Namer)) == id { + index = i + return true + } + + return false + }) + + return index +} + +type MainDraggable interface { + ID() string + SetName(string) + SetSensitive(bool) + + gtk.IWidget + Draggable +} + +type Draggable interface { + DragSourceSet(gdk.ModifierType, []gtk.TargetEntry, gdk.DragAction) + DragDestSet(gtk.DestDefaults, []gtk.TargetEntry, gdk.DragAction) + + primitives.Connector +} + +// Swapper is the type for a swap function. +type Swapper = func(targetID, movingID string) + +// BindDraggable binds the draggable widget and make it drag-and-droppable. The +// parent MUST have its own state of children and MUST NOT rely on its container +// states. +// +// This function can take additional draggers, which will override the main +// draggable and will be the only widgets that can be dragged away. The source +// ID will be taken from the main draggable. +func BindDraggable(dg MainDraggable, icon string, fn Swapper, draggers ...Draggable) { + var atom = "data_" + icon + var dragEntries = []gtk.TargetEntry{NewTargetEntry(atom)} + var dragAtom = gdk.GdkAtomIntern(atom, false) + + // Set the ID for Find(). + dg.SetName(dg.ID()) + + // Make closures function so we can use twice. + srcSet := func(dragger Draggable) { + // Drag source so you can drag the button away. + dragger.DragSourceSet(gdk.BUTTON1_MASK, dragEntries, gdk.ACTION_MOVE) + + dragger.Connect("drag-data-get", + func(_ gtk.IWidget, ctx *gdk.DragContext, data *gtk.SelectionData) { + // Set the index-in-bytes. + data.SetData(dragAtom, []byte(dg.ID())) + }, + ) + + dragger.Connect("drag-begin", + func(_ gtk.IWidget, ctx *gdk.DragContext) { + gtk.DragSetIconName(ctx, icon, 0, 0) + dg.SetSensitive(false) + }, + ) + + dragger.Connect("drag-end", + func() { + dg.SetSensitive(true) + }, + ) + } + dstSet := func(dragger Draggable) { + // Drag destination so you can drag the button here. + dragger.DragDestSet(gtk.DEST_DEFAULT_ALL, dragEntries, gdk.ACTION_MOVE) + + dragger.Connect("drag-data-received", + func(_ gtk.IWidget, ctx *gdk.DragContext, x, y uint, data *gtk.SelectionData) { + // Receive the incoming row's ID and call MoveSession. + fn(dg.ID(), string(data.GetData())) + }, + ) + } + + // If we have no extra draggers given, then the MainDraggable should also be + // a source. + if len(draggers) == 0 { + srcSet(dg) + } else { + // Else, set drag sources only on those extra draggables. + for _, dragger := range draggers { + srcSet(dragger) + dstSet(dragger) + } + } + + // Make MainDraggable a drag destination as well. + dstSet(dg) +} diff --git a/internal/ui/primitives/primitives.go b/internal/ui/primitives/primitives.go index 826e16e..91af3d2 100644 --- a/internal/ui/primitives/primitives.go +++ b/internal/ui/primitives/primitives.go @@ -35,7 +35,7 @@ func GetName(namer Namer) string { return nm } -func EachChildren(w interface{ GetChildren() *glib.List }, fn func(i int, v interface{}) bool) { +func EachChildren(w Container, fn func(i int, v interface{}) bool) { var cursor int = -1 for ptr := w.GetChildren(); ptr != nil; ptr = ptr.Next() { cursor++ @@ -46,45 +46,6 @@ func EachChildren(w interface{ GetChildren() *glib.List }, fn func(i int, v inte } } -type DragSortable interface { - DragSourceSet(gdk.ModifierType, []gtk.TargetEntry, gdk.DragAction) - DragDestSet(gtk.DestDefaults, []gtk.TargetEntry, gdk.DragAction) - GetAllocation() *gtk.Allocation - Connector -} - -func BindDragSortable(ds DragSortable, target, id string, fn func(id, target string)) { - var dragEntries = []gtk.TargetEntry{NewTargetEntry(target)} - var dragAtom = gdk.GdkAtomIntern(target, true) - - // Drag source so you can drag the button away. - ds.DragSourceSet(gdk.BUTTON1_MASK, dragEntries, gdk.ACTION_MOVE) - - // Drag destination so you can drag the button here. - ds.DragDestSet(gtk.DEST_DEFAULT_ALL, dragEntries, gdk.ACTION_MOVE) - - ds.Connect("drag-data-get", - // TODO change ToggleButton. - func(ds DragSortable, ctx *gdk.DragContext, data *gtk.SelectionData) { - // Set the index-in-bytes. - data.SetData(dragAtom, []byte(id)) - }, - ) - - ds.Connect("drag-data-received", - func(ds DragSortable, ctx *gdk.DragContext, x, y uint, data *gtk.SelectionData) { - // Receive the incoming row's ID and call MoveSession. - fn(id, string(data.GetData())) - }, - ) - - ds.Connect("drag-begin", - func(ds DragSortable, ctx *gdk.DragContext) { - gtk.DragSetIconName(ctx, "user-available-symbolic", 0, 0) - }, - ) -} - type StyleContexter interface { GetStyleContext() (*gtk.StyleContext, error) } @@ -199,11 +160,6 @@ func BindDynamicMenu(connector Connector, constr func(menu *gtk.Menu)) { }) } -func NewTargetEntry(target string) gtk.TargetEntry { - e, _ := gtk.TargetEntryNew(target, gtk.TARGET_SAME_APP, 0) - return *e -} - // NewMenuActionButton is the same as NewActionButton, but it uses the // open-menu-symbolic icon. func NewMenuActionButton(actions [][2]string) *gtk.MenuButton { @@ -286,3 +242,7 @@ func AttachCSS(ctx StyleContexter, prov *gtk.CssProvider) { s, _ := ctx.GetStyleContext() s.AddProvider(prov, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) } + +func InlineCSS(ctx StyleContexter, css string) { + AttachCSS(ctx, PrepareCSS(css)) +} diff --git a/internal/ui/primitives/roundimage/roundimage.go b/internal/ui/primitives/roundimage/roundimage.go index 7b49b93..5841bf1 100644 --- a/internal/ui/primitives/roundimage/roundimage.go +++ b/internal/ui/primitives/roundimage/roundimage.go @@ -3,6 +3,7 @@ package roundimage import ( "math" + "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/gotk3/gotk3/cairo" "github.com/gotk3/gotk3/gtk" ) @@ -12,6 +13,32 @@ const ( circle = 2 * math.Pi ) +// Button implements a rounded button with a rounded image. This widget only +// supports a full circle for rounding. +type Button struct { + *gtk.Button + Image *Image +} + +var roundButtonCSS = primitives.PrepareClassCSS("round-button", ` + .round-button { + padding: 0; + border-radius: 50%; + } +`) + +func NewButton() (*Button, error) { + image, _ := NewImage(0) + image.Show() + + b, _ := gtk.ButtonNew() + b.SetImage(image) + b.SetRelief(gtk.RELIEF_NONE) + roundButtonCSS(b) + + return &Button{Button: b, Image: image}, nil +} + type Image struct { *gtk.Image Radius float64 @@ -28,74 +55,7 @@ func NewImage(radius float64) (*Image, error) { image := &Image{Image: i, Radius: radius} // Connect to the draw callback and clip the context. - i.Connect("draw", func(i *gtk.Image, cc *cairo.Context) bool { - var w = float64(i.GetAllocatedWidth()) - var h = float64(i.GetAllocatedHeight()) - - var min = w - // Use the smallest side for radius calculation. - if h < w { - min = h - } - - // Copy the variables in case we need to change them. - var r = image.Radius - - // We have to do this so the arc paint doesn't leave back a black - // background instead of the usual alpha. - // cc.SetSourceRGBA(255, 255, 255, 0) - - switch { - // If radius is less than 0, then don't round. - case r < 0: - return false - - // If radius is 0, then we have to calculate our own radius.:This only - // works if the image is a square. - case r == 0: - // Calculate the radius by dividing a side by 2. - r = (min / 2) - - // Draw an arc from 0deg to 360deg. - cc.Arc(w/2, h/2, r, 0, circle) - cc.SetSourceRGBA(255, 255, 255, 0) - - // Clip the image with the arc we drew. - cc.Clip() - - // If radius is more than 0, then we have to calculate the radius from - // the edges. - case r > 0: - // StackOverflow is godly. - // https://stackoverflow.com/a/6959843. - - // Account for the offset. - // r += o - - // Radius should be largest a single side divided by 2. - if max := min / 2; r > max { - r = max - } - - // Draw 4 arcs at 4 corners. - cc.Arc(0+r, 0+r, r, 2*(pi/2), 3*(pi/2)) // top left - cc.Arc(w-r, 0+r, r, 3*(pi/2), 4*(pi/2)) // top right - cc.Arc(w-r, h-r, r, 0*(pi/2), 1*(pi/2)) // bottom right - cc.Arc(0+r, h-r, r, 1*(pi/2), 2*(pi/2)) // bottom left - - // Close the created path. - cc.ClosePath() - cc.SetSourceRGBA(255, 255, 255, 0) - - // Clip the image with the arc we drew. - cc.Clip() - } - - // Paint the changes. - cc.Paint() - - return false - }) + i.Connect("draw", image.drawer) return image, nil } @@ -103,3 +63,68 @@ func NewImage(radius float64) (*Image, error) { func (i *Image) SetRadius(r float64) { i.Radius = r } + +func (i *Image) drawer(widget gtk.IWidget, cc *cairo.Context) bool { + var w = float64(i.GetAllocatedWidth()) + var h = float64(i.GetAllocatedHeight()) + + var min = w + // Use the smallest side for radius calculation. + if h < w { + min = h + } + + // Copy the variables in case we need to change them. + var r = i.Radius + + switch { + // If radius is less than 0, then don't round. + case r < 0: + return false + + // If radius is 0, then we have to calculate our own radius.:This only + // works if the image is a square. + case r == 0: + // Calculate the radius by dividing a side by 2. + r = (min / 2) + + // Draw an arc from 0deg to 360deg. + cc.Arc(w/2, h/2, r, 0, circle) + + // We have to do this so the arc paint doesn't leave back a black + // background instead of the usual alpha. + cc.SetSourceRGBA(255, 255, 255, 0) + + // Clip the image with the arc we drew. + cc.Clip() + + // If radius is more than 0, then we have to calculate the radius from + // the edges. + case r > 0: + // StackOverflow is godly. + // https://stackoverflow.com/a/6959843. + + // Radius should be largest a single side divided by 2. + if max := min / 2; r > max { + r = max + } + + // Draw 4 arcs at 4 corners. + cc.Arc(0+r, 0+r, r, 2*(pi/2), 3*(pi/2)) // top left + cc.Arc(w-r, 0+r, r, 3*(pi/2), 4*(pi/2)) // top right + cc.Arc(w-r, h-r, r, 0*(pi/2), 1*(pi/2)) // bottom right + cc.Arc(0+r, h-r, r, 1*(pi/2), 2*(pi/2)) // bottom left + + // Close the created path. + cc.ClosePath() + cc.SetSourceRGBA(255, 255, 255, 0) + + // Clip the image with the arc we drew. + cc.Clip() + } + + // Paint the changes. + cc.Paint() + + return false +} diff --git a/internal/ui/primitives/spinner/spinner.go b/internal/ui/primitives/spinner/spinner.go index 61f631f..7d9e752 100644 --- a/internal/ui/primitives/spinner/spinner.go +++ b/internal/ui/primitives/spinner/spinner.go @@ -9,11 +9,15 @@ type Boxed struct { func New() *Boxed { spin, _ := gtk.SpinnerNew() + spin.SetHAlign(gtk.ALIGN_CENTER) + spin.SetVAlign(gtk.ALIGN_CENTER) spin.Show() box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) box.SetHAlign(gtk.ALIGN_CENTER) box.SetVAlign(gtk.ALIGN_CENTER) + box.SetHExpand(true) + box.SetVExpand(true) box.Add(spin) return &Boxed{box, spin} diff --git a/internal/ui/rich/labeluri/labeluri.go b/internal/ui/rich/labeluri/labeluri.go index b53b732..f7c86f3 100644 --- a/internal/ui/rich/labeluri/labeluri.go +++ b/internal/ui/rich/labeluri/labeluri.go @@ -82,19 +82,8 @@ func BindRichLabel(label Labeler) { var output = label.Output() if mention := output.IsMention(uri); mention != nil { - if info := mention.MentionInfo(); !info.Empty() { - l, _ := gtk.LabelNew(markup.Render(info)) - l.SetUseMarkup(true) - l.SetXAlign(0) - l.Show() - - // Enable images??? - BindActivator(l) - - p, _ := gtk.PopoverNew(label) + if p := popoverMentioner(label, mention); p != nil { p.SetPointingTo(ptr) - p.Add(l) - p.Connect("destroy", l.Destroy) p.Popup() } @@ -105,6 +94,32 @@ func BindRichLabel(label Labeler) { }) } +func PopoverMentioner(rel gtk.IWidget, mention text.Mentioner) { + if p := popoverMentioner(rel, mention); p != nil { + p.Popup() + } +} + +func popoverMentioner(rel gtk.IWidget, mention text.Mentioner) *gtk.Popover { + var info = mention.MentionInfo() + if info.Empty() { + return nil + } + + l, _ := gtk.LabelNew(markup.Render(info)) + l.SetUseMarkup(true) + l.SetXAlign(0) + l.Show() + + // Enable images??? + BindActivator(l) + + p, _ := gtk.PopoverNew(rel) + p.Add(l) + p.Connect("destroy", l.Destroy) + return p +} + func BindActivator(connector WidgetConnector) { bind(connector, nil) } diff --git a/internal/ui/rich/parser/markup/markup.go b/internal/ui/rich/parser/markup/markup.go index 856c137..bb2e72a 100644 --- a/internal/ui/rich/parser/markup/markup.go +++ b/internal/ui/rich/parser/markup/markup.go @@ -61,6 +61,14 @@ func RenderCmplx(content text.Rich) RenderOutput { buf := bytes.Buffer{} buf.Grow(len(content.Content)) + // Sort so that all ending points are sorted decrementally. We probably + // don't need SliceStable here, as we're sorting again. + sort.Slice(content.Segments, func(i, j int) bool { + _, i = content.Segments[i].Bounds() + _, j = content.Segments[j].Bounds() + return i > j + }) + // Sort so that all starting points are sorted incrementally. sort.SliceStable(content.Segments, func(i, j int) bool { i, _ = content.Segments[i].Bounds() @@ -93,6 +101,10 @@ func RenderCmplx(content text.Rich) RenderOutput { appended.Open(start, composeAvatarMarkup(segment)) } + if segment, ok := segment.(text.Colorer); ok { + appended.Span(start, end, fmt.Sprintf("color=\"#%06X\"", segment.Color())) + } + // Mentioner needs to be before colorer, as we'd want the below color // segment to also highlight the full mention as well as make the // padding part of the hyperlink. @@ -101,12 +113,15 @@ func RenderCmplx(content text.Rich) RenderOutput { // components will take care of showing the information. appended.AnchorNU(start, end, fmt.Sprintf(f_Mention, len(mentions))) mentions = append(mentions, segment) - } - if segment, ok := segment.(text.Colorer); ok { - var covered = attrmap.CoverAll(content, start, end) - appended.Span(start, end, color(segment.Color(), !covered)...) - if !covered { // add padding if doesn't cover all + if segment, ok := segment.(text.Colorer); ok { + // Add a dimmed background highlight and pad the button-like + // link. + appended.Span( + start, end, + "bgalpha=\"10%\"", + fmt.Sprintf("bgcolor=\"#%06X\"", segment.Color()), + ) appended.Pad(start, end) } } diff --git a/internal/ui/service/list.go b/internal/ui/service/list.go index f3fb897..466c64e 100644 --- a/internal/ui/service/list.go +++ b/internal/ui/service/list.go @@ -3,6 +3,7 @@ package service import ( "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" + "github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag" "github.com/diamondburned/cchat-gtk/internal/ui/service/session" "github.com/diamondburned/cchat-gtk/internal/ui/service/session/server" "github.com/gotk3/gotk3/gtk" @@ -43,6 +44,8 @@ func NewList(vctl ViewController) *List { // List box of buttons. svlist.ListBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) svlist.ListBox.Show() + svlist.ListBox.SetHAlign(gtk.ALIGN_START) + svlist.ListBox.SetHExpand(false) listCSS(svlist.ListBox) svlist.ScrolledWindow, _ = gtk.ScrolledWindowNew(nil, nil) @@ -82,230 +85,23 @@ func (sl *List) AddService(svc cchat.Service) { // TODO: drag-and-drop? } -/* -type View struct { - *gtk.ScrolledWindow - Box *gtk.Box - Services []*Container -} - -var servicesCSS = primitives.PrepareCSS(` - .services { - background-color: @theme_base_color; - } -`) - -func NewView() *View { - box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) - box.Show() - - primitives.AddClass(box, "services") - primitives.AttachCSS(box, servicesCSS) - - sw, _ := gtk.ScrolledWindowNew(nil, nil) - sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) - sw.Add(box) - - return &View{ - sw, - box, - nil, - } -} - -func (v *View) AddService(svc cchat.Service, ctrl Controller) *Container { - s := NewContainer(svc, ctrl) - v.Services = append(v.Services, s) - v.Box.Add(s) - - // Try and restore all sessions. - s.restoreAllSessions() - - return s -} - -type Controller interface { - // RowSelected is wrapped around session's MessageRowSelected. - RowSelected(*session.Row, *server.ServerRow, cchat.ServerMessage) - // AuthenticateSession is called to spawn the authentication dialog. - AuthenticateSession(*Container, cchat.Service) - // 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 -// child containers. -type Container struct { - *gtk.Box - Service cchat.Service - - header *header - revealer *gtk.Revealer - children *children - - // Embed controller and extend it to override RestoreSession. - Controller -} - -// Guarantee that our interface is up-to-date with session's controller. -var _ session.Controller = (*Container)(nil) - -func NewContainer(svc cchat.Service, ctrl Controller) *Container { - children := newChildren() - - chrev, _ := gtk.RevealerNew() - chrev.SetRevealChild(true) - chrev.Add(children) - chrev.Show() - - header := newHeader(svc) - header.SetActive(chrev.GetRevealChild()) - - box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) - box.Show() - box.PackStart(header, false, false, 0) - box.PackStart(chrev, false, false, 0) - - primitives.AddClass(box, "service") - - container := &Container{ - Box: box, - Service: svc, - header: header, - revealer: chrev, - children: children, - Controller: ctrl, - } - - // On click, toggle reveal. - header.Connect("clicked", func() { - revealed := !chrev.GetRevealChild() - chrev.SetRevealChild(revealed) - header.SetActive(revealed) - }) - - // On click, show the auth dialog. - header.Add.Connect("clicked", func() { - ctrl.AuthenticateSession(container, svc) - }) - - // Add more menu item(s). - header.Menu.AddSimpleItem("Save Sessions", container.SaveAllSessions) - - return container -} - -func (c *Container) GetService() cchat.Service { - return c.Service -} - -func (c *Container) Sessions() []*session.Row { - return c.children.Sessions() -} - -func (c *Container) AddSession(ses cchat.Session) *session.Row { - srow := session.New(c, ses, c) - c.children.AddSessionRow(ses.ID(), srow) - c.SaveAllSessions() - return srow -} - -func (c *Container) AddLoadingSession(id, name string) *session.Row { - srow := session.NewLoading(c, id, name, c) - c.children.AddSessionRow(id, srow) - return srow -} - -func (c *Container) RemoveSession(row *session.Row) { - var id = row.Session.ID() - c.children.RemoveSessionRow(id) - c.SaveAllSessions() - // Call the parent's method. - c.Controller.OnSessionRemove(id) -} - -func (c *Container) MoveSession(rowID, beneathRowID string) { - c.children.MoveSession(rowID, beneathRowID) - 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, id string) { - // Can this session be restored? If not, exit. - restorer, ok := c.Service.(cchat.SessionRestorer) - if !ok { - return - } - - // 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. -func (c *Container) restoreAllSessions() { - // Can this session be restored? If not, exit. - restorer, ok := c.Service.(cchat.SessionRestorer) - if !ok { - return - } - - var sessions = keyring.RestoreSessions(c.Service.Name()) - - for _, krs := range sessions { - // Copy the session to avoid race conditions. - krs := krs - row := c.AddLoadingSession(krs.ID, krs.Name) - - c.restoreSession(row, restorer, krs) - } -} - -func (c *Container) restoreSession(r *session.Row, res cchat.SessionRestorer, k keyring.Session) { - go func() { - s, err := res.RestoreSession(k.Data) - if err != nil { - err = errors.Wrapf(err, "Failed to restore session %s (%s)", k.ID, k.Name) - log.Error(err) - - gts.ExecAsync(func() { r.SetFailed(err) }) - } else { - gts.ExecAsync(func() { r.SetSession(s) }) - } - }() -} - -func (c *Container) SaveAllSessions() { - var sessions = c.children.Sessions() - var ksessions = make([]keyring.Session, 0, len(sessions)) - - for _, s := range sessions { - if k := s.KeyringSession(); k != nil { - ksessions = append(ksessions, *k) +func (sl *List) MoveService(targetID, movingID string) { + // Find the widgets. + var movingsv *Service + for _, svc := range sl.Services { + if svc.ID() == movingID { + movingsv = svc } } - keyring.SaveSessions(c.Service.Name(), ksessions) -} + // Not found, return. + if movingsv == nil { + return + } -func (c *Container) Breadcrumb() breadcrumb.Breadcrumb { - return breadcrumb.Try(nil, c.header.GetText()) + // Get the location of where to move the widget to. + var targetix = drag.Find(sl.ListBox, targetID) + + // Actually move the child. + sl.ListBox.ReorderChild(movingsv, targetix) } -*/ diff --git a/internal/ui/service/service.go b/internal/ui/service/service.go index f363255..e4e768a 100644 --- a/internal/ui/service/service.go +++ b/internal/ui/service/service.go @@ -7,6 +7,7 @@ import ( "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/primitives/drag" "github.com/diamondburned/cchat-gtk/internal/ui/rich" "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup" "github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb" @@ -24,6 +25,8 @@ type ListController interface { SessionSelected(*Service, *session.Row) // AuthenticateSession tells View to call to the parent's authenticator. AuthenticateSession(*Service) + // MoveService tells the view to shift the service to before the target. + MoveService(id, targetID string) OnSessionRemove(*Service, *session.Row) OnSessionDisconnect(*Service, *session.Row) @@ -125,6 +128,9 @@ func NewService(svc cchat.Service, svclctrl ListController) *Service { service.Box.Show() serviceCSS(service.Box) + // Bind a drag and drop on the button instead of the entire box. + drag.BindDraggable(service, "network-workgroup", svclctrl.MoveService, service.Button) + return service } @@ -163,6 +169,10 @@ func (s *Service) AddSession(ses cchat.Session) *session.Row { return srow } +func (s *Service) ID() string { + return s.service.Name().Content +} + func (s *Service) Service() cchat.Service { return s.service } @@ -247,38 +257,3 @@ func (s *Service) restoreAll() { func restoreAsync(r *session.Row, res cchat.SessionRestorer, k keyring.Session) { r.RestoreSession(res, k) } - -/* -type header struct { - *rich.ToggleButtonImage - Add *gtk.Button - - Menu *menu.LazyMenu -} - -func newHeader(svc cchat.Service) *header { - b := rich.NewToggleButtonImage(svc.Name()) - b.Image.SetPlaceholderIcon("folder-remote-symbolic", IconSize) - b.SetRelief(gtk.RELIEF_NONE) - b.SetMode(true) - b.Show() - - if iconer, ok := svc.(cchat.Icon); ok { - b.Image.AsyncSetIconer(iconer, "Error getting session logo") - } - - add, _ := gtk.ButtonNewFromIconName("list-add-symbolic", gtk.ICON_SIZE_BUTTON) - add.Show() - - // Add the button overlay into the main button. - buttonoverlay.Take(b, add, IconSize) - - // Construct a menu and its items. - var menu = menu.NewLazyMenu(b) - if configurator, ok := svc.(config.Configurator); ok { - menu.AddItems(config.MenuItem(configurator)) - } - - return &header{b, add, menu} -} -*/ diff --git a/internal/ui/service/session/list.go b/internal/ui/service/session/list.go index 27172b9..9ddca2d 100644 --- a/internal/ui/service/session/list.go +++ b/internal/ui/service/session/list.go @@ -2,6 +2,7 @@ package session import ( "github.com/diamondburned/cchat-gtk/internal/ui/primitives" + "github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag" "github.com/gotk3/gotk3/gtk" ) @@ -89,9 +90,6 @@ func (sl *List) AddSessionRow(id string, row *Row) { // Set the map, which increases the length by 1. sl.sessions[id] = row - // Bind the mover. - row.BindMover(id) - // Assert that a name can be obtained. namer := primitives.Namer(row) namer.SetName(id) // set ID here, get it in Move @@ -108,23 +106,16 @@ func (sl *List) RemoveSessionRow(sessionID string) bool { // MoveSession moves sessions around. This function must not touch the add // button. -func (sl *List) MoveSession(id, movingID string) { +func (sl *List) MoveSession(targetID, movingID string) { // Get the widget of the row that is moving. - var moving = sl.sessions[movingID] + var moving, ok = sl.sessions[movingID] + if !ok { + return // sometimes movingID might come from other services + } // Find the current position of the row that we're moving the other one // underneath of. - var rowix = -1 - - primitives.EachChildren(sl.ListBox, func(i int, v interface{}) bool { - // The obtained name will be the ID set in AddSessionRow. - if primitives.GetName(v.(primitives.Namer)) == id { - rowix = i - return true - } - - return false - }) + var rowix = drag.Find(sl.ListBox, targetID) // Reorder the box. sl.ListBox.Remove(moving) diff --git a/internal/ui/service/session/server/children.go b/internal/ui/service/session/server/children.go index ed31383..0aa0c05 100644 --- a/internal/ui/service/session/server/children.go +++ b/internal/ui/service/session/server/children.go @@ -26,7 +26,11 @@ type Children struct { // reserved var childrenCSS = primitives.PrepareClassCSS("server-children", ` - .server-children {} + .server-children { + margin: 0; + margin-top: 3px; + border-radius: 0; + } `) func NewChildren(p breadcrumb.Breadcrumber, ctrl Controller) *Children { diff --git a/internal/ui/service/session/server/server.go b/internal/ui/service/session/server/server.go index 34fdb37..355733c 100644 --- a/internal/ui/service/session/server/server.go +++ b/internal/ui/service/session/server/server.go @@ -4,10 +4,10 @@ import ( "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" + "github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu" "github.com/diamondburned/cchat-gtk/internal/ui/rich" "github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb" "github.com/diamondburned/cchat-gtk/internal/ui/service/button" - "github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu" "github.com/diamondburned/cchat/text" "github.com/gotk3/gotk3/gtk" "github.com/pkg/errors" @@ -22,7 +22,8 @@ type ServerRow struct { } var serverCSS = primitives.PrepareClassCSS("server", ` - .server { + /* Ignore first child because .server-children already covers this */ + .server:not(:first-child) { margin: 0; margin-top: 3px; border-radius: 0; @@ -130,8 +131,8 @@ func (r *Row) Reset() { // SetLoading is called by the parent struct. func (r *Row) SetLoading() { - r.Button.SetLoading() r.SetSensitive(false) + r.Button.SetLoading() } // SetFailed is shared between the parent struct and the children list. This is @@ -216,6 +217,7 @@ func (r *Row) Load() { // Set that we're now loading. r.children.setLoading() + r.SetLoading() r.SetSensitive(false) // Load the list of servers if we're still in loading mode. diff --git a/internal/ui/service/session/session.go b/internal/ui/service/session/session.go index d967ad9..d4fd0d5 100644 --- a/internal/ui/service/session/session.go +++ b/internal/ui/service/session/session.go @@ -7,6 +7,7 @@ import ( "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/primitives/drag" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/spinner" "github.com/diamondburned/cchat-gtk/internal/ui/rich" "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup" @@ -132,6 +133,9 @@ func newRow(parent breadcrumb.Breadcrumber, name text.Rich, ctrl Servicer) *Row } }) + // Bind drag-and-drop events. + drag.BindDraggable(row, "face-smile", ctrl.MoveSession) + return row } @@ -163,10 +167,6 @@ func (r *Row) Reset() { r.cmder = nil } -func (r *Row) SessionID() string { - return r.sessionID -} - func (r *Row) Breadcrumb() breadcrumb.Breadcrumb { return breadcrumb.Try(r.parentcrumb, r.Session.Name().Content) } @@ -204,6 +204,7 @@ func (r *Row) SetLoading() { spin.SetSizeRequest(IconSize, IconSize) spin.Start() spin.Show() + rowIconCSS(spin) r.Add(spin) r.SetSensitive(false) // no activate @@ -283,13 +284,6 @@ func (r *Row) SetSession(ses cchat.Session) { r.Servers.SetList(ses) } -// BindMover binds with the ID stored in the parent container to be used in the -// method itself. The ID may or may not have to do with session. -func (r *Row) BindMover(id string) { - // TODO: rows can be highlighted. - // primitives.BindDragSortable(r.Button, "GTK_TOGGLE_BUTTON", id, r.ctrl.MoveSession) -} - func (r *Row) RowSelected(sr *server.ServerRow, smsg cchat.ServerMessage) { r.svcctrl.RowSelected(r, sr, smsg) } @@ -371,196 +365,3 @@ func (r *Row) ShowCommander() { } r.cmder.ShowDialog() } - -// deprecate server.Row inheritance since the structure is entirely different - -/* -// Row represents a single session, including the button header and the -// children servers. -type Row struct { - *server.Row - Session cchat.Session - sessionID string // used for reconnection - - ctrl Servicer - - cmder *commander.Buffer - cmdbtn *gtk.Button -} - -func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Servicer) *Row { - row := newRow(parent, text.Rich{}, ctrl) - row.SetSession(ses) - return row -} - -func NewLoading(parent breadcrumb.Breadcrumber, id, name string, ctrl Servicer) *Row { - row := newRow(parent, text.Rich{Content: name}, ctrl) - row.sessionID = id - row.Row.SetLoading() - return row -} - -func newRow(parent breadcrumb.Breadcrumber, name text.Rich, ctrl Servicer) *Row { - srow := server.NewRow(parent, name) - srow.Button.SetPlaceholderIcon(IconName, IconSize) - srow.Show() - - // Bind the row to .session in CSS. - primitives.AddClass(srow, "session") - primitives.AddClass(srow, "server-list") - - // Make a commander button that's hidden by default in case. - cmdbtn, _ := gtk.ButtonNewFromIconName("utilities-terminal-symbolic", gtk.ICON_SIZE_BUTTON) - buttonoverlay.Take(srow.Button, cmdbtn, server.IconSize) - primitives.AddClass(cmdbtn, "command-button") - - row := &Row{ - Row: srow, - ctrl: ctrl, - cmdbtn: cmdbtn, - } - - cmdbtn.Connect("clicked", row.ShowCommander) - - return row -} - -// Reset extends the server row's Reset function and resets additional states. -// It resets all states back to nil, but the session ID stays. -func (r *Row) Reset() { - r.Row.Reset() - r.Session = nil - r.cmder = nil - r.cmdbtn.Hide() -} - -// 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, then don't run. In a legitimate case, this - // shouldn't happen. - if r.sessionID == "" { - return - } - - // Set the row as loading. - r.Row.SetLoading() - // Try to restore the session. - r.ctrl.RestoreSession(r, r.sessionID) -} - -// DisconnectSession disconnects the current session. It does nothing if the row -// does not have a session active. -func (r *Row) DisconnectSession() { - // No-op if no session. - if r.Session == nil { - return - } - - // Call the disconnect function from the controller first. - r.ctrl.OnSessionDisconnect(r) - - // Show visually that we're disconnected first by wiping all servers. - r.Reset() - - // Set the offline icon to the button. - r.Button.Image.SetPlaceholderIcon(IconName, 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.Button.SetNormalExtraMenu([]menu.Item{ - menu.SimpleItem("Connect", r.ReconnectSession), - menu.SimpleItem("Remove", r.RemoveSession), - }) - }, err - }) -} - -// KeyringSession returns a keyring session, or nil if the session cannot be -// saved. -func (r *Row) KeyringSession() *keyring.Session { - return keyring.ConvertSession(r.Session, r.Button.GetText()) -} - -// ID returns the session ID. -func (r *Row) ID() string { - return r.sessionID -} - -// SetFailed sets the initial connect status to failed. Do note that session can -// have 2 types of loading: loading the session and loading the server list. -// This one sets the former. -func (r *Row) SetFailed(err error) { - // SetFailed, but also add the callback to retry. - r.Row.SetFailed(err, r.ReconnectSession) -} - -// SetSession binds the session and marks the row as ready. It extends SetDone. -func (r *Row) SetSession(ses cchat.Session) { - r.Session = ses - r.sessionID = ses.ID() - r.SetLabelUnsafe(ses.Name()) - r.SetIconer(ses) - - // Set the commander, if any. The function will return nil if the assertion - // returns nil. As such, we assert with an ignored ok bool, allowing cmd to - // be nil. - cmd, _ := ses.(commander.SessionCommander) - r.cmder = commander.NewBuffer(r.ctrl.GetService(), cmd) - // Show the command button if the session actually supports the commander. - if r.cmder != nil { - r.cmdbtn.Show() - } - - // Bind extra menu items before loading. These items won't be clickable - // during loading. - r.SetNormalExtraMenu([]menu.Item{ - menu.SimpleItem("Disconnect", r.DisconnectSession), - menu.SimpleItem("Remove", r.RemoveSession), - }) - - // Preload now. - r.SetServerList(ses, r) - r.Load() -} - -func (r *Row) RowSelected(server *server.ServerRow, smsg cchat.ServerMessage) { - r.ctrl.RowSelected(r, server, smsg) -} - -// ShowCommander shows the commander dialog, or it does nothing if session does -// not implement commander. -func (r *Row) ShowCommander() { - if r.cmder == nil { - return - } - r.cmder.ShowDialog() -} -*/ diff --git a/internal/ui/service/view.go b/internal/ui/service/view.go index 3806706..20c2e55 100644 --- a/internal/ui/service/view.go +++ b/internal/ui/service/view.go @@ -8,22 +8,6 @@ import ( "github.com/gotk3/gotk3/gtk" ) -/* - -Design: - -____________________________ -| # | | | -|-----|-----------|--------| -| D | nixhub | | -| --- | #home | | <- shaded revealer -| O | #dev... | | <- user accounts collapsed -| --- | astolf... | | -| | asdada... | | -| M | | | -|_____|___________|________| -*/ - type Controller interface { // SessionSelected is called when SessionSelected(svc *Service, srow *session.Row) @@ -66,16 +50,17 @@ func NewView(ctrller Controller) *View { view.ServerStack.SetTransitionDuration(50) view.ServerStack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE) view.ServerStack.SetHomogeneous(true) + view.ServerStack.SetHExpand(true) view.ServerStack.Show() view.ServerView, _ = gtk.ScrolledWindowNew(nil, nil) view.ServerView.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + view.ServerView.SetHExpand(true) view.ServerView.Add(view.ServerStack) view.ServerView.Show() view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) view.Box.PackStart(view.Services, false, false, 0) - // view.Box.PackStart(sep, false, false, 0) view.Box.PackStart(view.ServerView, true, true, 0) view.Box.Show() diff --git a/internal/ui/ui.go b/internal/ui/ui.go index aa7f6ab..7b405fd 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -23,7 +23,7 @@ func init() { /* Make CSS more consistent across themes */ headerbar { padding-left: 0 } - .appmenu { margin: 0 18px } + /* .appmenu { margin: 0 20px } */ popover > *:not(stack):not(button) { margin: 6px } @@ -68,10 +68,16 @@ func NewApplication() *App { app.window = newWindow(app) app.header = newHeader() + // Resize the app icon with the left-most sidebar. + services := app.window.Services.Services + services.Connect("size-allocate", func() { + app.header.left.appmenu.SetSizeRequest(services.GetAllocatedWidth(), -1) + }) + // Resize the left-side header w/ the left-side pane. - app.window.Services.Connect("size-allocate", func(wv gtk.IWidget) { + app.window.Services.ServerView.Connect("size-allocate", func() { // Get the current width of the left sidebar. - var width = app.window.GetPosition() + width := app.window.GetPosition() // Set the left-side header's size. app.header.left.SetSizeRequest(width, -1) }) @@ -90,7 +96,7 @@ func (app *App) AddService(svc cchat.Service) { // OnSessionRemove resets things before the session is removed. func (app *App) OnSessionRemove(s *service.Service, r *session.Row) { // Reset the message view if it's what we're showing. - if app.window.MessageView.SessionID() == r.SessionID() { + if app.window.MessageView.SessionID() == r.ID() { app.window.MessageView.Reset() app.header.SetBreadcrumber(nil) } @@ -170,13 +176,9 @@ func (app *App) Window() gtk.IWidget { } func (app *App) Icon() *gdk.Pixbuf { - return icons.Logo256() + return icons.Logo256(0) } func (app *App) Menu() *glib.MenuModel { - menu := glib.MenuNew() - menu.Append("Preferences", "app.preferences") - menu.Append("Quit", "app.quit") - - return &menu.MenuModel + return &app.header.menu.MenuModel }