1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-gtk.git synced 2024-12-14 00:24:57 +00:00

update cchat-gtk to latest gotk3 to fix leaks

This commit is contained in:
diamondburned 2020-12-29 22:30:41 -08:00
parent e159b0d611
commit 50376cb2b0
48 changed files with 606 additions and 446 deletions

17
go.mod
View file

@ -2,22 +2,21 @@ module github.com/diamondburned/cchat-gtk
go 1.14
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20201225074909-7bf1378bcba4
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20201229104206-9bea3709a385
//replace github.com/diamondburned/cchat-discord => ../cchat-discord
//replace github.com/diamondburned/ningen/v2 => ../../ningen
//replace github.com/diamondburned/arikawa/v2 => ../../arikawa
// replace github.com/diamondburned/gotk3-tcmalloc => ../../gotk3-tcmalloc
// replace github.com/diamondburned/cchat-discord => ../cchat-discord
// replace github.com/diamondburned/ningen/v2 => ../../ningen
// replace github.com/diamondburned/arikawa/v2 => ../../arikawa
require (
github.com/Xuanwo/go-locale v1.0.0
github.com/alecthomas/chroma v0.7.3
github.com/diamondburned/cchat v0.3.15
github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af
github.com/diamondburned/cchat-discord v0.0.0-20201227035212-6beff5225092
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828
github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
github.com/disintegration/imaging v1.6.2
github.com/goodsign/monday v1.0.0

26
go.sum
View file

@ -52,6 +52,8 @@ github.com/diamondburned/arikawa/v2 v2.0.0-20201219075756-36c2f166becd h1:HCaw0Y
github.com/diamondburned/arikawa/v2 v2.0.0-20201219075756-36c2f166becd/go.mod h1:/vapSS3yfYRAt5hhgI6JiPkca+wKhgi0MdanT1dBBQY=
github.com/diamondburned/arikawa/v2 v2.0.0-20201220032235-088b30430377 h1:71BLnECSl0/Ns7iZmEm7MpE5+qSuWw/BQBQY2XCUmVc=
github.com/diamondburned/arikawa/v2 v2.0.0-20201220032235-088b30430377/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0=
github.com/diamondburned/arikawa/v2 v2.0.0-20201227001310-f3f075b27f44 h1:i6Jec7bvVY8NhwW3L0SlpfWM6r2p2i67XuhiOEzkfwI=
github.com/diamondburned/arikawa/v2 v2.0.0-20201227001310-f3f075b27f44/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0=
github.com/diamondburned/cchat v0.3.11 h1:C1f9Tp7Kz3t+T1SlepL1RS7b/kACAKWAIZXAgJEpCHg=
github.com/diamondburned/cchat v0.3.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.15 h1:BJf8ZiRtDWTGMtQ3QqjNU0H+784WSrkJEpFGkKY5gEw=
@ -60,6 +62,10 @@ github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d h1:n61
github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms=
github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af h1:pTdxsrVSYCdraGormbu1t8uQJMe/OD/ZIz9KljDWAvc=
github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms=
github.com/diamondburned/cchat-discord v0.0.0-20201227023505-c4e360010fb8 h1:eyK9GRaHg1KcWZx4hBPHWG16+EwgZi5groEW5I/FRq0=
github.com/diamondburned/cchat-discord v0.0.0-20201227023505-c4e360010fb8/go.mod h1:i3y8dyAFrtigpGOwunBdoJK/phwt9Gp/wfpVJb4imV0=
github.com/diamondburned/cchat-discord v0.0.0-20201227035212-6beff5225092 h1:oxY7APUclLgaWjaTK++7kHBdl0GdVyqOvHQv68TcpHw=
github.com/diamondburned/cchat-discord v0.0.0-20201227035212-6beff5225092/go.mod h1:rFBGZYLq0g6Pb/WGN/K0++kXrhCYlQQ1nc2FX4r8CO0=
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg=
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc=
github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw=
@ -70,16 +76,36 @@ github.com/diamondburned/gotk3 v0.0.0-20201221091325-c5152a10909f h1:Lnrq+vXBgzb
github.com/diamondburned/gotk3 v0.0.0-20201221091325-c5152a10909f/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/gotk3 v0.0.0-20201225074909-7bf1378bcba4 h1:KvlmpqxLoXKg+j5uiJWZXhacfgPg4fi/8wLecWX+XlE=
github.com/diamondburned/gotk3 v0.0.0-20201225074909-7bf1378bcba4/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/gotk3 v0.0.0-20201225090124-444dc90054da h1:ovty7leKv+E6PqocAeK+toLYnaMqbhZX69oPFNQK5Fk=
github.com/diamondburned/gotk3 v0.0.0-20201225090124-444dc90054da/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/gotk3 v0.0.0-20201226041445-1e6de5f7c2b2 h1:ExJxwhfSnIRJxL+CdcRZM33xoJ8WycoXGw6z3LaBFVM=
github.com/diamondburned/gotk3 v0.0.0-20201226041445-1e6de5f7c2b2/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/gotk3 v0.0.0-20201226085844-7ac29c072f03 h1:/azfkq4zFt8JPo8p2c/7kJJcf4cgTH1OaigR3fZHvIA=
github.com/diamondburned/gotk3 v0.0.0-20201226085844-7ac29c072f03/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/gotk3 v0.0.0-20201229054305-848200601f20 h1:/jSna2cSHrNXSzR9rNp9kUQRd/VOC/FL7vhRBor4zOc=
github.com/diamondburned/gotk3 v0.0.0-20201229054305-848200601f20/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/gotk3 v0.0.0-20201229092333-f5c9db5d1d59 h1:9nYQE9MZDcu++bFyyxKUQdin4ML+0PRoRm0bIUhjuBs=
github.com/diamondburned/gotk3 v0.0.0-20201229092333-f5c9db5d1d59/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/gotk3 v0.0.0-20201229104206-9bea3709a385 h1:nmlMCeEWmT6z9GAslxkTBZd9mH0Yt2QtWFD1yxGCJ98=
github.com/diamondburned/gotk3 v0.0.0-20201229104206-9bea3709a385/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894 h1:QgI21deaQbCUMnxKkQQUXzQolnAe1dMIXAWwqAyOp2g=
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894/go.mod h1:IoyMxPKSJOMoP0BiBuFwf2RDMeA4Uqx0HPKN5BzqTtA=
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828 h1:Lm1F+GwrDdAaaMzrR7AYl4GGd/T+FE2OgOz25QWwsIg=
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828/go.mod h1:ODW0Ai5dTVVp/HNUSSDTl5qE6K642CoOSZ8isIhupNg=
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4 h1:qF5VHC35+GyCjUmKz+1O94xpFc0JQd4Ui3h+I955pJw=
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4/go.mod h1:V0qyhW4v6KPFwtDpXdBm5aWH7zWEyrzZpcB6MPnKArQ=
github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374 h1:1KLPz5mbYF7t3ajrK55alkDcbjWc+7aQFjKzV9qJRN4=
github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374/go.mod h1:9EiMOAKWhEFoVnFLTco2v0v0rJn855YAHDDa2bL8urU=
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 h1:OWxllHbUptXzDias6YI4MM0R3o50q8MfhkkwVIlfiNo=
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
github.com/diamondburned/ningen/v2 v2.0.0-20201219070301-15610044db9a h1:w8CWPYiwH9p2XGlHHeTqRWx7e8CJJLN8i4orAkOa27Y=
github.com/diamondburned/ningen/v2 v2.0.0-20201219070301-15610044db9a/go.mod h1:Pw4ZPQmZUonCytlKhHgan98CZeCQ4AWh0DWqvnhsuNE=
github.com/diamondburned/ningen/v2 v2.0.0-20201220054153-c69c4f7057b4 h1:qzh5ghfgvUllilOhkrGP29IGQT6DGfcc3lhk9uSA6nU=
github.com/diamondburned/ningen/v2 v2.0.0-20201220054153-c69c4f7057b4/go.mod h1:2ZjyeHqO9jCdlfmJhhVhk8eCumx418n39uVaC/LgEgY=
github.com/diamondburned/ningen/v2 v2.0.0-20201227020621-a4e33db11d3c h1:IWn/N54JkJz7PVgmAt7cWXLky9wY0UjXzznNoWLIFgA=
github.com/diamondburned/ningen/v2 v2.0.0-20201227020621-a4e33db11d3c/go.mod h1:zQkAo1RT4ru4HW6B5T4IRO2pee8ITzTUA2Y7XNpgjqo=
github.com/diamondburned/ningen/v2 v2.0.0-20201227034843-dc1d22fc28e4 h1:ptIpcyB/FIBM5viKLVXuBiE1utqBcH4S3l5kUQrsL9Q=
github.com/diamondburned/ningen/v2 v2.0.0-20201227034843-dc1d22fc28e4/go.mod h1:zQkAo1RT4ru4HW6B5T4IRO2pee8ITzTUA2Y7XNpgjqo=
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

@ -3,28 +3,43 @@ package icons
import (
"log"
"github.com/gotk3/gotk3/cairo"
"github.com/gotk3/gotk3/gdk"
)
// static assets
var assets = map[string]*gdk.Pixbuf{}
// var assets = map[string]*gdk.Pixbuf{}
func Logo256Variant2(sz int) *gdk.Pixbuf {
return loadPixbuf(__cchat_variant2_256, sz)
func Logo256Variant2(sz, scale int) *cairo.Surface {
return mustSurface(loadPixbuf(__cchat_variant2_256, sz, scale), scale)
}
func Logo256(sz int) *gdk.Pixbuf {
return loadPixbuf(__cchat_256, sz)
func Logo256(sz, scale int) *cairo.Surface {
return mustSurface(loadPixbuf(__cchat_256, sz, scale), scale)
}
func loadPixbuf(data []byte, sz int) *gdk.Pixbuf {
func Logo256Pixbuf() *gdk.Pixbuf {
return loadPixbuf(__cchat_256, 256, 1)
}
func mustSurface(p *gdk.Pixbuf, scale int) *cairo.Surface {
surface, err := gdk.CairoSurfaceCreateFromPixbuf(p, scale, nil)
if err != nil {
log.Fatalln("Failed to create surface from pixbuf:", err)
}
return surface
}
func loadPixbuf(data []byte, sz, scale 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) })
l.Connect("size-prepared", func(l *gdk.PixbufLoader) {
l.SetSize(sz*scale, sz*scale)
})
}
p, err := l.WriteAndReturnPixbuf(data)

View file

@ -1,15 +1,12 @@
package gts
import (
"fmt"
"image"
"os"
"time"
"github.com/diamondburned/cchat-gtk/internal/gts/throttler"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/handy"
"github.com/disintegration/imaging"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
@ -66,15 +63,14 @@ func NewEmptyModalDialog() (*gtk.Dialog, error) {
if err != nil {
return nil, errors.Wrap(err, "Failed to get content area")
}
d.Remove(b)
b.Destroy()
return d, nil
}
func AddAppAction(name string, call func()) {
action := glib.SimpleActionNew(name, nil)
action.Connect("activate", call)
action.Connect("activate", func(*glib.SimpleAction) { call() })
App.AddAction(action)
}
@ -90,12 +86,12 @@ func init() {
type MainApplication interface {
gtk.IWidget
Menu() *glib.MenuModel
Icon() *gdk.Pixbuf
Icon() *gdk.Pixbuf // assume scale 1
Close()
}
func Main(wfn func() MainApplication) {
App.Application.Connect("activate", func() {
App.Application.Connect("activate", func(*gtk.Application) {
handy.Init()
// Load all CSS onto the default screen.
@ -119,17 +115,16 @@ func Main(wfn func() MainApplication) {
w := wfn()
App.Window.Add(w)
App.Window.SetIcon(w.Icon())
// App.Application.SetAppMenu(w.Menu())
// Connect the destructor.
App.Window.Window.Connect("destroy", func() {
App.Window.Window.Connect("destroy", func(window *handy.ApplicationWindow) {
// Hide the application window.
App.Window.Hide()
window.Hide()
// Let the main loop run once by queueing the stop loop afterwards.
// This is to allow the main loop to properly hide the Gtk window
// before trying to disconnect.
ExecAsync(func() {
ExecLater(func() {
// Stop the application loop.
App.Application.Quit()
// Finalize the application by running the closer.
@ -164,9 +159,14 @@ func Async(fn func() (func(), error)) {
}()
}
// ExecLater executes the function asynchronously with a low priority.
func ExecLater(fn func()) {
glib.IdleAddPriority(glib.PRIORITY_LOW, fn)
}
// ExecAsync executes function asynchronously in the Gtk main thread.
func ExecAsync(fn func()) {
glib.IdleAdd(fn)
glib.IdleAddPriority(glib.PRIORITY_HIGH, fn)
}
// ExecSync executes the function asynchronously, but returns a channel that
@ -174,7 +174,7 @@ func ExecAsync(fn func()) {
func ExecSync(fn func()) <-chan struct{} {
var ch = make(chan struct{})
glib.IdleAdd(func() {
glib.IdleAddPriority(glib.PRIORITY_HIGH, func() {
fn()
close(ch)
})
@ -189,10 +189,7 @@ func DoAfter(d time.Duration, f func()) {
// DoAfterMs calls f after the given ms in the Gtk main loop.
func DoAfterMs(ms uint, f func()) {
_, err := glib.TimeoutAdd(ms, f)
if err != nil {
panic(err)
}
glib.TimeoutAddPriority(ms, glib.PRIORITY_HIGH_IDLE, f)
}
// AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk
@ -203,11 +200,7 @@ func AfterFunc(d time.Duration, f func()) (stop func()) {
// AfterMsFunc is similar to AfterFunc but takes in milliseconds instead.
func AfterMsFunc(ms uint, f func()) (stop func()) {
h, err := glib.TimeoutAdd(ms, func() bool { f(); return true })
if err != nil {
panic(err)
}
h := glib.TimeoutAddPriority(ms, glib.PRIORITY_HIGH_IDLE, func() bool { f(); return true })
return func() { glib.SourceRemove(h) }
}
@ -216,30 +209,6 @@ func EventIsRightClick(ev *gdk.Event) bool {
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
}
func RenderPixbuf(img image.Image) *gdk.Pixbuf {
var nrgba *image.NRGBA
if n, ok := img.(*image.NRGBA); ok {
nrgba = n
} else {
nrgba = imaging.Clone(img)
}
pix, err := gdk.PixbufNewFromData(
nrgba.Pix, gdk.COLORSPACE_RGB,
true, // NRGBA has alpha.
8, // 8-bit aka 1-byte per sample.
nrgba.Rect.Dx(),
nrgba.Rect.Dy(), // We already know the image size.
nrgba.Stride,
)
if err != nil {
panic(fmt.Sprintf("Failed to create pixbuf from *NRGBA: %v", err))
}
return pix
}
func SpawnUploader(dirpath string, callback func(absolutePaths []string)) {
dialog, _ := gtk.FileChooserNativeDialogNew(
"Upload File", App.Window,
@ -276,7 +245,7 @@ func BindPreviewer(fc *gtk.FileChooserNativeDialog) {
fc.SetPreviewWidget(img)
fc.Connect("update-preview",
func(_ interface{}, img *gtk.Image) {
func(fc *gtk.FileChooserNativeDialog) {
file := fc.GetPreviewFilename()
b, err := gdk.PixbufNewFromFileAtScale(file, 256, 256, true)
@ -288,6 +257,5 @@ func BindPreviewer(fc *gtk.FileChooserNativeDialog) {
img.SetFromPixbuf(b)
fc.SetPreviewWidgetActive(true)
},
img,
)
}

View file

@ -2,20 +2,18 @@ package httputil
import (
"context"
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/gregjones/httpcache"
"github.com/gregjones/httpcache/diskcache"
"github.com/peterbourgon/diskv"
"github.com/pkg/errors"
)
var basePath = filepath.Join(os.TempDir(), "cchat-gtk-sabotaging-the-desktop-experience")
var basePath = filepath.Join(os.TempDir(), "cchat-gtk-totally-not-node-modules")
var dskcached = http.Client{
Timeout: 15 * time.Second,
@ -25,40 +23,12 @@ var dskcached = http.Client{
TempDir: filepath.Join(basePath, "tmp"),
PathPerm: 0750,
FilePerm: 0750,
Compression: diskv.NewZlibCompressionLevel(2),
CacheSizeMax: 25 * 1024 * 1024, // 25 MiB in memory
Compression: diskv.NewZlibCompressionLevel(5),
CacheSizeMax: 0, // 25 MiB in memory
})),
),
}
func AsyncStreamUncached(url string, fn func(r io.Reader)) {
gts.Async(func() (func(), error) {
r, err := get(context.Background(), url, false)
if err != nil {
return nil, err
}
return func() {
fn(r.Body)
r.Body.Close()
}, nil
})
}
func AsyncStream(url string, fn func(r io.Reader)) {
gts.Async(func() (func(), error) {
r, err := get(context.Background(), url, true)
if err != nil {
return nil, err
}
return func() {
fn(r.Body)
r.Body.Close()
}, nil
})
}
func get(ctx context.Context, url string, cached bool) (r *http.Response, err error) {
q, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {

View file

@ -3,6 +3,10 @@ package httputil
import (
"context"
"io"
"mime"
"net/http"
"net/url"
"path"
"strings"
"github.com/diamondburned/cchat-gtk/internal/gts"
@ -41,7 +45,7 @@ type surfaceWrapper struct {
scale int
}
func (wrapper surfaceWrapper) SetFromPixbuf(pb *gdk.Pixbuf) {
func (wrapper *surfaceWrapper) SetFromPixbuf(pb *gdk.Pixbuf) {
surface, _ := gdk.CairoSurfaceCreateFromPixbuf(pb, wrapper.scale, nil)
wrapper.SetFromSurface(surface)
}
@ -49,98 +53,149 @@ func (wrapper surfaceWrapper) SetFromPixbuf(pb *gdk.Pixbuf) {
// AsyncImage loads an image. This method uses the cache. It prefers loading
// SetFromSurface over SetFromPixbuf, but will fallback if needed be.
func AsyncImage(ctx context.Context,
img ImageContainer, url string, procs ...imgutil.Processor) {
img ImageContainer, imageURL string, procs ...imgutil.Processor) {
if url == "" {
return
}
gif := strings.Contains(url, ".gif")
scale := 1
surfaceContainer, canSurface := img.(SurfaceContainer)
if canSurface = canSurface && !gif; canSurface {
// Only bother with this API if we even have HiDPI.
if scale = surfaceContainer.GetScaleFactor(); scale > 1 {
img = surfaceWrapper{surfaceContainer, scale}
}
}
ctx = primitives.HandleDestroyCtx(ctx, img)
l, err := gdk.PixbufLoaderNew()
if err != nil {
log.Error(errors.Wrap(err, "Failed to make pixbuf loader"))
if imageURL == "" {
return
}
w, h := img.GetSizeRequest()
l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) {
w, h = imgutil.MaxSize(imgW, imgH, w, h)
if w != imgW || h != imgH || scale > 1 {
l.SetSize(w*scale, h*scale)
scale := 1
surfaceContainer, canSurface := img.(SurfaceContainer)
if canSurface {
scale = surfaceContainer.GetScaleFactor()
}
go func() {
ctx := primitives.HandleDestroyCtx(ctx, img)
// Try and guess the MIME type from the URL.
mimeType := mime.TypeByExtension(urlExt(imageURL))
r, err := get(ctx, imageURL, true)
if err != nil {
log.Error(errors.Wrap(err, "failed to GET"))
return
}
})
defer r.Body.Close()
l.Connect("area-prepared", areaPreparedFn(ctx, img, gif))
// Try and use the image type from the MIME header over the type from
// the URL, as it is more reliable.
if mime := mimeFromHeaders(r.Header); mime != "" {
mimeType = mime
}
go downloadImage(ctx, l, url, procs, gif)
_, fileType := path.Split(mimeType) // abuse split "a/b" to get b
isGIF := fileType == "gif"
if isGIF {
canSurface = false
scale = 1
}
// Only bother with this if we even have HiDPI. We also can't use a
// Surface for a GIF.
if canSurface && scale > 1 {
img = &surfaceWrapper{surfaceContainer, scale}
}
l, err := gdk.PixbufLoaderNewWithType(fileType)
if err != nil {
log.Error(errors.Wrap(err, "failed to make pixbuf loader"))
return
}
l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) {
w, h = imgutil.MaxSize(imgW, imgH, w, h)
if w != imgW || h != imgH || scale > 1 {
l.SetSize(w*scale, h*scale)
}
})
load := loadFn(ctx, img, isGIF)
l.Connect("area-prepared", load)
l.Connect("area-updated", load)
if err := downloadImage(r.Body, l, procs, isGIF); err != nil {
log.Error(errors.Wrapf(err, "failed to download %q", imageURL))
// Force close after downloading.
}
if err := l.Close(); err != nil {
log.Error(errors.Wrapf(err, "failed to close pixbuf loader for %q", imageURL))
}
}()
}
func areaPreparedFn(ctx context.Context, img ImageContainer, gif bool) func(l *gdk.PixbufLoader) {
func urlExt(anyURL string) string {
u, err := url.Parse(anyURL)
if err != nil {
return path.Ext(strings.SplitN(anyURL, "?", 1)[0])
}
return path.Ext(u.Path)
}
func mimeFromHeaders(headers http.Header) string {
cType := headers.Get("Content-Type")
if cType == "" {
return ""
}
media, _, err := mime.ParseMediaType(cType)
if err != nil {
return ""
}
return media
}
func loadFn(ctx context.Context, img ImageContainer, isGIF bool) func(l *gdk.PixbufLoader) {
var pixbuf interface{}
return func(l *gdk.PixbufLoader) {
if !gif {
p, err := l.GetPixbuf()
if err != nil {
log.Error(errors.Wrap(err, "Failed to get pixbuf"))
return
if pixbuf == nil {
if !isGIF {
pixbuf, _ = l.GetPixbuf()
} else {
pixbuf, _ = l.GetAnimation()
}
execIfCtx(ctx, func() { img.SetFromPixbuf(p) })
} else {
p, err := l.GetAnimation()
if err != nil {
log.Error(errors.Wrap(err, "Failed to get animation"))
return
}
execIfCtx(ctx, func() { img.SetFromAnimation(p) })
}
switch pixbuf := pixbuf.(type) {
case *gdk.Pixbuf:
execIfCtx(ctx, func() { img.SetFromPixbuf(pixbuf) })
case *gdk.PixbufAnimation:
execIfCtx(ctx, func() { img.SetFromAnimation(pixbuf) })
}
}
}
func execIfCtx(ctx context.Context, fn func()) {
gts.ExecAsync(func() {
gts.ExecLater(func() {
if ctx.Err() == nil {
fn()
}
})
}
func downloadImage(ctx context.Context, dst io.WriteCloser, url string, p []imgutil.Processor, gif bool) {
// Close at the end when done.
defer dst.Close()
r, err := get(ctx, url, true)
if err != nil {
log.Error(err)
return
}
defer r.Body.Close()
func downloadImage(src io.Reader, dst io.Writer, p []imgutil.Processor, isGIF bool) error {
var err error
// If we have processors, then write directly in there.
if len(p) > 0 {
if !gif {
err = imgutil.ProcessStream(dst, r.Body, p)
if !isGIF {
err = imgutil.ProcessStream(dst, src, p)
} else {
err = imgutil.ProcessAnimationStream(dst, r.Body, p)
err = imgutil.ProcessAnimationStream(dst, src, p)
}
} else {
// Else, directly copy the body over.
_, err = io.Copy(dst, r.Body)
_, err = io.Copy(dst, src)
}
if err != nil {
log.Error(errors.Wrap(err, "Error processing image"))
return
return errors.Wrap(err, "failed to process image")
}
return nil
}

View file

@ -16,7 +16,7 @@ type State struct {
}
type Connector interface {
Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error)
Connect(string, interface{}) glib.SignalHandle
}
func Bind(app *gtk.Application) *State {
@ -34,8 +34,8 @@ func Bind(app *gtk.Application) *State {
}
func (s *State) Connect(c Connector) {
c.Connect("focus-out-event", s.Start)
c.Connect("focus-in-event", s.Stop)
c.Connect("focus-out-event", func(interface{}) { s.Start() })
c.Connect("focus-in-event", func(interface{}) { s.Stop() })
}
func (s *State) Start() {

View file

@ -81,7 +81,7 @@ func NewPreferenceDialog() *Dialog {
func SpawnPreferenceDialog() {
p := NewPreferenceDialog()
p.Connect("destroy", func() {
p.Connect("destroy", func(interface{}) {
// On close, save the settings.
if err := config.Save(); err != nil {
log.Error(errors.Wrap(err, "Failed to save settings"))

View file

@ -36,7 +36,7 @@ func (c *_combo) Construct() gtk.IWidget {
combo.Append(opt, opt)
}
combo.Connect("changed", func() { c.set(combo.GetActive()) })
combo.Connect("changed", func(combo *gtk.ComboBoxText) { c.set(combo.GetActive()) })
combo.SetActive(*c.selected)
combo.SetHAlign(gtk.ALIGN_END)
combo.Show()
@ -76,7 +76,7 @@ func (s *_switch) set(v bool) {
func (s *_switch) Construct() gtk.IWidget {
sw, _ := gtk.SwitchNew()
sw.SetActive(*s.value)
sw.Connect("notify::active", func() { s.set(sw.GetActive()) })
sw.Connect("notify::active", func(sw *gtk.Switch) { s.set(sw.GetActive()) })
sw.SetHAlign(gtk.ALIGN_END)
sw.Show()
@ -118,7 +118,7 @@ func (e *_inputentry) Construct() gtk.IWidget {
entry.SetHExpand(true)
entry.SetText(*e.value)
entry.Connect("changed", func() {
entry.Connect("changed", func(entry *gtk.Entry) {
v, err := entry.GetText()
if err != nil {
return

View file

@ -53,8 +53,8 @@ func NewModal(body gtk.IWidget, title, button string, clicked func(m *Modal)) *M
header,
}
cancel.Connect("clicked", dialog.Destroy)
action.Connect("clicked", func() { clicked(modald) })
cancel.Connect("clicked", func(interface{}) { dialog.Destroy() })
action.Connect("clicked", func(interface{}) { clicked(modald) })
return modald
}
@ -78,7 +78,7 @@ func newCSD(body, header gtk.IWidget) *gtk.Dialog {
dialog.Add(body)
if oldh, _ := dialog.GetHeaderBar(); oldh != nil {
dialog.Remove(oldh)
oldh.ToWidget().Destroy()
}
dialog.SetTitlebar(header)

View file

@ -66,9 +66,9 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
avatar := NewAvatar()
avatar.SetMarginTop(TopFullMargin)
avatar.SetMarginStart(container.ColumnSpacing * 2)
avatar.Connect("clicked", func() {
avatar.Connect("clicked", func(w gtk.IWidget) {
if output := gc.Username.Output(); len(output.Mentions) > 0 {
labeluri.PopoverMentioner(avatar, output.Input, output.Mentions[0])
labeluri.PopoverMentioner(w, output.Input, output.Mentions[0])
}
})
// We don't call avatar.Show(). That's called in Attach.

View file

@ -95,12 +95,12 @@ func (h *Header) Reset() {
}
func (h *Header) OnBackPressed(fn func()) {
h.BackButton.Connect("clicked", fn)
h.BackButton.Connect("clicked", func(*gtk.Button) { fn() })
}
func (h *Header) OnShowMembersToggle(fn func(show bool)) {
h.ShowMembers.Connect("toggled", func() {
fn(h.ShowMembers.GetActive())
h.ShowMembers.Connect("toggled", func(showMembers *gtk.ToggleButton) {
fn(showMembers.GetActive())
})
}

View file

@ -1,32 +1,23 @@
package attachment
import (
"bytes"
"fmt"
"image"
"image/png"
"io"
"io/ioutil"
"mime"
"os"
"path/filepath"
"strings"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
"github.com/disintegration/imaging"
"github.com/gotk3/gotk3/cairo"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
var pngEncoder = png.Encoder{
CompressionLevel: png.BestCompression,
}
const (
ThumbSize = 72
IconSize = 56
@ -37,7 +28,7 @@ const (
type File struct {
Prog *Progress
Name string
Size int64
Size int64 // -1 = stream
}
// NewFile creates a new attachment file with a progress state.
@ -70,7 +61,7 @@ type Container struct {
// states
files []File
items map[string]gtk.IWidget
items map[string]primitives.WidgetDestroyer
}
var attachmentsCSS = primitives.PrepareCSS(`
@ -120,7 +111,7 @@ func New() *Container {
Revealer: rev,
Scroll: scr,
Box: box,
items: map[string]gtk.IWidget{},
items: map[string]primitives.WidgetDestroyer{},
}
}
@ -155,11 +146,11 @@ func (c *Container) Reset() {
// Clear all items.
for _, item := range c.items {
c.Box.Remove(item)
item.Destroy()
}
// Reset the map.
c.items = map[string]gtk.IWidget{}
c.items = map[string]primitives.WidgetDestroyer{}
// Hide the window.
c.SetRevealChild(false)
@ -187,61 +178,31 @@ func (c *Container) AddFile(path string) error {
func() (io.ReadCloser, error) { return os.Open(path) },
)
scale := c.GetScaleFactor()
// Maybe try making a preview. A nil image is fine, so we can skip the error
// check.
// TODO: add a filesize check
i, _ := imaging.Open(path, imaging.AutoOrientation(true))
c.addPreview(filename, i)
pixbuf, _ := gdk.PixbufNewFromFileAtScale(path, ThumbSize*scale, ThumbSize*scale, true)
c.addPreview(filename, thumbnailPixbuf(pixbuf, scale))
return nil
}
// AddPixbuf is used for adding pixbufs from the clipboard.
func (c *Container) AddPixbuf(pb *gdk.Pixbuf) error {
// Pixbuf's colorspace is only RGB. This is indicated with
// GDK_COLORSPACE_RGB.
if pb.GetColorspace() != gdk.COLORSPACE_RGB {
return errors.New("Pixbuf has unsupported colorspace")
}
// Assert that the pixbuf has alpha, as we're using RGBA.
if !pb.GetHasAlpha() {
return errors.New("Pixbuf has no alpha channel")
}
// Assert that there are 4 channels: red, green, blue and alpha.
if pb.GetNChannels() != 4 {
return errors.New("Pixbuf has unexpected channel count")
}
// Assert that there are 8 bits in a channel/sample.
if pb.GetBitsPerSample() != 8 {
return errors.New("Pixbuf has unexpected bits per sample")
}
var img = &image.NRGBA{
Pix: pb.GetPixels(),
Stride: pb.GetRowstride(),
Rect: image.Rect(0, 0, pb.GetWidth(), pb.GetHeight()),
}
// Store the image in memory.
var buf bytes.Buffer
if err := pngEncoder.Encode(&buf, img); err != nil {
return errors.Wrap(err, "Failed to encode PNG")
}
func (c *Container) AddPixbuf(pb *gdk.Pixbuf) {
var filename = c.append(
fmt.Sprintf("clipboard_%d.png", len(c.files)+1), int64(buf.Len()),
fmt.Sprintf("clipboard_%d.png", len(c.files)+1), -1,
func() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil
r, w := io.Pipe()
go func() { w.CloseWithError(pb.WritePNG(w, 9)) }()
return r, nil
},
)
c.addPreview(filename, img)
scale := c.GetScaleFactor()
return nil
c.addPreview(filename, thumbnailPixbuf(pb, scale))
return
}
// -- internal methods --
@ -273,7 +234,7 @@ func (c *Container) remove(name string) {
}
if w, ok := c.items[name]; ok {
c.Box.Remove(w)
w.Destroy()
delete(c.items, name)
}
@ -309,7 +270,7 @@ var deleteAttBtnCSS = primitives.PrepareCSS(`
}
`)
func (c *Container) addPreview(name string, src image.Image) {
func (c *Container) addPreview(name string, thumbnail *cairo.Surface) {
// Make a fallback image first.
gimg, _ := roundimage.NewImage(4) // border-radius: 4px
primitives.SetImageIcon(gimg.Image, iconFromName(name), IconSize)
@ -322,19 +283,8 @@ func (c *Container) addPreview(name string, src image.Image) {
primitives.AttachCSS(gimg, previewCSS)
// Determine if we could generate an image preview.
if src != nil {
// Get the minimum dimension.
var w, h = minsize(src.Bounds().Dx(), src.Bounds().Dy(), ThumbSize)
var img *image.NRGBA
// Downscale the image.
img = imaging.Resize(src, w, h, imaging.Lanczos)
// Crop to a square.
img = imaging.CropCenter(img, ThumbSize, ThumbSize)
// Copy the image to a pixbuf.
gimg.SetFromPixbuf(gts.RenderPixbuf(img))
if thumbnail != nil {
gimg.SetFromSurface(thumbnail)
}
// BLOAT!!! Make an overlay of an event box that, when hovered, will show
@ -343,7 +293,7 @@ func (c *Container) addPreview(name string, src image.Image) {
del.SetVAlign(gtk.ALIGN_CENTER)
del.SetHAlign(gtk.ALIGN_CENTER)
del.SetTooltipText("Remove " + name)
del.Connect("clicked", func() { c.remove(name) })
del.Connect("clicked", func(del *gtk.Button) { c.remove(name) })
del.Show()
primitives.AddClass(del, "delete-attachment")
primitives.AttachCSS(del, deleteAttBtnCSS)
@ -358,6 +308,58 @@ func (c *Container) addPreview(name string, src image.Image) {
c.Box.PackStart(ovl, false, false, 0)
}
func thumbnailPixbuf(pixbuf *gdk.Pixbuf, scale int) *cairo.Surface {
if pixbuf == nil {
return nil
}
var (
originalWidth = pixbuf.GetWidth()
originalHeight = pixbuf.GetHeight()
scaledThumbSize = ThumbSize * scale
scaledWidth, scaledHeight = minsize(originalWidth, originalHeight, scaledThumbSize)
// offset of src on thumbnail; one of those will be 0
offsetX = float64(scaledThumbSize-scaledWidth) / 2
offsetY = float64(scaledThumbSize-scaledHeight) / 2
)
thumbnail, err := gdk.PixbufNew(
pixbuf.GetColorspace(),
true, 8, // always have alpha, 8bpc
scaledThumbSize, scaledThumbSize,
)
if err != nil {
panic("failed to allocate upload thumbnail pixbuf: " + err.Error())
}
// Fill with transparent pixels.
thumbnail.Fill(0x0)
pixbuf.Scale(
thumbnail,
int(offsetX), int(offsetY),
// size of src on thumbnail
scaledWidth, scaledHeight,
// no offset on source image
offsetX, offsetY,
// scale ratio for both sides
float64(scaledWidth)/float64(originalWidth),
float64(scaledHeight)/float64(originalHeight),
// expensive rescale algorithm
gdk.INTERP_HYPER,
)
surface, err := gdk.CairoSurfaceCreateFromPixbuf(thumbnail, scale, nil)
if err != nil {
panic("failed to create thumbnail cairo surface: " + err.Error())
}
return surface
}
func iconFromName(filename string) string {
switch t := mime.TypeByExtension(filepath.Ext(filename)); {
case strings.HasPrefix(t, "image"):
@ -376,8 +378,9 @@ func iconFromName(filename string) string {
}
}
// minsize returns the scaled size so that the largest edge is maxsz.
func minsize(w, h, maxsz int) (int, int) {
if w < h {
if w > h {
// return the scaled width as max
// h*max/w is the same as h/w*max but with more accuracy
return maxsz, h * maxsz / w
@ -385,3 +388,17 @@ func minsize(w, h, maxsz int) (int, int) {
return w * maxsz / h, maxsz
}
func min(w, h int) int {
if w > h {
return h
}
return w
}
func max(w, h int) int {
if w > h {
return w
}
return h
}

View file

@ -6,6 +6,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
)
@ -47,7 +48,12 @@ func NewProgressBar(file File) *ProgressBar {
bar.SetVAlign(gtk.ALIGN_CENTER)
bar.Show()
name, _ := gtk.LabelNew(file.Name)
var label = file.Name
if file.Size > 0 {
label += " - " + glib.FormatSize(uint64(file.Size))
}
name, _ := gtk.LabelNew(label)
name.SetMaxWidthChars(45)
name.SetSingleLineMode(true)
name.SetEllipsize(pango.ELLIPSIZE_MIDDLE)

View file

@ -196,16 +196,23 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
// Bind text events.
text.Connect("key-press-event", field.keyDown)
// Bind the send button.
field.send.Connect("clicked", field.sendInput)
field.send.Connect("clicked", func(*gtk.Button) { field.sendInput() })
// Bind the attach button.
field.attach.Connect("clicked", func() { gts.SpawnUploader("", field.Attachments.AddFiles) })
field.attach.Connect("clicked", func(attach *gtk.Button) {
gts.SpawnUploader("", field.Attachments.AddFiles)
})
// allocatedWidthGetter is used below.
type allocatedWidthGetter interface {
GetAllocatedWidth() int
}
// Connect to the field's revealer. On resize, we want the attachments
// carousel to have the same padding too.
field.Username.Connect("size-allocate", func(w gtk.IWidget) {
field.Username.Connect("size-allocate", func(w allocatedWidthGetter) {
// Calculate the left width: from the left of the message box to the
// right of the attach button, covering the username container.
var leftWidth = 5 + field.attach.GetAllocatedWidth() + w.ToWidget().GetAllocatedWidth()
var leftWidth = 5 + field.attach.GetAllocatedWidth() + w.GetAllocatedWidth()
// Set the autocompleter's left margin to be the same.
field.Attachments.SetMarginStart(leftWidth)
})

View file

@ -84,23 +84,23 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
return false
}
// TODO: make this asynchronous.
// Is there an image in the clipboard?
if !gts.Clipboard.WaitIsImageAvailable() {
// No.
return false
}
// Yes.
p, err := gts.Clipboard.WaitForImage()
if err != nil {
log.Error(errors.Wrap(err, "Failed to get image from clipboard"))
return true // interrupt as technically valid
}
gts.Async(func() (func(), error) {
p, err := gts.Clipboard.WaitForImage()
if err != nil {
return nil, errors.Wrap(err, "Failed to get image from clipboard")
}
if err := f.Attachments.AddPixbuf(p); err != nil {
log.Error(errors.Wrap(err, "Failed to add image to attachment list"))
return true
}
return func() { f.Attachments.AddPixbuf(p) }, nil
})
return true
}
// If the server supports typing indication, then announce that we are

View file

@ -87,7 +87,7 @@ func (c *Container) Reset() {
c.Revealer.SetRevealChild(false)
for _, section := range c.Sections {
c.Main.Remove(section)
section.Destroy()
}
c.Sections = map[string]*Section{}
@ -276,7 +276,7 @@ func (s *Section) RemoveMember(id string) {
}
}
func listSortNameAsc(r1, r2 *gtk.ListBoxRow, _ ...interface{}) int {
func listSortNameAsc(r1, r2 *gtk.ListBoxRow) int {
n1, _ := r1.GetName()
n2, _ := r2.GetName()
@ -400,7 +400,7 @@ func (m *Member) Popup(evq EventQueuer) {
// Unbounded concurrency is kind of bad. We should deal with
// this in the future.
evq.Activate()
p.Connect("closed", evq.Deactivate)
p.Connect("closed", func(interface{}) { evq.Deactivate() })
p.SetPosition(gtk.POS_LEFT)
p.Popup()

View file

@ -58,7 +58,7 @@ func New() *Container {
})
// On label destroy, stop the state loop as well.
l.Connect("destroy", state.stopper)
l.Connect("destroy", func(interface{}) { state.stopper() })
return &Container{
Revealer: r,

View file

@ -155,7 +155,8 @@ func NewView(c Controller) *View {
drag.BindFileDest(view.LeftBox, view.InputView.Attachments.AddFiles)
// placeholder logo
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256Variant2(128))
logo, _ := gtk.ImageNew()
logo.SetFromSurface(icons.Logo256Variant2(128, logo.GetScaleFactor()))
logo.Show()
view.FaceView = sadface.New(view.Leaflet, logo)
@ -201,6 +202,7 @@ func (v *View) createMessageContainer() {
// Remove the old message container.
if v.Container != nil {
v.Container.Reset()
v.MsgBox.Remove(v.Container)
}
@ -221,13 +223,19 @@ func (v *View) createMessageContainer() {
func (v *View) Bottomed() bool { return v.Scroller.Bottomed }
// Reset resets the message view.
func (v *View) Reset() {
v.FaceView.Reset() // Switch back to the main screen.
v.reset()
}
// reset resets the message view, but does not change visible containers.
func (v *View) reset() {
v.Header.Reset() // Reset the header.
v.state.Reset() // Reset the state variables.
v.Typing.Reset() // Reset the typing state.
v.InputView.Reset() // Reset the input.
v.MemberList.Reset() // Reset the member list.
v.FaceView.Reset() // Switch back to the main screen.
// Bring the leaflet view back to the message.
v.Leaflet.SetVisibleChild(v.LeftBox)
@ -282,16 +290,8 @@ func (v *View) JoinServer(session cchat.Session, server cchat.Server, bc travers
v.FaceView.SetLoading()
v.ctrl.OnMessageBusy()
// We can be dumb. Reset afterwards so the animation goes smoother.
gts.DoAfterMs(
v.FaceView.GetTransitionDuration(),
func() { v.joinServer(session, server, bc) },
)
}
func (v *View) joinServer(session cchat.Session, server cchat.Server, bc traverse.Breadcrumber) {
// Reset before setting.
v.Reset()
v.reset()
// Get the messenger once.
var messenger = server.AsMessenger()

View file

@ -46,7 +46,7 @@ func (m *MenuButton) Bind(menu *Menu) {
// menu items.
m.SetSensitive(model.GetNItems() > 0)
// Subscribe the button to menu update events.
m.lastsig, _ = model.Connect("items-changed", func() {
m.lastsig = model.Connect("items-changed", func(model *glib.MenuModel) {
m.SetSensitive(model.GetNItems() > 0)
})
} else {

View file

@ -46,10 +46,6 @@ func Take(b, smallbutton Button, size int) {
childv, _ := b.GetChild()
widget := childv.ToWidget()
// As GetChild doesn't reference, we'll want our own reference.
widget.Ref()
defer widget.Unref()
// This will unreference.
b.Remove(widget)
// Wrap will reference.

View file

@ -6,6 +6,7 @@ import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
@ -68,9 +69,9 @@ func NewCompleter(input *gtk.TextView) *Completer {
}
// This one is for buffer modification.
ibuf.Connect("end-user-action", c.onChange)
ibuf.Connect("end-user-action", func(interface{}) { c.onChange() })
// This one is for when the cursor moves.
input.Connect("move-cursor", c.onChange)
input.Connect("move-cursor", func(interface{}) { c.onChange() })
l.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
SwapWord(ibuf, c.entries[r.GetIndex()].Raw, c.cursor)
@ -115,9 +116,7 @@ func (c *Completer) Clear() {
}
children.Foreach(func(i interface{}) {
w := i.(gtk.IWidget).ToWidget()
c.List.Remove(w)
w.Destroy()
i.(primitives.WidgetDestroyer).Destroy()
})
}