Added drag-and-drop; minor tweaks and bug fixes
This commit is contained in:
parent
cebc5f58b4
commit
af3f3ec178
4
go.mod
4
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue