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
}