package gts import ( "fmt" "image" "os" "time" "github.com/diamondburned/cchat-gtk/internal/gts/throttler" "github.com/diamondburned/cchat-gtk/internal/log" "github.com/disintegration/imaging" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/gtk" "github.com/pkg/errors" ) const AppID = "com.github.diamondburned.cchat-gtk" var Args = append([]string{}, os.Args...) var App struct { *gtk.Application Window *gtk.ApplicationWindow Header *gtk.HeaderBar Throttler *throttler.State } // Clipboard is initialized on init(). var Clipboard *gtk.Clipboard // 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) App.Throttler.Connect(d) 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) return d, nil } func AddAppAction(name string, call func()) { action := glib.SimpleActionNew(name, nil) action.Connect("activate", call) App.AddAction(action) } func init() { gtk.Init(&Args) App.Application, _ = gtk.ApplicationNew(AppID, 0) Clipboard, _ = gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD) // Limit the TPS of the main loop on window unfocus. App.Throttler = throttler.Bind(App.Application) } // // AppMenuWidget returns the box that holds the app menu. // func AppMenuWidget() (widget *gtk.Widget) { // App.Header.For().Foreach(func(v interface{}) { // // If we've already found the widget, then stop finding. // if widget != nil { // return // } // // Cast the interface to a widget. // curr := v.(gtk.IWidget).ToWidget() // log.Println("testing") // // Check if the widget has a class named "left". // if sctx, _ := curr.GetStyleContext(); sctx.HasClass("left") { // log.Println("has class .left") // widget = curr // } // }) // return // } type Window interface { Window() gtk.IWidget Header() gtk.IWidget Menu() *glib.MenuModel Icon() *gdk.Pixbuf Close() } func Main(wfn func() Window) { App.Application.Connect("activate", func() { // Load all CSS onto the default screen. loadProviders(getDefaultScreen()) App.Header, _ = gtk.HeaderBarNew() // Right buttons only. App.Header.SetDecorationLayout(":minimize,close") App.Header.SetShowCloseButton(true) App.Header.SetProperty("spacing", 0) b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) App.Header.SetCustomTitle(b) App.Window, _ = gtk.ApplicationWindowNew(App.Application) App.Window.SetDefaultSize(1000, 500) App.Window.SetTitlebar(App.Header) // Execute the function later, because we need it to run after // initialization. w := wfn() App.Application.SetAppMenu(w.Menu()) App.Window.SetIcon(w.Icon()) App.Window.Add(w.Window()) App.Window.Show() App.Header.Add(w.Header()) App.Header.Show() // Connect extra actions. AddAppAction("quit", App.Window.Destroy) // 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() }) }) }) // Use a special function to run the application. Exit with the appropriate // exit code if necessary. if code := App.Run(Args); code > 0 { os.Exit(code) } } // 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) } // Attempt to run the callback if it's there. if f != nil { ExecAsync(f) } }() } // 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{} { var ch = make(chan struct{}) glib.IdleAdd(func() { fn() close(ch) }) return ch } // AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk // main loop. func AfterFunc(d time.Duration, f func()) (stop func()) { h, err := glib.TimeoutAdd( uint(d.Milliseconds()), func() bool { f(); return true }, ) if err != nil { panic(err) } 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 } 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.FileChooserDialogNewWith2Buttons( "Upload File", App.Window, gtk.FILE_CHOOSER_ACTION_OPEN, "Cancel", gtk.RESPONSE_CANCEL, "Upload", gtk.RESPONSE_ACCEPT, ) App.Throttler.Connect(dialog) 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) defer dialog.Close() if res := dialog.Run(); res != gtk.RESPONSE_ACCEPT { return } names, _ := dialog.GetFilenames() callback(names) } // BindPreviewer binds the file chooser dialog with a previewer. func BindPreviewer(fc *gtk.FileChooserDialog) { img, _ := gtk.ImageNew() fc.SetPreviewWidget(img) fc.Connect("update-preview", func(fc *gtk.FileChooserDialog, img *gtk.Image) { 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, ) }