2020-05-26 06:51:06 +00:00
|
|
|
package gts
|
|
|
|
|
|
|
|
import (
|
2020-07-10 23:26:07 +00:00
|
|
|
"fmt"
|
|
|
|
"image"
|
2020-05-26 06:51:06 +00:00
|
|
|
"os"
|
2020-06-17 07:06:34 +00:00
|
|
|
"time"
|
2020-05-26 06:51:06 +00:00
|
|
|
|
2020-07-14 07:24:55 +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-07-10 23:26:07 +00:00
|
|
|
"github.com/disintegration/imaging"
|
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
|
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")
|
|
|
|
}
|
|
|
|
|
|
|
|
d.Remove(b)
|
|
|
|
|
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)
|
|
|
|
action.Connect("activate", call)
|
|
|
|
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
|
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) {
|
2020-05-26 06:51:06 +00:00
|
|
|
App.Application.Connect("activate", func() {
|
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)
|
2020-07-17 00:21:14 +00:00
|
|
|
|
|
|
|
// 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-08-28 07:16:03 +00:00
|
|
|
// App.Application.SetAppMenu(w.Menu())
|
2020-06-20 07:28:47 +00:00
|
|
|
|
|
|
|
// Connect the destructor.
|
2020-08-28 07:16:03 +00:00
|
|
|
App.Window.Window.Connect("destroy", func() {
|
2020-06-20 07:28:47 +00:00
|
|
|
// Hide the application window.
|
|
|
|
App.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() {
|
|
|
|
// 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.
|
2020-06-17 07:06:34 +00:00
|
|
|
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.
|
|
|
|
func Async(fn func() (func(), error)) {
|
|
|
|
go func() {
|
|
|
|
f, err := fn()
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err)
|
|
|
|
}
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
// Attempt to run the callback if it's there.
|
|
|
|
if f != nil {
|
2020-06-17 07:06:34 +00:00
|
|
|
ExecAsync(f)
|
2020-06-13 07:29:32 +00:00
|
|
|
}
|
2020-05-26 06:51:06 +00:00
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
// ExecAsync executes function asynchronously in the Gtk main thread.
|
|
|
|
func ExecAsync(fn func()) {
|
|
|
|
glib.IdleAdd(fn)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ExecSync executes the function asynchronously, but returns a channel that
|
|
|
|
// indicates when the job is done.
|
|
|
|
func ExecSync(fn func()) <-chan struct{} {
|
2020-06-07 04:27:28 +00:00
|
|
|
var ch = make(chan struct{})
|
2020-05-26 06:51:06 +00:00
|
|
|
|
|
|
|
glib.IdleAdd(func() {
|
|
|
|
fn()
|
|
|
|
close(ch)
|
|
|
|
})
|
|
|
|
|
|
|
|
return ch
|
|
|
|
}
|
2020-06-07 04:27:28 +00:00
|
|
|
|
2020-12-20 09:48:52 +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()) {
|
|
|
|
_, err := glib.TimeoutAdd(ms, f)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-04 04:41:12 +00:00
|
|
|
// AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk
|
|
|
|
// main loop.
|
|
|
|
func AfterFunc(d time.Duration, f func()) (stop func()) {
|
2020-12-20 09:48:52 +00:00
|
|
|
return AfterMsFunc(uint(d.Milliseconds()), f)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 })
|
2020-07-04 04:41:12 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
2020-06-17 07:06:34 +00:00
|
|
|
}
|
|
|
|
|
2020-07-04 04:41:12 +00:00
|
|
|
return func() { glib.SourceRemove(h) }
|
2020-06-17 07:06:34 +00:00
|
|
|
}
|
|
|
|
|
2020-07-04 04:41:12 +00:00
|
|
|
func EventIsRightClick(ev *gdk.Event) bool {
|
|
|
|
keyev := gdk.EventButtonNewFromEvent(ev)
|
|
|
|
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
|
2020-06-17 07:06:34 +00:00
|
|
|
}
|
2020-07-10 23:26:07 +00:00
|
|
|
|
|
|
|
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)) {
|
2020-11-05 19:08:30 +00:00
|
|
|
dialog, _ := gtk.FileChooserNativeDialogNew(
|
2020-07-10 23:26:07 +00:00
|
|
|
"Upload File", App.Window,
|
|
|
|
gtk.FILE_CHOOSER_ACTION_OPEN,
|
2020-11-05 19:08:30 +00:00
|
|
|
"Upload", "Cancel",
|
2020-07-10 23:26:07 +00:00
|
|
|
)
|
|
|
|
|
2020-07-16 05:41:21 +00:00
|
|
|
App.Throttler.Connect(dialog)
|
2020-07-10 23:26:07 +00:00
|
|
|
BindPreviewer(dialog)
|
|
|
|
|
|
|
|
if dirpath == "" {
|
|
|
|
p, err := os.Getwd()
|
|
|
|
if err != nil {
|
|
|
|
p = glib.GetUserDataDir()
|
|
|
|
}
|
|
|
|
dirpath = p
|
|
|
|
}
|
|
|
|
|
|
|
|
dialog.SetLocalOnly(false)
|
|
|
|
dialog.SetCurrentFolder(dirpath)
|
|
|
|
dialog.SetSelectMultiple(true)
|
|
|
|
|
2020-11-05 19:08:30 +00:00
|
|
|
if res := dialog.Run(); 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.
|
2020-11-05 19:08:30 +00:00
|
|
|
func BindPreviewer(fc *gtk.FileChooserNativeDialog) {
|
2020-07-10 23:26:07 +00:00
|
|
|
img, _ := gtk.ImageNew()
|
|
|
|
|
|
|
|
fc.SetPreviewWidget(img)
|
|
|
|
fc.Connect("update-preview",
|
2020-11-05 19:08:30 +00:00
|
|
|
func(_ interface{}, img *gtk.Image) {
|
2020-07-10 23:26:07 +00:00
|
|
|
file := fc.GetPreviewFilename()
|
|
|
|
|
|
|
|
b, err := gdk.PixbufNewFromFileAtScale(file, 256, 256, true)
|
|
|
|
if err != nil {
|
|
|
|
fc.SetPreviewWidgetActive(false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
img.SetFromPixbuf(b)
|
|
|
|
fc.SetPreviewWidgetActive(true)
|
|
|
|
},
|
|
|
|
img,
|
|
|
|
)
|
|
|
|
}
|