2020-05-26 06:51:06 +00:00
|
|
|
package gts
|
|
|
|
|
|
|
|
import (
|
2020-06-17 07:06:34 +00:00
|
|
|
"context"
|
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
|
|
|
|
|
|
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
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
|
|
|
|
Window *gtk.ApplicationWindow
|
|
|
|
Header *gtk.HeaderBar
|
|
|
|
}
|
|
|
|
|
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-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 AddWindowAction(name string, call func()) {
|
|
|
|
action := glib.SimpleActionNew(name, nil)
|
|
|
|
action.Connect("activate", call)
|
|
|
|
App.Window.AddAction(action)
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
gtk.Init(&Args)
|
|
|
|
App.Application, _ = gtk.ApplicationNew(AppID, 0)
|
2020-05-26 06:51:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type WindowHeaderer interface {
|
2020-06-20 04:40:34 +00:00
|
|
|
Window() gtk.IWidget
|
|
|
|
Header() gtk.IWidget
|
2020-06-20 07:28:47 +00:00
|
|
|
Close()
|
2020-05-26 06:51:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func Main(wfn func() WindowHeaderer) {
|
|
|
|
App.Application.Connect("activate", func() {
|
2020-06-06 00:47:28 +00:00
|
|
|
// Load all CSS onto the default screen.
|
|
|
|
loadProviders(getDefaultScreen())
|
|
|
|
|
2020-05-26 06:51:06 +00:00
|
|
|
App.Header, _ = gtk.HeaderBarNew()
|
|
|
|
App.Header.SetShowCloseButton(true)
|
2020-05-28 19:26:55 +00:00
|
|
|
App.Header.Show()
|
2020-05-26 06:51:06 +00:00
|
|
|
|
2020-06-06 00:47:28 +00:00
|
|
|
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
|
|
|
App.Header.SetCustomTitle(b)
|
|
|
|
|
2020-05-26 06:51:06 +00:00
|
|
|
App.Window, _ = gtk.ApplicationWindowNew(App.Application)
|
2020-06-04 23:00:41 +00:00
|
|
|
App.Window.SetDefaultSize(1000, 500)
|
2020-05-26 06:51:06 +00:00
|
|
|
App.Window.SetTitlebar(App.Header)
|
2020-06-20 07:28:47 +00:00
|
|
|
|
2020-05-28 19:26:55 +00:00
|
|
|
App.Window.Show()
|
2020-05-26 06:51:06 +00:00
|
|
|
|
|
|
|
// Execute the function later, because we need it to run after
|
|
|
|
// initialization.
|
|
|
|
w := wfn()
|
|
|
|
App.Window.Add(w.Window())
|
|
|
|
App.Header.Add(w.Header())
|
2020-06-20 04:40:34 +00:00
|
|
|
|
2020-06-20 07:28:47 +00:00
|
|
|
// Connect extra actions.
|
2020-06-20 04:40:34 +00:00
|
|
|
AddAppAction("quit", App.Window.Destroy)
|
2020-06-20 07:28:47 +00:00
|
|
|
|
|
|
|
// Connect the destructor.
|
|
|
|
App.Window.Connect("destroy", func() {
|
|
|
|
// 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-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
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
// Reuser is an interface for structs that inherit Reusable.
|
|
|
|
type Reuser interface {
|
|
|
|
Context() context.Context
|
|
|
|
Acquire() int64
|
|
|
|
Validate(int64) bool
|
|
|
|
}
|
|
|
|
|
2020-06-30 03:39:42 +00:00
|
|
|
type AsyncUser = func(context.Context) (interface{}, error)
|
|
|
|
|
2020-06-17 07:06:34 +00:00
|
|
|
// AsyncUse is a handler for structs that implement the Reuser primitive. The
|
|
|
|
// passed in function will be called asynchronously, but swap will be called in
|
|
|
|
// the Gtk main thread.
|
2020-06-30 03:39:42 +00:00
|
|
|
func AsyncUse(r Reuser, swap func(interface{}), fn AsyncUser) {
|
2020-06-17 07:06:34 +00:00
|
|
|
// Acquire an ID.
|
|
|
|
id := r.Acquire()
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
Async(func() (func(), error) {
|
|
|
|
// Run the callback asynchronously.
|
|
|
|
v, err := fn(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return func() {
|
|
|
|
// Validate the ID. Cancel if it's invalid.
|
|
|
|
if !r.Validate(id) {
|
|
|
|
log.Println("Async function value dropped for reusable primitive.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the resource.
|
|
|
|
swap(v)
|
|
|
|
}, nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reusable is the synchronization primitive to provide a method for
|
|
|
|
// asynchronous cancellation and reusability.
|
|
|
|
//
|
|
|
|
// It works by copying the ID (time) for each asynchronous operation. The
|
|
|
|
// operation then completes, and the ID is then compared again before being
|
|
|
|
// used. It provides a cancellation abstraction around the Gtk main thread.
|
|
|
|
//
|
|
|
|
// This struct is not thread-safe, as it relies on the Gtk main thread
|
|
|
|
// synchronization.
|
|
|
|
type Reusable struct {
|
|
|
|
time int64 // creation time, used as ID
|
|
|
|
ctx context.Context
|
|
|
|
cancel func()
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewReusable() *Reusable {
|
|
|
|
r := &Reusable{}
|
|
|
|
r.Invalidate()
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
// Invalidate generates a new ID for the primitive, which would render
|
|
|
|
// asynchronously updating elements invalid.
|
|
|
|
func (r *Reusable) Invalidate() {
|
|
|
|
// Cancel the old context.
|
|
|
|
if r.cancel != nil {
|
|
|
|
r.cancel()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset.
|
|
|
|
r.time = time.Now().UnixNano()
|
|
|
|
r.ctx, r.cancel = context.WithCancel(context.Background())
|
|
|
|
}
|
|
|
|
|
|
|
|
// Context returns the reusable's cancellable context. It never returns nil.
|
|
|
|
func (r *Reusable) Context() context.Context {
|
|
|
|
return r.ctx
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reusable checks the acquired ID against the current one.
|
|
|
|
func (r *Reusable) Validate(acquired int64) (valid bool) {
|
|
|
|
return r.time == acquired
|
|
|
|
}
|
|
|
|
|
|
|
|
// Acquire lends the ID to be given to Reusable() after finishing.
|
|
|
|
func (r *Reusable) Acquire() int64 {
|
|
|
|
return r.time
|
|
|
|
}
|