Files

464 lines
11 KiB
Go
Raw Permalink Normal View History

2019-07-12 10:12:15 +10:00
package renderer
2019-01-08 07:58:46 +11:00
import (
"encoding/json"
"fmt"
"math/rand"
2019-07-12 10:12:15 +10:00
"strings"
2019-01-08 07:58:46 +11:00
"sync"
"time"
2021-04-02 20:44:55 +11:00
"github.com/wailsapp/wails/runtime"
2019-01-08 07:58:46 +11:00
"github.com/go-playground/colors"
2019-07-20 19:32:30 +10:00
"github.com/wailsapp/wails/lib/interfaces"
2019-07-12 10:12:15 +10:00
"github.com/wailsapp/wails/lib/logger"
"github.com/wailsapp/wails/lib/messages"
2019-08-16 19:12:55 +10:00
wv "github.com/wailsapp/wails/lib/renderer/webview"
2019-01-08 07:58:46 +11:00
)
2019-07-12 10:12:15 +10:00
// WebView defines the main webview application window
2019-01-08 07:58:46 +11:00
// Default values in []
2020-10-28 21:20:47 +11:00
// UseFirebug indicates whether to inject the firebug console
var UseFirebug = ""
2019-07-12 10:12:15 +10:00
type WebView struct {
2021-09-27 21:05:43 +10:00
window wv.WebView // The webview object
ipc interfaces.IPCManager
log *logger.CustomLogger
config interfaces.AppConfig
eventManager interfaces.EventManager
bindingCache []string
2021-04-02 16:24:48 +11:00
maximumSizeSet bool
2019-01-08 07:58:46 +11:00
}
2019-07-12 10:12:15 +10:00
// NewWebView returns a new WebView struct
func NewWebView() *WebView {
2019-07-20 19:32:30 +10:00
return &WebView{}
2019-07-12 10:12:15 +10:00
}
2019-01-08 07:58:46 +11:00
// Initialise sets up the WebView
2019-07-12 10:12:15 +10:00
func (w *WebView) Initialise(config interfaces.AppConfig, ipc interfaces.IPCManager, eventManager interfaces.EventManager) error {
2019-01-08 07:58:46 +11:00
// Store reference to eventManager
w.eventManager = eventManager
// Set up logger
2019-07-12 10:12:15 +10:00
w.log = logger.NewCustomLogger("WebView")
2019-01-08 07:58:46 +11:00
// Set up the dispatcher function
w.ipc = ipc
2019-07-12 10:12:15 +10:00
ipc.BindRenderer(w)
2019-01-08 07:58:46 +11:00
// Save the config
w.config = config
2021-04-02 16:24:48 +11:00
width := config.GetWidth()
height := config.GetHeight()
// Clamp width and height
minWidth, minHeight := config.GetMinWidth(), config.GetMinHeight()
maxWidth, maxHeight := config.GetMaxWidth(), config.GetMaxHeight()
setMinSize := minWidth != -1 && minHeight != -1
setMaxSize := maxWidth != -1 && maxHeight != -1
if setMinSize {
if width < minWidth {
width = minWidth
}
if height < minHeight {
height = minHeight
}
}
if setMaxSize {
if width > maxWidth {
width = maxWidth
}
if height > maxHeight {
height = maxHeight
}
}
2019-01-08 07:58:46 +11:00
// Create the WebView instance
2019-08-25 04:08:21 -04:00
w.window = wv.NewWebview(wv.Settings{
2021-04-02 16:24:48 +11:00
Width: width,
Height: height,
2019-07-12 10:12:15 +10:00
Title: config.GetTitle(),
Resizable: config.GetResizable(),
2020-11-01 06:31:35 +11:00
URL: config.GetHTML(),
2019-07-12 10:12:15 +10:00
Debug: !config.GetDisableInspector(),
2019-08-25 04:08:21 -04:00
ExternalInvokeCallback: func(_ wv.WebView, message string) {
2020-01-25 18:07:53 -06:00
w.ipc.Dispatch(message, w.callback)
2019-01-08 07:58:46 +11:00
},
})
2021-04-02 16:24:48 +11:00
// Set minimum and maximum sizes
if setMinSize {
w.SetMinSize(minWidth, minHeight)
}
if setMaxSize {
w.SetMaxSize(maxWidth, maxHeight)
}
2019-01-08 07:58:46 +11:00
2021-04-02 20:44:55 +11:00
// Set minimum and maximum sizes
if setMinSize {
w.SetMinSize(minWidth, minHeight)
}
if setMaxSize {
w.SetMaxSize(maxWidth, maxHeight)
2021-04-02 16:24:48 +11:00
}
2019-01-08 07:58:46 +11:00
// SignalManager.OnExit(w.Exit)
// Set colour
color := config.GetColour()
if color != "" {
err := w.SetColour(color)
if err != nil {
return err
}
2019-01-08 07:58:46 +11:00
}
w.log.Info("Initialised")
return nil
}
2019-07-12 10:12:15 +10:00
// SetColour sets the window colour
func (w *WebView) SetColour(colour string) error {
2019-01-08 07:58:46 +11:00
color, err := colors.Parse(colour)
if err != nil {
return err
}
rgba := color.ToRGBA()
alpha := uint8(255 * rgba.A)
w.window.Dispatch(func() {
w.window.SetColor(rgba.R, rgba.G, rgba.B, alpha)
})
return nil
}
// evalJS evaluates the given js in the WebView
// I should rename this to evilJS lol
2019-07-12 10:12:15 +10:00
func (w *WebView) evalJS(js string) error {
2019-01-08 07:58:46 +11:00
outputJS := fmt.Sprintf("%.45s", js)
if len(js) > 45 {
outputJS += "..."
}
2019-07-12 10:12:15 +10:00
w.log.DebugFields("Eval", logger.Fields{"js": outputJS})
2019-01-08 07:58:46 +11:00
//
w.window.Dispatch(func() {
w.window.Eval(js)
})
return nil
}
2019-07-12 10:12:15 +10:00
// Escape the Javascripts!
func escapeJS(js string) (string, error) {
result := strings.Replace(js, "\\", "\\\\", -1)
result = strings.Replace(result, "'", "\\'", -1)
result = strings.Replace(result, "\n", "\\n", -1)
return result, nil
}
2019-01-08 07:58:46 +11:00
// evalJSSync evaluates the given js in the WebView synchronously
// Do not call this from the main thread or you'll nuke your app because
// you won't get the callback.
2019-07-12 10:12:15 +10:00
func (w *WebView) evalJSSync(js string) error {
2019-01-08 07:58:46 +11:00
minified, err := escapeJS(js)
2019-07-12 10:12:15 +10:00
2019-01-08 07:58:46 +11:00
if err != nil {
return err
}
outputJS := fmt.Sprintf("%.45s", js)
if len(js) > 45 {
outputJS += "..."
}
2019-07-12 10:12:15 +10:00
w.log.DebugFields("EvalSync", logger.Fields{"js": outputJS})
2019-01-08 07:58:46 +11:00
ID := fmt.Sprintf("syncjs:%d:%d", time.Now().Unix(), rand.Intn(9999))
var wg sync.WaitGroup
wg.Add(1)
go func() {
exit := false
2019-04-10 08:46:49 +10:00
// We are done when we receive the Callback ID
2019-01-08 07:58:46 +11:00
w.log.Debug("SyncJS: sending with ID = " + ID)
w.eventManager.On(ID, func(...interface{}) {
w.log.Debug("SyncJS: Got callback ID = " + ID)
wg.Done()
exit = true
})
2019-07-13 15:30:52 +10:00
command := fmt.Sprintf("wails._.AddScript('%s', '%s')", minified, ID)
2019-01-08 07:58:46 +11:00
w.window.Dispatch(func() {
w.window.Eval(command)
})
for exit == false {
time.Sleep(time.Millisecond * 1)
}
}()
wg.Wait()
return nil
}
// injectCSS adds the given CSS to the WebView
2019-07-12 10:12:15 +10:00
func (w *WebView) injectCSS(css string) {
2019-01-08 07:58:46 +11:00
w.window.Dispatch(func() {
w.window.InjectCSS(css)
})
}
2019-07-12 10:12:15 +10:00
// Exit closes the window
func (w *WebView) Exit() {
2019-01-08 07:58:46 +11:00
w.window.Exit()
}
// Run the window main loop
2019-07-12 10:12:15 +10:00
func (w *WebView) Run() error {
2019-01-08 07:58:46 +11:00
2019-09-19 08:57:40 +10:00
w.log.Info("Running...")
2019-01-08 07:58:46 +11:00
2019-11-27 22:55:19 +11:00
// Inject firebug in debug mode on Windows
2020-10-28 21:20:47 +11:00
if UseFirebug != "" {
w.log.Debug("Injecting Firebug")
w.evalJS(`window.usefirebug=true;`)
2019-11-27 22:55:19 +11:00
}
2019-01-08 07:58:46 +11:00
// Runtime assets
2021-04-02 20:44:55 +11:00
w.log.DebugFields("Injecting wails JS runtime", logger.Fields{"js": runtime.WailsJS})
w.evalJS(runtime.WailsJS)
2019-01-08 07:58:46 +11:00
// Ping the wait channel when the wails runtime is loaded
w.eventManager.On("wails:loaded", func(...interface{}) {
// Run this in a different go routine to free up the main process
go func() {
// Inject Bindings
for _, binding := range w.bindingCache {
w.evalJSSync(binding)
}
// Inject user CSS
2019-07-12 10:12:15 +10:00
if w.config.GetCSS() != "" {
outputCSS := fmt.Sprintf("%.45s", w.config.GetCSS())
2019-01-08 07:58:46 +11:00
if len(outputCSS) > 45 {
outputCSS += "..."
}
2019-07-12 10:12:15 +10:00
w.log.DebugFields("Inject User CSS", logger.Fields{"css": outputCSS})
w.injectCSS(w.config.GetCSS())
2019-01-15 18:50:26 +11:00
} else {
// Use default wails css
2021-04-02 20:44:55 +11:00
w.log.Debug("Injecting Default Wails CSS: " + runtime.WailsCSS)
w.injectCSS(runtime.WailsCSS)
2019-01-08 07:58:46 +11:00
}
// Inject user JS
2019-07-12 10:12:15 +10:00
if w.config.GetJS() != "" {
outputJS := fmt.Sprintf("%.45s", w.config.GetJS())
2019-01-08 07:58:46 +11:00
if len(outputJS) > 45 {
outputJS += "..."
}
2019-07-12 10:12:15 +10:00
w.log.DebugFields("Inject User JS", logger.Fields{"js": outputJS})
w.evalJSSync(w.config.GetJS())
2019-01-08 07:58:46 +11:00
}
// Emit that everything is loaded and ready
w.eventManager.Emit("wails:ready")
}()
})
// Kick off main window loop
w.window.Run()
return nil
}
2019-07-12 10:12:15 +10:00
// NewBinding registers a new binding with the frontend
func (w *WebView) NewBinding(methodName string) error {
2019-07-13 15:30:52 +10:00
objectCode := fmt.Sprintf("window.wails._.NewBinding('%s');", methodName)
2019-01-08 07:58:46 +11:00
w.bindingCache = append(w.bindingCache, objectCode)
return nil
}
2019-07-12 10:12:15 +10:00
// SelectFile opens a dialog that allows the user to select a file
2020-06-17 19:09:09 +09:00
func (w *WebView) SelectFile(title string, filter string) string {
2019-01-08 07:58:46 +11:00
var result string
// We need to run this on the main thread, however Dispatch is
// non-blocking so we launch this in a goroutine and wait for
// dispatch to finish before returning the result
var wg sync.WaitGroup
wg.Add(1)
go func() {
w.window.Dispatch(func() {
2020-06-17 19:09:09 +09:00
result = w.window.Dialog(wv.DialogTypeOpen, 0, title, "", filter)
2019-01-08 07:58:46 +11:00
wg.Done()
})
}()
2021-03-27 21:06:02 +11:00
defer w.focus() // Ensure the main window is put back into focus afterwards
2019-01-08 07:58:46 +11:00
wg.Wait()
return result
}
2019-07-12 10:12:15 +10:00
// SelectDirectory opens a dialog that allows the user to select a directory
func (w *WebView) SelectDirectory() string {
2019-01-08 07:58:46 +11:00
var result string
// We need to run this on the main thread, however Dispatch is
// non-blocking so we launch this in a goroutine and wait for
// dispatch to finish before returning the result
var wg sync.WaitGroup
wg.Add(1)
go func() {
w.window.Dispatch(func() {
result = w.window.Dialog(wv.DialogTypeOpen, wv.DialogFlagDirectory, "Select Directory", "", "")
2019-01-08 07:58:46 +11:00
wg.Done()
})
}()
2021-03-27 21:06:02 +11:00
defer w.focus() // Ensure the main window is put back into focus afterwards
2019-01-08 07:58:46 +11:00
wg.Wait()
return result
}
2019-07-12 10:12:15 +10:00
// SelectSaveFile opens a dialog that allows the user to select a file to save
2020-06-17 19:09:09 +09:00
func (w *WebView) SelectSaveFile(title string, filter string) string {
2019-01-08 07:58:46 +11:00
var result string
// We need to run this on the main thread, however Dispatch is
// non-blocking so we launch this in a goroutine and wait for
// dispatch to finish before returning the result
var wg sync.WaitGroup
wg.Add(1)
go func() {
w.window.Dispatch(func() {
2020-06-17 19:09:09 +09:00
result = w.window.Dialog(wv.DialogTypeSave, 0, title, "", filter)
2019-01-08 07:58:46 +11:00
wg.Done()
})
}()
2021-03-27 21:06:02 +11:00
defer w.focus() // Ensure the main window is put back into focus afterwards
2019-01-08 07:58:46 +11:00
wg.Wait()
return result
}
2021-03-27 21:06:02 +11:00
// focus puts the main window into focus
func (w *WebView) focus() {
w.window.Dispatch(func() {
w.window.Focus()
})
}
2020-01-25 18:07:53 -06:00
// callback sends a callback to the frontend
func (w *WebView) callback(data string) error {
2019-07-13 15:30:52 +10:00
callbackCMD := fmt.Sprintf("window.wails._.Callback('%s');", data)
2019-01-08 07:58:46 +11:00
return w.evalJS(callbackCMD)
}
2019-07-12 10:12:15 +10:00
// NotifyEvent notifies the frontend about a backend runtime event
func (w *WebView) NotifyEvent(event *messages.EventData) error {
2019-01-08 07:58:46 +11:00
// Look out! Nils about!
var err error
if event == nil {
2019-07-12 10:12:15 +10:00
err = fmt.Errorf("Sent nil event to renderer.WebView")
w.log.Error(err.Error())
2019-01-08 07:58:46 +11:00
return err
}
// Default data is a blank array
data := []byte("[]")
// Process event data
if event.Data != nil {
// Marshall the data
data, err = json.Marshal(event.Data)
if err != nil {
w.log.Errorf("Cannot unmarshall JSON data in event: %s ", err.Error())
return err
}
}
2020-08-21 14:28:29 +10:00
// Double encode data to ensure everything is escaped correctly.
data, err = json.Marshal(string(data))
if err != nil {
w.log.Errorf("Cannot marshal JSON data in event: %s ", err.Error())
return err
}
message := "window.wails._.Notify('" + event.Name + "'," + string(data) + ")"
2019-01-08 07:58:46 +11:00
return w.evalJS(message)
}
2021-04-02 16:24:48 +11:00
// SetMinSize sets the minimum size of a resizable window
func (w *WebView) SetMinSize(width, height int) {
if w.config.GetResizable() == false {
w.log.Warn("Cannot call SetMinSize() - App.Resizable = false")
return
}
w.window.Dispatch(func() {
w.window.SetMinSize(width, height)
})
}
// SetMaxSize sets the maximum size of a resizable window
func (w *WebView) SetMaxSize(width, height int) {
if w.config.GetResizable() == false {
w.log.Warn("Cannot call SetMaxSize() - App.Resizable = false")
return
}
w.maximumSizeSet = true
w.window.Dispatch(func() {
w.window.SetMaxSize(width, height)
})
}
2019-07-12 10:12:15 +10:00
// Fullscreen makes the main window go fullscreen
func (w *WebView) Fullscreen() {
if w.config.GetResizable() == false {
2019-01-08 07:58:46 +11:00
w.log.Warn("Cannot call Fullscreen() - App.Resizable = false")
return
2021-04-02 16:24:48 +11:00
} else if w.maximumSizeSet {
w.log.Warn("Cannot call Fullscreen() - Maximum size of window set")
return
2019-01-08 07:58:46 +11:00
}
w.window.Dispatch(func() {
w.window.SetFullscreen(true)
})
}
2019-07-12 10:12:15 +10:00
// UnFullscreen returns the window to the position prior to a fullscreen call
func (w *WebView) UnFullscreen() {
if w.config.GetResizable() == false {
2019-01-08 07:58:46 +11:00
w.log.Warn("Cannot call UnFullscreen() - App.Resizable = false")
return
}
w.window.Dispatch(func() {
w.window.SetFullscreen(false)
})
}
2019-07-12 10:12:15 +10:00
// SetTitle sets the window title
func (w *WebView) SetTitle(title string) {
2019-01-08 07:58:46 +11:00
w.window.Dispatch(func() {
w.window.SetTitle(title)
})
}
2019-07-12 10:12:15 +10:00
// Close closes the window
func (w *WebView) Close() {
2019-01-08 07:58:46 +11:00
w.window.Dispatch(func() {
w.window.Terminate()
})
}