Added drag-and-drop; minor tweaks and bug fixes

This commit is contained in:
diamondburned 2020-07-16 17:21:14 -07:00 committed by diamondburned
parent cebc5f58b4
commit af3f3ec178
24 changed files with 471 additions and 713 deletions

4
go.mod
View File

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

4
go.sum
View File

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

View File

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

View File

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

View File

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

View File

@ -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 = `<span weight="light" rise="-1024" size="x-large">/</span>`
const BreadcrumbSlash = `<span rise="-1024" size="x-large">/</span>`
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,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
}
*/

View File

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

View File

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