cchat-gtk/internal/gts/gts.go

367 lines
8.2 KiB
Go
Raw Normal View History

2020-05-26 06:51:06 +00:00
package gts
import (
2021-05-05 03:30:50 +00:00
"context"
2020-12-30 08:31:03 +00:00
"io"
2020-05-26 06:51:06 +00:00
"os"
"time"
2020-05-26 06:51:06 +00:00
"github.com/diamondburned/cchat-gtk/internal/gts/throttler"
2020-05-26 06:51:06 +00:00
"github.com/diamondburned/cchat-gtk/internal/log"
2020-08-28 07:16:03 +00:00
"github.com/diamondburned/handy"
2020-06-07 04:27:28 +00:00
"github.com/gotk3/gotk3/gdk"
2020-05-26 06:51:06 +00:00
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
2020-06-29 01:38:09 +00:00
"github.com/pkg/errors"
2020-05-26 06:51:06 +00:00
)
2020-06-06 00:47:28 +00:00
const AppID = "com.github.diamondburned.cchat-gtk"
2020-05-26 06:51:06 +00:00
var Args = append([]string{}, os.Args...)
var App struct {
*gtk.Application
2020-08-28 07:16:03 +00:00
Window *handy.ApplicationWindow
2020-07-16 05:41:21 +00:00
Throttler *throttler.State
2021-01-05 02:05:33 +00:00
closing bool
}
// IsClosing returns true if the window is destroyed.
func IsClosing() bool {
return App.closing
2020-05-26 06:51:06 +00:00
}
2020-08-28 07:16:03 +00:00
// Windower is the interface for a window.
type Windower interface {
gtk.IWidget
gtk.IWindow
throttler.Connector
}
func AddWindow(w Windower) {
App.AddWindow(w)
App.Throttler.Connect(w)
}
2020-07-10 23:26:07 +00:00
// Clipboard is initialized on init().
var Clipboard *gtk.Clipboard
2020-06-20 04:40:34 +00:00
// NewModalDialog returns a new modal dialog that's transient for the main
// window.
func NewModalDialog() (*gtk.Dialog, error) {
d, err := gtk.DialogNew()
if err != nil {
return nil, err
}
d.SetModal(true)
d.SetTransientFor(App.Window)
2020-08-28 07:16:03 +00:00
AddWindow(d)
2020-06-29 01:38:09 +00:00
return d, nil
}
func NewEmptyModalDialog() (*gtk.Dialog, error) {
d, err := NewModalDialog()
if err != nil {
return nil, err
}
b, err := d.GetContentArea()
if err != nil {
return nil, errors.Wrap(err, "Failed to get content area")
}
b.Destroy()
2020-06-29 01:38:09 +00:00
2020-06-20 04:40:34 +00:00
return d, nil
2020-05-26 06:51:06 +00:00
}
2020-06-20 04:40:34 +00:00
func AddAppAction(name string, call func()) {
action := glib.SimpleActionNew(name, nil)
2020-12-30 07:48:18 +00:00
action.Connect("activate", func(interface{}) { call() })
2020-06-20 04:40:34 +00:00
App.AddAction(action)
2020-05-26 06:51:06 +00:00
}
2020-06-20 04:40:34 +00:00
func init() {
gtk.Init(&Args)
App.Application, _ = gtk.ApplicationNew(AppID, 0)
2020-07-10 23:26:07 +00:00
Clipboard, _ = gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD)
2020-07-16 05:41:21 +00:00
// Limit the TPS of the main loop on window unfocus.
App.Throttler = throttler.Bind(App.Application)
2020-05-26 06:51:06 +00:00
}
2020-08-28 07:16:03 +00:00
type MainApplication interface {
gtk.IWidget
2020-07-16 05:41:21 +00:00
Menu() *glib.MenuModel
Icon() *gdk.Pixbuf // assume scale 1
2020-06-20 07:28:47 +00:00
Close()
2020-05-26 06:51:06 +00:00
}
2020-08-28 07:16:03 +00:00
func Main(wfn func() MainApplication) {
App.Application.Connect("activate", func(*gtk.Application) {
2020-08-28 07:16:03 +00:00
handy.Init()
2020-06-06 00:47:28 +00:00
// Load all CSS onto the default screen.
loadProviders(getDefaultScreen())
2020-08-28 07:16:03 +00:00
// App.Header, _ = gtk.HeaderBarNew()
// // Right buttons only.
// App.Header.SetDecorationLayout(":minimize,close")
// App.Header.SetShowCloseButton(true)
// App.Header.SetProperty("spacing", 0)
2020-05-26 06:51:06 +00:00
2020-08-28 07:16:03 +00:00
App.Window = handy.ApplicationWindowNew()
2020-06-04 23:00:41 +00:00
App.Window.SetDefaultSize(1000, 500)
2020-08-28 07:16:03 +00:00
App.Window.Show()
AddWindow(&App.Window.Window)
App.Throttler.Connect(&App.Window.Window)
// Execute the function later, because we need it to run after
// initialization.
w := wfn()
2020-08-28 07:16:03 +00:00
App.Window.Add(w)
2020-07-16 05:41:21 +00:00
App.Window.SetIcon(w.Icon())
2020-06-20 07:28:47 +00:00
// Connect the destructor.
App.Window.Window.Connect("destroy", func(window *handy.ApplicationWindow) {
2020-06-20 07:28:47 +00:00
// Hide the application window.
window.Hide()
2021-01-05 02:05:33 +00:00
App.closing = true
2020-06-20 07:28:47 +00:00
// 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.
ExecLater(func() {
2020-06-20 07:28:47 +00:00
// Stop the application loop.
App.Application.Quit()
// Finalize the application by running the closer.
w.Close()
})
})
2020-08-28 07:16:03 +00:00
// Connect extra actions.
AddAppAction("quit", App.Window.Destroy)
2020-05-26 06:51:06 +00:00
})
// Use a special function to run the application. Exit with the appropriate
2020-06-20 07:28:47 +00:00
// exit code if necessary.
if code := App.Run(Args); code > 0 {
os.Exit(code)
}
2020-05-26 06:51:06 +00:00
}
// Async runs fn asynchronously, then runs the function it returns in the Gtk
// main thread.
2021-05-05 03:30:50 +00:00
// TODO: deprecate Async.
2020-05-26 06:51:06 +00:00
func Async(fn func() (func(), error)) {
go func() {
f, err := fn()
if err != nil {
log.Error(err)
}
// Attempt to run the callback if it's there.
if f != nil {
ExecAsync(f)
}
2020-05-26 06:51:06 +00:00
}()
}
2021-05-05 03:30:50 +00:00
// AsyncCancel is similar to AsyncCtx, but the context is created internally.
func AsyncCancel(fn func(ctx context.Context) (func(), error)) context.CancelFunc {
ctx, cancel := context.WithCancel(context.Background())
go func() {
// fn() is assumed to use the same given ctx.
f, err := fn(ctx)
if err != nil {
log.Error(err)
}
// Attempt to run the callback if it's there.
if f != nil {
ExecAsyncCtx(ctx, f)
}
}()
return cancel
}
// AsyncCtx does what Async does, except the returned callback will not be
// executed if the given context has expired or the returned callback is called.
func AsyncCtx(ctx context.Context, fn func() (func(), error)) {
go func() {
// fn() is assumed to use the same given ctx.
f, err := fn()
if err != nil {
log.Error(err)
}
// Attempt to run the callback if it's there.
if f != nil {
ExecAsyncCtx(ctx, f)
}
}()
}
// ExecLater executes the function asynchronously with a low priority.
func ExecLater(fn func()) {
2020-12-30 07:48:18 +00:00
glib.IdleAddPriority(glib.PRIORITY_DEFAULT_IDLE, fn)
}
2020-05-26 06:51:06 +00:00
// ExecAsync executes function asynchronously in the Gtk main thread.
2021-05-05 03:30:50 +00:00
// TODO: deprecate Async.
2020-05-26 06:51:06 +00:00
func ExecAsync(fn func()) {
glib.IdleAddPriority(glib.PRIORITY_HIGH, fn)
2020-05-26 06:51:06 +00:00
}
2021-05-05 03:30:50 +00:00
// ExecAsyncCtx executes the function asynchronously in the Gtk main thread only
// if the context has not expired. This API has absolutely no race conditions if
// the context is only canceled in the main thread.
func ExecAsyncCtx(ctx context.Context, fn func()) {
ExecAsync(func() {
select {
case <-ctx.Done():
2020-05-26 06:51:06 +00:00
2021-05-05 03:30:50 +00:00
default:
fn()
}
2020-05-26 06:51:06 +00:00
})
}
2020-06-07 04:27:28 +00:00
// DoAfter calls f after the given duration in the Gtk main loop.
func DoAfter(d time.Duration, f func()) {
DoAfterMs(uint(d.Milliseconds()), f)
}
// DoAfterMs calls f after the given ms in the Gtk main loop.
func DoAfterMs(ms uint, f func()) {
if secs := ms / 1000; secs*1000 == ms {
glib.TimeoutSecondsAddPriority(secs, glib.PRIORITY_HIGH_IDLE, f)
} else {
glib.TimeoutAddPriority(ms, glib.PRIORITY_HIGH_IDLE, f)
}
}
// AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk
// main loop.
func AfterFunc(d time.Duration, f func()) (stop func()) {
return AfterMsFunc(uint(d.Milliseconds()), f)
}
// AfterMsFunc is similar to AfterFunc but takes in milliseconds instead.
func AfterMsFunc(ms uint, f func()) (stop func()) {
fn := func() bool { f(); return true }
var h glib.SourceHandle
if secs := ms / 1000; secs*1000 == ms {
h = glib.TimeoutSecondsAddPriority(secs, glib.PRIORITY_HIGH_IDLE, fn)
} else {
h = glib.TimeoutAddPriority(ms, glib.PRIORITY_HIGH_IDLE, fn)
}
return func() { glib.SourceRemove(h) }
}
func EventIsRightClick(ev *gdk.Event) bool {
keyev := gdk.EventButtonNewFromEvent(ev)
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
}
2020-07-10 23:26:07 +00:00
func EventIsLeftClick(ev *gdk.Event) bool {
keyev := gdk.EventButtonNewFromEvent(ev)
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_PRIMARY
}
2020-07-10 23:26:07 +00:00
func SpawnUploader(dirpath string, callback func(absolutePaths []string)) {
dialog, _ := gtk.FileChooserNativeDialogNew(
2020-07-10 23:26:07 +00:00
"Upload File", App.Window,
gtk.FILE_CHOOSER_ACTION_OPEN,
"Upload", "Cancel",
2020-07-10 23:26:07 +00:00
)
2020-12-30 08:31:03 +00:00
// BindPreviewer(dialog)
2020-07-10 23:26:07 +00:00
if dirpath == "" {
p, err := os.Getwd()
if err != nil {
p = glib.GetUserDataDir()
}
dirpath = p
}
dialog.SetLocalOnly(false)
dialog.SetCurrentFolder(dirpath)
dialog.SetSelectMultiple(true)
2020-12-30 08:31:03 +00:00
res := dialog.Run()
dialog.Destroy()
if res != int(gtk.RESPONSE_ACCEPT) {
2020-07-10 23:26:07 +00:00
return
}
names, _ := dialog.GetFilenames()
callback(names)
}
// BindPreviewer binds the file chooser dialog with a previewer.
func BindPreviewer(fc *gtk.FileChooserNativeDialog) {
2020-07-10 23:26:07 +00:00
img, _ := gtk.ImageNew()
fc.SetPreviewWidget(img)
2020-12-30 08:31:03 +00:00
fc.Connect("update-preview", func(interface{}) { loadImage(fc, img) })
}
func loadImage(fc *gtk.FileChooserNativeDialog, img *gtk.Image) {
file := fc.GetPreviewFilename()
go func() {
var animation *gdk.PixbufAnimation
var pixbuf *gdk.Pixbuf
defer ExecAsync(func() {
if fc.GetPreviewFilename() == file {
if animation == nil && pixbuf == nil {
fc.SetPreviewWidgetActive(false)
return
}
if animation != nil {
img.SetFromAnimation(animation)
} else {
img.SetFromPixbuf(pixbuf)
}
fc.SetPreviewWidgetActive(true)
2020-07-10 23:26:07 +00:00
}
2020-12-30 08:31:03 +00:00
})
2020-07-10 23:26:07 +00:00
2020-12-30 08:31:03 +00:00
l, err := gdk.PixbufLoaderNew()
if err != nil {
return
}
f, err := os.Open(file)
if err != nil {
return
}
defer f.Close()
if _, err := io.Copy(l, f); err != nil {
return
}
if err := l.Close(); err != nil {
return
}
if pixbuf == nil {
return
}
}()
2020-07-10 23:26:07 +00:00
}