github.com/secoba/wails/v2@v2.6.4/internal/frontend/desktop/linux/window.go (about)

     1  //go:build linux
     2  // +build linux
     3  
     4  package linux
     5  
     6  /*
     7  #cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
     8  
     9  #include <JavaScriptCore/JavaScript.h>
    10  #include <gtk/gtk.h>
    11  #include <webkit2/webkit2.h>
    12  #include <stdio.h>
    13  #include <limits.h>
    14  #include <stdint.h>
    15  #include "window.h"
    16  
    17  */
    18  import "C"
    19  import (
    20  	"log"
    21  	"strings"
    22  	"sync"
    23  	"unsafe"
    24  
    25  	"github.com/secoba/wails/v2/internal/frontend"
    26  	"github.com/secoba/wails/v2/pkg/menu"
    27  	"github.com/secoba/wails/v2/pkg/options"
    28  	"github.com/secoba/wails/v2/pkg/options/linux"
    29  )
    30  
    31  func gtkBool(input bool) C.gboolean {
    32  	if input {
    33  		return C.gboolean(1)
    34  	}
    35  	return C.gboolean(0)
    36  }
    37  
    38  type Window struct {
    39  	appoptions                               *options.App
    40  	debug                                    bool
    41  	devtoolsEnabled                          bool
    42  	gtkWindow                                unsafe.Pointer
    43  	contentManager                           unsafe.Pointer
    44  	webview                                  unsafe.Pointer
    45  	applicationMenu                          *menu.Menu
    46  	menubar                                  *C.GtkWidget
    47  	webviewBox                               *C.GtkWidget
    48  	vbox                                     *C.GtkWidget
    49  	accels                                   *C.GtkAccelGroup
    50  	minWidth, minHeight, maxWidth, maxHeight int
    51  }
    52  
    53  func bool2Cint(value bool) C.int {
    54  	if value {
    55  		return C.int(1)
    56  	}
    57  	return C.int(0)
    58  }
    59  
    60  func NewWindow(appoptions *options.App, debug bool, devtoolsEnabled bool) *Window {
    61  	validateWebKit2Version(appoptions)
    62  
    63  	result := &Window{
    64  		appoptions:      appoptions,
    65  		debug:           debug,
    66  		devtoolsEnabled: devtoolsEnabled,
    67  		minHeight:       appoptions.MinHeight,
    68  		minWidth:        appoptions.MinWidth,
    69  		maxHeight:       appoptions.MaxHeight,
    70  		maxWidth:        appoptions.MaxWidth,
    71  	}
    72  
    73  	gtkWindow := C.gtk_window_new(C.GTK_WINDOW_TOPLEVEL)
    74  	C.g_object_ref_sink(C.gpointer(gtkWindow))
    75  	result.gtkWindow = unsafe.Pointer(gtkWindow)
    76  
    77  	webviewName := C.CString("webview-box")
    78  	defer C.free(unsafe.Pointer(webviewName))
    79  	result.webviewBox = C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0)
    80  	C.gtk_widget_set_name(result.webviewBox, webviewName)
    81  
    82  	result.vbox = C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0)
    83  	C.gtk_container_add(result.asGTKContainer(), result.vbox)
    84  
    85  	result.contentManager = unsafe.Pointer(C.webkit_user_content_manager_new())
    86  	external := C.CString("external")
    87  	defer C.free(unsafe.Pointer(external))
    88  	C.webkit_user_content_manager_register_script_message_handler(result.cWebKitUserContentManager(), external)
    89  	C.SetupInvokeSignal(result.contentManager)
    90  
    91  	var webviewGpuPolicy int
    92  	if appoptions.Linux != nil {
    93  		webviewGpuPolicy = int(appoptions.Linux.WebviewGpuPolicy)
    94  	} else {
    95  		// workaround for https://github.com/wailsapp/wails/issues/2977
    96  		webviewGpuPolicy = int(linux.WebviewGpuPolicyNever)
    97  	}
    98  
    99  	webview := C.SetupWebview(
   100  		result.contentManager,
   101  		result.asGTKWindow(),
   102  		bool2Cint(appoptions.HideWindowOnClose),
   103  		C.int(webviewGpuPolicy),
   104  	)
   105  	result.webview = unsafe.Pointer(webview)
   106  	buttonPressedName := C.CString("button-press-event")
   107  	defer C.free(unsafe.Pointer(buttonPressedName))
   108  	C.ConnectButtons(unsafe.Pointer(webview))
   109  
   110  	if devtoolsEnabled {
   111  		C.DevtoolsEnabled(unsafe.Pointer(webview), C.int(1), C.bool(debug && appoptions.Debug.OpenInspectorOnStartup))
   112  		// Install Ctrl-Shift-F12 hotkey to call ShowInspector
   113  		C.InstallF12Hotkey(unsafe.Pointer(gtkWindow))
   114  	}
   115  
   116  	if !(debug || appoptions.EnableDefaultContextMenu) {
   117  		C.DisableContextMenu(unsafe.Pointer(webview))
   118  	}
   119  
   120  	// Set background colour
   121  	RGBA := appoptions.BackgroundColour
   122  	result.SetBackgroundColour(RGBA.R, RGBA.G, RGBA.B, RGBA.A)
   123  
   124  	// Setup window
   125  	result.SetKeepAbove(appoptions.AlwaysOnTop)
   126  	result.SetResizable(!appoptions.DisableResize)
   127  	result.SetDefaultSize(appoptions.Width, appoptions.Height)
   128  	result.SetDecorated(!appoptions.Frameless)
   129  	result.SetTitle(appoptions.Title)
   130  	result.SetMinSize(appoptions.MinWidth, appoptions.MinHeight)
   131  	result.SetMaxSize(appoptions.MaxWidth, appoptions.MaxHeight)
   132  	if appoptions.Linux != nil {
   133  		if appoptions.Linux.Icon != nil {
   134  			result.SetWindowIcon(appoptions.Linux.Icon)
   135  		}
   136  		if appoptions.Linux.WindowIsTranslucent {
   137  			C.SetWindowTransparency(gtkWindow)
   138  		}
   139  	}
   140  
   141  	// Menu
   142  	result.SetApplicationMenu(appoptions.Menu)
   143  
   144  	return result
   145  }
   146  
   147  func (w *Window) asGTKWidget() *C.GtkWidget {
   148  	return C.GTKWIDGET(w.gtkWindow)
   149  }
   150  
   151  func (w *Window) asGTKWindow() *C.GtkWindow {
   152  	return C.GTKWINDOW(w.gtkWindow)
   153  }
   154  
   155  func (w *Window) asGTKContainer() *C.GtkContainer {
   156  	return C.GTKCONTAINER(w.gtkWindow)
   157  }
   158  
   159  func (w *Window) cWebKitUserContentManager() *C.WebKitUserContentManager {
   160  	return (*C.WebKitUserContentManager)(w.contentManager)
   161  }
   162  
   163  func (w *Window) Fullscreen() {
   164  	C.ExecuteOnMainThread(C.Fullscreen, C.gpointer(w.asGTKWindow()))
   165  }
   166  
   167  func (w *Window) UnFullscreen() {
   168  	if !w.IsFullScreen() {
   169  		return
   170  	}
   171  	C.ExecuteOnMainThread(C.UnFullscreen, C.gpointer(w.asGTKWindow()))
   172  	w.SetMinSize(w.minWidth, w.minHeight)
   173  	w.SetMaxSize(w.maxWidth, w.maxHeight)
   174  }
   175  
   176  func (w *Window) Destroy() {
   177  	C.gtk_widget_destroy(w.asGTKWidget())
   178  	C.g_object_unref(C.gpointer(w.gtkWindow))
   179  }
   180  
   181  func (w *Window) Close() {
   182  	C.gtk_window_close(w.asGTKWindow())
   183  }
   184  
   185  func (w *Window) Center() {
   186  	C.ExecuteOnMainThread(C.Center, C.gpointer(w.asGTKWindow()))
   187  }
   188  
   189  func (w *Window) SetPosition(x int, y int) {
   190  	invokeOnMainThread(func() {
   191  		C.SetPosition(unsafe.Pointer(w.asGTKWindow()), C.int(x), C.int(y))
   192  	})
   193  }
   194  
   195  func (w *Window) Size() (int, int) {
   196  	var width, height C.int
   197  	var wg sync.WaitGroup
   198  	wg.Add(1)
   199  	invokeOnMainThread(func() {
   200  		C.gtk_window_get_size(w.asGTKWindow(), &width, &height)
   201  		wg.Done()
   202  	})
   203  	wg.Wait()
   204  	return int(width), int(height)
   205  }
   206  
   207  func (w *Window) GetPosition() (int, int) {
   208  	var width, height C.int
   209  	var wg sync.WaitGroup
   210  	wg.Add(1)
   211  	invokeOnMainThread(func() {
   212  		C.gtk_window_get_position(w.asGTKWindow(), &width, &height)
   213  		wg.Done()
   214  	})
   215  	wg.Wait()
   216  	return int(width), int(height)
   217  }
   218  
   219  func (w *Window) SetMaxSize(maxWidth int, maxHeight int) {
   220  	w.maxHeight = maxHeight
   221  	w.maxWidth = maxWidth
   222  	invokeOnMainThread(func() {
   223  		C.SetMinMaxSize(w.asGTKWindow(), C.int(w.minWidth), C.int(w.minHeight), C.int(w.maxWidth), C.int(w.maxHeight))
   224  	})
   225  }
   226  
   227  func (w *Window) SetMinSize(minWidth int, minHeight int) {
   228  	w.minHeight = minHeight
   229  	w.minWidth = minWidth
   230  	invokeOnMainThread(func() {
   231  		C.SetMinMaxSize(w.asGTKWindow(), C.int(w.minWidth), C.int(w.minHeight), C.int(w.maxWidth), C.int(w.maxHeight))
   232  	})
   233  }
   234  
   235  func (w *Window) Show() {
   236  	C.ExecuteOnMainThread(C.Show, C.gpointer(w.asGTKWindow()))
   237  }
   238  
   239  func (w *Window) Hide() {
   240  	C.ExecuteOnMainThread(C.Hide, C.gpointer(w.asGTKWindow()))
   241  }
   242  
   243  func (w *Window) Maximise() {
   244  	C.ExecuteOnMainThread(C.Maximise, C.gpointer(w.asGTKWindow()))
   245  }
   246  
   247  func (w *Window) UnMaximise() {
   248  	C.ExecuteOnMainThread(C.UnMaximise, C.gpointer(w.asGTKWindow()))
   249  }
   250  
   251  func (w *Window) Minimise() {
   252  	C.ExecuteOnMainThread(C.Minimise, C.gpointer(w.asGTKWindow()))
   253  }
   254  
   255  func (w *Window) UnMinimise() {
   256  	C.ExecuteOnMainThread(C.UnMinimise, C.gpointer(w.asGTKWindow()))
   257  }
   258  
   259  func (w *Window) IsFullScreen() bool {
   260  	result := C.IsFullscreen(w.asGTKWidget())
   261  	if result != 0 {
   262  		return true
   263  	}
   264  	return false
   265  }
   266  
   267  func (w *Window) IsMaximised() bool {
   268  	result := C.IsMaximised(w.asGTKWidget())
   269  	return result > 0
   270  }
   271  
   272  func (w *Window) IsMinimised() bool {
   273  	result := C.IsMinimised(w.asGTKWidget())
   274  	return result > 0
   275  }
   276  
   277  func (w *Window) IsNormal() bool {
   278  	return !w.IsMaximised() && !w.IsMinimised() && !w.IsFullScreen()
   279  }
   280  
   281  func (w *Window) SetBackgroundColour(r uint8, g uint8, b uint8, a uint8) {
   282  	windowIsTranslucent := false
   283  	if w.appoptions.Linux != nil && w.appoptions.Linux.WindowIsTranslucent {
   284  		windowIsTranslucent = true
   285  	}
   286  	data := C.RGBAOptions{
   287  		r:                   C.uchar(r),
   288  		g:                   C.uchar(g),
   289  		b:                   C.uchar(b),
   290  		a:                   C.uchar(a),
   291  		webview:             w.webview,
   292  		webviewBox:          unsafe.Pointer(w.webviewBox),
   293  		windowIsTranslucent: gtkBool(windowIsTranslucent),
   294  	}
   295  	invokeOnMainThread(func() { C.SetBackgroundColour(unsafe.Pointer(&data)) })
   296  
   297  }
   298  
   299  func (w *Window) SetWindowIcon(icon []byte) {
   300  	if len(icon) == 0 {
   301  		return
   302  	}
   303  	C.SetWindowIcon(w.asGTKWindow(), (*C.guchar)(&icon[0]), (C.gsize)(len(icon)))
   304  }
   305  
   306  func (w *Window) Run(url string) {
   307  	if w.menubar != nil {
   308  		C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.vbox)), w.menubar, 0, 0, 0)
   309  	}
   310  
   311  	C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.webviewBox)), C.GTKWIDGET(w.webview), 1, 1, 0)
   312  	C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.vbox)), w.webviewBox, 1, 1, 0)
   313  	_url := C.CString(url)
   314  	C.LoadIndex(w.webview, _url)
   315  	defer C.free(unsafe.Pointer(_url))
   316  	if w.appoptions.StartHidden {
   317  		w.Hide()
   318  	}
   319  	C.gtk_widget_show_all(w.asGTKWidget())
   320  	w.Center()
   321  	switch w.appoptions.WindowStartState {
   322  	case options.Fullscreen:
   323  		w.Fullscreen()
   324  	case options.Minimised:
   325  		w.Minimise()
   326  	case options.Maximised:
   327  		w.Maximise()
   328  	}
   329  }
   330  
   331  func (w *Window) SetKeepAbove(top bool) {
   332  	C.gtk_window_set_keep_above(w.asGTKWindow(), gtkBool(top))
   333  }
   334  
   335  func (w *Window) SetResizable(resizable bool) {
   336  	C.gtk_window_set_resizable(w.asGTKWindow(), gtkBool(resizable))
   337  }
   338  
   339  func (w *Window) SetDefaultSize(width int, height int) {
   340  	C.gtk_window_set_default_size(w.asGTKWindow(), C.int(width), C.int(height))
   341  }
   342  
   343  func (w *Window) SetSize(width int, height int) {
   344  	C.gtk_window_resize(w.asGTKWindow(), C.gint(width), C.gint(height))
   345  }
   346  
   347  func (w *Window) SetDecorated(frameless bool) {
   348  	C.gtk_window_set_decorated(w.asGTKWindow(), gtkBool(frameless))
   349  }
   350  
   351  func (w *Window) SetTitle(title string) {
   352  	C.SetTitle(w.asGTKWindow(), C.CString(title))
   353  }
   354  
   355  func (w *Window) ExecJS(js string) {
   356  	jscallback := C.JSCallback{
   357  		webview: w.webview,
   358  		script:  C.CString(js),
   359  	}
   360  	invokeOnMainThread(func() { C.ExecuteJS(unsafe.Pointer(&jscallback)) })
   361  }
   362  
   363  func (w *Window) StartDrag() {
   364  	C.StartDrag(w.webview, w.asGTKWindow())
   365  }
   366  
   367  func (w *Window) StartResize(edge uintptr) {
   368  	C.StartResize(w.webview, w.asGTKWindow(), C.GdkWindowEdge(edge))
   369  }
   370  
   371  func (w *Window) Quit() {
   372  	C.gtk_main_quit()
   373  }
   374  
   375  func (w *Window) OpenFileDialog(dialogOptions frontend.OpenDialogOptions, multipleFiles int, action C.GtkFileChooserAction) {
   376  
   377  	data := C.OpenFileDialogOptions{
   378  		window:        w.asGTKWindow(),
   379  		title:         C.CString(dialogOptions.Title),
   380  		multipleFiles: C.int(multipleFiles),
   381  		action:        action,
   382  	}
   383  
   384  	if len(dialogOptions.Filters) > 0 {
   385  		// Create filter array
   386  		mem := NewCalloc()
   387  		arraySize := len(dialogOptions.Filters) + 1
   388  		data.filters = C.AllocFileFilterArray((C.size_t)(arraySize))
   389  		filters := unsafe.Slice((**C.struct__GtkFileFilter)(unsafe.Pointer(data.filters)), arraySize)
   390  		for index, filter := range dialogOptions.Filters {
   391  			thisFilter := C.gtk_file_filter_new()
   392  			C.g_object_ref(C.gpointer(thisFilter))
   393  			if filter.DisplayName != "" {
   394  				cName := mem.String(filter.DisplayName)
   395  				C.gtk_file_filter_set_name(thisFilter, cName)
   396  			}
   397  			if filter.Pattern != "" {
   398  				for _, thisPattern := range strings.Split(filter.Pattern, ";") {
   399  					cThisPattern := mem.String(thisPattern)
   400  					C.gtk_file_filter_add_pattern(thisFilter, cThisPattern)
   401  				}
   402  			}
   403  			// Add filter to array
   404  			filters[index] = thisFilter
   405  		}
   406  		mem.Free()
   407  		filters[arraySize-1] = nil
   408  	}
   409  
   410  	if dialogOptions.CanCreateDirectories {
   411  		data.createDirectories = C.int(1)
   412  	}
   413  
   414  	if dialogOptions.ShowHiddenFiles {
   415  		data.showHiddenFiles = C.int(1)
   416  	}
   417  
   418  	if dialogOptions.DefaultFilename != "" {
   419  		data.defaultFilename = C.CString(dialogOptions.DefaultFilename)
   420  	}
   421  
   422  	if dialogOptions.DefaultDirectory != "" {
   423  		data.defaultDirectory = C.CString(dialogOptions.DefaultDirectory)
   424  	}
   425  
   426  	invokeOnMainThread(func() { C.Opendialog(unsafe.Pointer(&data)) })
   427  }
   428  
   429  func (w *Window) MessageDialog(dialogOptions frontend.MessageDialogOptions) {
   430  
   431  	data := C.MessageDialogOptions{
   432  		window:  w.gtkWindow,
   433  		title:   C.CString(dialogOptions.Title),
   434  		message: C.CString(dialogOptions.Message),
   435  	}
   436  	switch dialogOptions.Type {
   437  	case frontend.InfoDialog:
   438  		data.messageType = C.int(0)
   439  	case frontend.ErrorDialog:
   440  		data.messageType = C.int(1)
   441  	case frontend.QuestionDialog:
   442  		data.messageType = C.int(2)
   443  	case frontend.WarningDialog:
   444  		data.messageType = C.int(3)
   445  	}
   446  	invokeOnMainThread(func() { C.MessageDialog(unsafe.Pointer(&data)) })
   447  }
   448  
   449  func (w *Window) ToggleMaximise() {
   450  	if w.IsMaximised() {
   451  		w.UnMaximise()
   452  	} else {
   453  		w.Maximise()
   454  	}
   455  }
   456  
   457  func (w *Window) ShowInspector() {
   458  	invokeOnMainThread(func() { C.ShowInspector(w.webview) })
   459  }
   460  
   461  // showModalDialogAndExit shows a modal dialog and exits the app.
   462  func showModalDialogAndExit(title, message string) {
   463  	go func() {
   464  		data := C.MessageDialogOptions{
   465  			title:       C.CString(title),
   466  			message:     C.CString(message),
   467  			messageType: C.int(1),
   468  		}
   469  
   470  		C.MessageDialog(unsafe.Pointer(&data))
   471  	}()
   472  
   473  	<-messageDialogResult
   474  	log.Fatal(message)
   475  }