2020-05-28 19:26:55 +00:00
|
|
|
package primitives
|
|
|
|
|
2020-06-07 07:06:13 +00:00
|
|
|
import (
|
2020-08-17 00:13:47 +00:00
|
|
|
"runtime/debug"
|
2020-08-29 01:42:28 +00:00
|
|
|
"time"
|
2020-06-29 01:38:09 +00:00
|
|
|
|
2020-06-07 07:06:13 +00:00
|
|
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
2020-06-29 01:38:09 +00:00
|
|
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
2020-08-29 01:42:28 +00:00
|
|
|
"github.com/diamondburned/handy"
|
2020-06-07 07:06:13 +00:00
|
|
|
"github.com/gotk3/gotk3/gdk"
|
|
|
|
"github.com/gotk3/gotk3/glib"
|
|
|
|
"github.com/gotk3/gotk3/gtk"
|
2020-06-29 01:38:09 +00:00
|
|
|
"github.com/pkg/errors"
|
2020-06-07 07:06:13 +00:00
|
|
|
)
|
2020-05-28 19:26:55 +00:00
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
type Container interface {
|
|
|
|
Remove(gtk.IWidget)
|
|
|
|
GetChildren() *glib.List
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ Container = (*gtk.Container)(nil)
|
|
|
|
|
|
|
|
func RemoveChildren(w Container) {
|
|
|
|
w.GetChildren().Foreach(func(child interface{}) {
|
|
|
|
w.Remove(child.(gtk.IWidget))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
type Namer interface {
|
|
|
|
SetName(string)
|
|
|
|
GetName() (string, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
func GetName(namer Namer) string {
|
|
|
|
nm, _ := namer.GetName()
|
|
|
|
return nm
|
|
|
|
}
|
|
|
|
|
2020-07-17 00:21:14 +00:00
|
|
|
func EachChildren(w Container, fn func(i int, v interface{}) bool) {
|
2020-06-13 07:29:32 +00:00
|
|
|
var cursor int = -1
|
|
|
|
for ptr := w.GetChildren(); ptr != nil; ptr = ptr.Next() {
|
|
|
|
cursor++
|
|
|
|
|
|
|
|
if fn(cursor, ptr.Data()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-28 19:26:55 +00:00
|
|
|
type StyleContexter interface {
|
|
|
|
GetStyleContext() (*gtk.StyleContext, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
func AddClass(styleCtx StyleContexter, classes ...string) {
|
|
|
|
var style, _ = styleCtx.GetStyleContext()
|
|
|
|
for _, class := range classes {
|
|
|
|
style.AddClass(class)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
func RemoveClass(styleCtx StyleContexter, classes ...string) {
|
|
|
|
var style, _ = styleCtx.GetStyleContext()
|
|
|
|
for _, class := range classes {
|
|
|
|
style.RemoveClass(class)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-17 07:26:55 +00:00
|
|
|
type ClassEnum struct{ class string }
|
|
|
|
|
|
|
|
func (c *ClassEnum) SetClass(ctx StyleContexter, class string) {
|
|
|
|
var style, _ = ctx.GetStyleContext()
|
|
|
|
if c.class != "" {
|
|
|
|
style.RemoveClass(c.class)
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.class = class; class != "" {
|
|
|
|
style.AddClass(class)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-10 23:26:07 +00:00
|
|
|
type StyleContextFocuser interface {
|
|
|
|
StyleContexter
|
|
|
|
GrabFocus()
|
|
|
|
}
|
|
|
|
|
|
|
|
// SuggestAction styles the element to have the suggeested action class.
|
|
|
|
func SuggestAction(styleCtx StyleContextFocuser) {
|
|
|
|
AddClass(styleCtx, "suggested-action")
|
|
|
|
styleCtx.GrabFocus()
|
|
|
|
}
|
|
|
|
|
2020-05-28 19:26:55 +00:00
|
|
|
type Bin interface {
|
|
|
|
GetChild() (gtk.IWidget, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ Bin = (*gtk.Bin)(nil)
|
|
|
|
|
|
|
|
func BinLeftAlignLabel(bin Bin) {
|
|
|
|
widget, _ := bin.GetChild()
|
2020-06-04 23:00:41 +00:00
|
|
|
widget.(interface{ SetHAlign(gtk.Align) }).SetHAlign(gtk.ALIGN_START)
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewButtonIcon(icon string) *gtk.Image {
|
|
|
|
img, _ := gtk.ImageNewFromIconName(icon, gtk.ICON_SIZE_BUTTON)
|
|
|
|
return img
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewImageIconPx(icon string, sizepx int) *gtk.Image {
|
|
|
|
img, _ := gtk.ImageNew()
|
|
|
|
SetImageIcon(img, icon, sizepx)
|
|
|
|
return img
|
|
|
|
}
|
|
|
|
|
2020-08-17 00:13:47 +00:00
|
|
|
type ImageIconSetter interface {
|
|
|
|
SetProperty(name string, value interface{}) error
|
|
|
|
SetSizeRequest(w, h int)
|
|
|
|
}
|
|
|
|
|
|
|
|
func SetImageIcon(img ImageIconSetter, icon string, sizepx int) {
|
2020-08-29 01:42:28 +00:00
|
|
|
// Prioritize SetSize()
|
|
|
|
if setter, ok := img.(interface{ SetSize(int) }); ok {
|
|
|
|
setter.SetSize(sizepx)
|
|
|
|
} else {
|
|
|
|
img.SetProperty("pixel-size", sizepx)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prioritize SetIconName().
|
|
|
|
if setter, ok := img.(interface{ SetIconName(string) }); ok {
|
|
|
|
setter.SetIconName(icon)
|
|
|
|
} else {
|
|
|
|
img.SetProperty("icon-name", icon)
|
|
|
|
}
|
|
|
|
|
2020-06-04 23:00:41 +00:00
|
|
|
img.SetSizeRequest(sizepx, sizepx)
|
2020-05-28 19:26:55 +00:00
|
|
|
}
|
2020-06-07 04:27:28 +00:00
|
|
|
|
2020-06-14 18:19:06 +00:00
|
|
|
func PrependMenuItems(menu interface{ Prepend(gtk.IMenuItem) }, items []gtk.IMenuItem) {
|
|
|
|
for i := len(items) - 1; i >= 0; i-- {
|
|
|
|
menu.Prepend(items[i])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func AppendMenuItems(menu interface{ Append(gtk.IMenuItem) }, items []gtk.IMenuItem) {
|
2020-06-07 04:27:28 +00:00
|
|
|
for _, item := range items {
|
2020-06-07 07:06:13 +00:00
|
|
|
menu.Append(item)
|
2020-06-07 04:27:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
func HiddenMenuItem(label string, fn interface{}) *gtk.MenuItem {
|
2020-06-07 04:27:28 +00:00
|
|
|
mb, _ := gtk.MenuItemNewWithLabel(label)
|
|
|
|
mb.Connect("activate", fn)
|
|
|
|
return mb
|
|
|
|
}
|
2020-06-07 07:06:13 +00:00
|
|
|
|
2020-06-14 18:19:06 +00:00
|
|
|
func HiddenDisabledMenuItem(label string, fn interface{}) *gtk.MenuItem {
|
|
|
|
mb := HiddenMenuItem(label, fn)
|
|
|
|
mb.SetSensitive(false)
|
|
|
|
return mb
|
|
|
|
}
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
func MenuItem(label string, fn interface{}) *gtk.MenuItem {
|
2020-06-07 07:06:13 +00:00
|
|
|
menuitem := HiddenMenuItem(label, fn)
|
|
|
|
menuitem.Show()
|
|
|
|
return menuitem
|
|
|
|
}
|
|
|
|
|
|
|
|
type Connector interface {
|
|
|
|
Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error)
|
|
|
|
}
|
|
|
|
|
2020-06-14 18:19:06 +00:00
|
|
|
func BindMenu(connector Connector, menu *gtk.Menu) {
|
2020-06-17 07:06:34 +00:00
|
|
|
connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
|
2020-06-14 18:19:06 +00:00
|
|
|
if gts.EventIsRightClick(ev) {
|
|
|
|
menu.PopupAtPointer(ev)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func BindDynamicMenu(connector Connector, constr func(menu *gtk.Menu)) {
|
2020-06-17 07:06:34 +00:00
|
|
|
connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
|
2020-06-07 07:06:13 +00:00
|
|
|
if gts.EventIsRightClick(ev) {
|
2020-06-14 18:19:06 +00:00
|
|
|
menu, _ := gtk.MenuNew()
|
|
|
|
constr(menu)
|
2020-06-17 07:06:34 +00:00
|
|
|
|
|
|
|
// Only show the menu if the callback added any children into the
|
|
|
|
// list.
|
|
|
|
if menu.GetChildren().Length() > 0 {
|
|
|
|
menu.PopupAtPointer(ev)
|
|
|
|
}
|
2020-06-07 07:06:13 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2020-06-13 07:29:32 +00:00
|
|
|
|
2020-06-20 04:40:34 +00:00
|
|
|
// NewMenuActionButton is the same as NewActionButton, but it uses the
|
|
|
|
// open-menu-symbolic icon.
|
|
|
|
func NewMenuActionButton(actions [][2]string) *gtk.MenuButton {
|
|
|
|
return NewActionButton("open-menu-symbolic", actions)
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewActionButton creates a new menu button that spawns a popover with the
|
|
|
|
// listed actions.
|
|
|
|
func NewActionButton(iconName string, actions [][2]string) *gtk.MenuButton {
|
|
|
|
p, _ := gtk.PopoverNew(nil)
|
|
|
|
p.SetSizeRequest(200, -1) // wide enough width
|
|
|
|
ActionPopover(p, actions)
|
|
|
|
|
|
|
|
i, _ := gtk.ImageNew()
|
|
|
|
i.SetProperty("icon-name", iconName)
|
|
|
|
i.SetProperty("icon-size", gtk.ICON_SIZE_SMALL_TOOLBAR)
|
|
|
|
i.Show()
|
|
|
|
|
|
|
|
b, _ := gtk.MenuButtonNew()
|
|
|
|
b.SetHAlign(gtk.ALIGN_CENTER)
|
|
|
|
b.SetPopover(p)
|
|
|
|
b.Add(i)
|
|
|
|
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
|
|
|
// LabelTweaker is used for ActionPopover and other functions that may need to
|
|
|
|
// change the alignment of children widgets.
|
|
|
|
type LabelTweaker interface {
|
|
|
|
SetUseMarkup(bool)
|
|
|
|
SetHAlign(gtk.Align)
|
|
|
|
SetXAlign(float64)
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ LabelTweaker = (*gtk.Label)(nil)
|
|
|
|
|
|
|
|
func ActionPopover(p *gtk.Popover, actions [][2]string) {
|
|
|
|
var box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
|
|
|
|
|
|
|
|
for _, action := range actions {
|
|
|
|
b, _ := gtk.ModelButtonNew()
|
|
|
|
b.SetLabel(action[0])
|
|
|
|
b.SetActionName(action[1])
|
|
|
|
b.Show()
|
|
|
|
|
|
|
|
// Set the label's alignment in a hacky way.
|
|
|
|
c, _ := b.GetChild()
|
|
|
|
l := c.(LabelTweaker)
|
|
|
|
l.SetUseMarkup(true)
|
|
|
|
l.SetHAlign(gtk.ALIGN_START)
|
|
|
|
|
|
|
|
box.PackStart(b, false, true, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
box.Show()
|
|
|
|
p.Add(box)
|
|
|
|
}
|
2020-06-29 01:38:09 +00:00
|
|
|
|
2020-07-14 07:24:55 +00:00
|
|
|
func PrepareClassCSS(class, css string) (attach func(StyleContexter)) {
|
|
|
|
prov := PrepareCSS(css)
|
|
|
|
|
|
|
|
return func(ctx StyleContexter) {
|
|
|
|
s, _ := ctx.GetStyleContext()
|
|
|
|
s.AddProvider(prov, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
|
|
|
s.AddClass(class)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-29 01:38:09 +00:00
|
|
|
func PrepareCSS(css string) *gtk.CssProvider {
|
|
|
|
p, _ := gtk.CssProviderNew()
|
|
|
|
if err := p.LoadFromData(css); err != nil {
|
2020-08-17 00:13:47 +00:00
|
|
|
log.Error(errors.Wrapf(err, "CSS fail at %s", debug.Stack()))
|
2020-06-29 01:38:09 +00:00
|
|
|
}
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
|
2020-07-10 23:26:07 +00:00
|
|
|
func AttachCSS(ctx StyleContexter, prov *gtk.CssProvider) {
|
2020-06-29 01:38:09 +00:00
|
|
|
s, _ := ctx.GetStyleContext()
|
|
|
|
s.AddProvider(prov, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
|
|
|
}
|
2020-07-17 00:21:14 +00:00
|
|
|
|
|
|
|
func InlineCSS(ctx StyleContexter, css string) {
|
|
|
|
AttachCSS(ctx, PrepareCSS(css))
|
|
|
|
}
|
2020-08-29 01:42:28 +00:00
|
|
|
|
|
|
|
// LeafletOnFold binds a callback to a leaflet that would be called when the
|
|
|
|
// leaflet's folded state changes.
|
|
|
|
func LeafletOnFold(leaflet *handy.Leaflet, foldedFn func(folded bool)) {
|
|
|
|
var lastFold = leaflet.GetFolded()
|
|
|
|
foldedFn(lastFold)
|
|
|
|
|
|
|
|
// Give each callback a 500ms wait for animations to complete.
|
|
|
|
const dt = 500 * time.Millisecond
|
|
|
|
var last = time.Now()
|
|
|
|
|
|
|
|
leaflet.ConnectAfter("size-allocate", func() {
|
|
|
|
// Ignore if this event is too recent.
|
|
|
|
if now := time.Now(); now.Add(-dt).Before(last) {
|
|
|
|
return
|
|
|
|
} else {
|
|
|
|
last = now
|
|
|
|
}
|
|
|
|
|
|
|
|
if folded := leaflet.GetFolded(); folded != lastFold {
|
|
|
|
lastFold = folded
|
|
|
|
foldedFn(folded)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|