mirror of
https://github.com/taigrr/wails.git
synced 2026-04-17 04:05:12 -07:00
Compare commits
16 Commits
v2.0.0-alp
...
v2.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a1a4d75ad | ||
|
|
b552c16539 | ||
|
|
d574d53fca | ||
|
|
2b69ac8391 | ||
|
|
e7cb40d5ee | ||
|
|
f409dbdab1 | ||
|
|
1748e8479f | ||
|
|
4f2788a294 | ||
|
|
856b81ab04 | ||
|
|
76aab2271c | ||
|
|
3192026e6d | ||
|
|
90dd05e52e | ||
|
|
e73cf44ddc | ||
|
|
4c2804eac9 | ||
|
|
e1dd77fd3f | ||
|
|
b69f1e6c43 |
@@ -20,7 +20,6 @@ func NewApp() *App {
|
|||||||
func (b *App) startup(runtime *wails.Runtime) {
|
func (b *App) startup(runtime *wails.Runtime) {
|
||||||
// Perform your setup here
|
// Perform your setup here
|
||||||
b.runtime = runtime
|
b.runtime = runtime
|
||||||
runtime.Window.SetTitle("{{.ProjectName}}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// shutdown is called at application termination
|
// shutdown is called at application termination
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ func NewApp() *App {
|
|||||||
func (b *App) startup(runtime *wails.Runtime) {
|
func (b *App) startup(runtime *wails.Runtime) {
|
||||||
// Perform your setup here
|
// Perform your setup here
|
||||||
b.runtime = runtime
|
b.runtime = runtime
|
||||||
runtime.Window.SetTitle("{{.ProjectName}}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// shutdown is called at application termination
|
// shutdown is called at application termination
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
var version = "v2.0.0-alpha.71"
|
var version = "v2.0.0-alpha.73"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ require (
|
|||||||
github.com/leaanthony/go-ansi-parser v1.0.1
|
github.com/leaanthony/go-ansi-parser v1.0.1
|
||||||
github.com/leaanthony/go-common-file-dialog v1.0.3
|
github.com/leaanthony/go-common-file-dialog v1.0.3
|
||||||
github.com/leaanthony/gosod v1.0.1
|
github.com/leaanthony/gosod v1.0.1
|
||||||
|
github.com/leaanthony/idgen v1.0.0
|
||||||
github.com/leaanthony/slicer v1.5.0
|
github.com/leaanthony/slicer v1.5.0
|
||||||
github.com/leaanthony/webview2runtime v1.1.0
|
github.com/leaanthony/webview2runtime v1.1.0
|
||||||
github.com/leaanthony/winicon v0.0.0-20200606125418-4419cea822a0
|
github.com/leaanthony/winicon v0.0.0-20200606125418-4419cea822a0
|
||||||
@@ -33,6 +34,7 @@ require (
|
|||||||
github.com/tidwall/sjson v1.1.7
|
github.com/tidwall/sjson v1.1.7
|
||||||
github.com/wzshiming/ctc v1.2.3
|
github.com/wzshiming/ctc v1.2.3
|
||||||
github.com/xyproto/xpm v1.2.1
|
github.com/xyproto/xpm v1.2.1
|
||||||
|
github.com/ztrue/tracerr v0.3.0
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
|
||||||
golang.org/x/mod v0.4.1 // indirect
|
golang.org/x/mod v0.4.1 // indirect
|
||||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897
|
golang.org/x/net v0.0.0-20210326060303-6b1517762897
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ github.com/leaanthony/go-common-file-dialog v1.0.3 h1:O0uGjKnWtdEADGrkg+TyAAbZyl
|
|||||||
github.com/leaanthony/go-common-file-dialog v1.0.3/go.mod h1:TGhEc9eSJgRsupZ+iH1ZgAOnEo9zp05cRH2j08RPrF0=
|
github.com/leaanthony/go-common-file-dialog v1.0.3/go.mod h1:TGhEc9eSJgRsupZ+iH1ZgAOnEo9zp05cRH2j08RPrF0=
|
||||||
github.com/leaanthony/gosod v1.0.1 h1:F+4c3DmEBfigi7oAswCV2RpQ+k4DcNbhuCZUGdBHacQ=
|
github.com/leaanthony/gosod v1.0.1 h1:F+4c3DmEBfigi7oAswCV2RpQ+k4DcNbhuCZUGdBHacQ=
|
||||||
github.com/leaanthony/gosod v1.0.1/go.mod h1:W8RyeSFBXu7RpIxPGEJfW4moSyGGEjlJMLV25wEbAdU=
|
github.com/leaanthony/gosod v1.0.1/go.mod h1:W8RyeSFBXu7RpIxPGEJfW4moSyGGEjlJMLV25wEbAdU=
|
||||||
|
github.com/leaanthony/idgen v1.0.0 h1:IZreR+JGEzFV4yeVuBZA25gM0keUoFy+RDUldncQ+Jw=
|
||||||
|
github.com/leaanthony/idgen v1.0.0/go.mod h1:4nBZnt8ml/f/ic/EVQuLxuj817RccT2fyrUaZFxrcVA=
|
||||||
github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
|
github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
|
||||||
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
|
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
|
||||||
github.com/leaanthony/webview2runtime v1.1.0 h1:N0pv55ift8XtqozIp4PNOtRCJ/Qdd/qzx80lUpalS4c=
|
github.com/leaanthony/webview2runtime v1.1.0 h1:N0pv55ift8XtqozIp4PNOtRCJ/Qdd/qzx80lUpalS4c=
|
||||||
@@ -107,6 +109,8 @@ github.com/leaanthony/winicon v0.0.0-20200606125418-4419cea822a0 h1:FPGYnfxuuxqC
|
|||||||
github.com/leaanthony/winicon v0.0.0-20200606125418-4419cea822a0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU=
|
github.com/leaanthony/winicon v0.0.0-20200606125418-4419cea822a0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU=
|
||||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e h1:9MlwzLdW7QSDrhDjFlsEYmxpFyIoXmYRon3dt0io31k=
|
||||||
|
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
@@ -166,6 +170,8 @@ github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6e
|
|||||||
github.com/xyproto/xpm v1.2.1 h1:trdvGjjWBsOOKzBBUPT6JvaIQM3acJEEYfbxN7M96wg=
|
github.com/xyproto/xpm v1.2.1 h1:trdvGjjWBsOOKzBBUPT6JvaIQM3acJEEYfbxN7M96wg=
|
||||||
github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY=
|
github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/ztrue/tracerr v0.3.0 h1:lDi6EgEYhPYPnKcjsYzmWw4EkFEoA/gfe+I9Y5f+h6Y=
|
||||||
|
github.com/ztrue/tracerr v0.3.0/go.mod h1:qEalzze4VN9O8tnhBXScfCrmoJo10o8TN5ciKjm6Mww=
|
||||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ func CreateApp(appoptions *options.App) (*App, error) {
|
|||||||
menuManager := menumanager.NewManager()
|
menuManager := menumanager.NewManager()
|
||||||
|
|
||||||
// Process the application menu
|
// Process the application menu
|
||||||
menuManager.SetApplicationMenu(options.GetApplicationMenu(appoptions))
|
appMenu := options.GetApplicationMenu(appoptions)
|
||||||
|
menuManager.SetApplicationMenu(appMenu)
|
||||||
|
|
||||||
// Process context menus
|
// Process context menus
|
||||||
contextMenus := options.GetContextMenus(appoptions)
|
contextMenus := options.GetContextMenus(appoptions)
|
||||||
|
|||||||
@@ -9,11 +9,18 @@ import (
|
|||||||
|
|
||||||
func (a *App) PreflightChecks(options *options.App) error {
|
func (a *App) PreflightChecks(options *options.App) error {
|
||||||
|
|
||||||
|
_ = options
|
||||||
|
|
||||||
// Process the webview2 runtime situation. We can pass a strategy in via the `webview2` flag for `wails build`.
|
// Process the webview2 runtime situation. We can pass a strategy in via the `webview2` flag for `wails build`.
|
||||||
// This will determine how wv2runtime.Process will handle a lack of valid runtime.
|
// This will determine how wv2runtime.Process will handle a lack of valid runtime.
|
||||||
err := wv2runtime.Process()
|
installedVersion, err := wv2runtime.Process()
|
||||||
|
if installedVersion != nil {
|
||||||
|
a.logger.Debug("WebView2 Runtime installed: Name: '%s' Version:'%s' Location:'%s'. Minimum version required: %s.",
|
||||||
|
installedVersion.Name, installedVersion.Version, installedVersion.Location, wv2runtime.MinimumRuntimeVersion)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,8 +284,8 @@ func (c *Client) DarkModeEnabled(callbackID string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetApplicationMenu sets the application menu
|
// SetApplicationMenu sets the application menu
|
||||||
func (c *Client) SetApplicationMenu(applicationMenuJSON string) {
|
func (c *Client) SetApplicationMenu(_ string) {
|
||||||
C.SetApplicationMenu(c.app.app, c.app.string2CString(applicationMenuJSON))
|
c.updateApplicationMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTrayMenu sets the tray menu
|
// SetTrayMenu sets the tray menu
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// License included in README.md
|
// License included in README.md
|
||||||
|
|
||||||
#include "ffenestri_windows.h"
|
#include "ffenestri_windows.h"
|
||||||
|
#include "shellscalingapi.h"
|
||||||
#include "wv2ComHandler_windows.h"
|
#include "wv2ComHandler_windows.h"
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
@@ -42,6 +43,30 @@ char* LPWSTRToCstr(LPWSTR input) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Credit: https://building.enlyze.com/posts/writing-win32-apps-like-its-2020-part-3/
|
||||||
|
typedef int (__cdecl *PGetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE,UINT*,UINT*);
|
||||||
|
void getDPIForWindow(struct Application *app)
|
||||||
|
{
|
||||||
|
HMODULE hShcore = LoadLibraryW(L"shcore");
|
||||||
|
if (hShcore)
|
||||||
|
{
|
||||||
|
PGetDpiForMonitor pGetDpiForMonitor = reinterpret_cast<PGetDpiForMonitor>(GetProcAddress(hShcore, "GetDpiForMonitor"));
|
||||||
|
if (pGetDpiForMonitor)
|
||||||
|
{
|
||||||
|
HMONITOR hMonitor = MonitorFromWindow(app->window, MONITOR_DEFAULTTOPRIMARY);
|
||||||
|
pGetDpiForMonitor(hMonitor, (MONITOR_DPI_TYPE)0, &app->dpix, &app->dpiy);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We couldn't get the window's DPI above, so get the DPI of the primary monitor
|
||||||
|
// using an API that is available in all Windows versions.
|
||||||
|
HDC hScreenDC = GetDC(0);
|
||||||
|
app->dpix = GetDeviceCaps(hScreenDC, LOGPIXELSX);
|
||||||
|
app->dpiy = GetDeviceCaps(hScreenDC, LOGPIXELSY);
|
||||||
|
ReleaseDC(0, hScreenDC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Application *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel, int hideWindowOnClose) {
|
struct Application *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel, int hideWindowOnClose) {
|
||||||
|
|
||||||
// Create application
|
// Create application
|
||||||
@@ -88,6 +113,9 @@ struct Application *NewApplication(const char *title, int width, int height, int
|
|||||||
// Used to remember the window location when going fullscreen
|
// Used to remember the window location when going fullscreen
|
||||||
result->previousPlacement = { sizeof(result->previousPlacement) };
|
result->previousPlacement = { sizeof(result->previousPlacement) };
|
||||||
|
|
||||||
|
// DPI
|
||||||
|
result->dpix = result->dpiy = 0;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +170,14 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|||||||
|
|
||||||
switch(msg) {
|
switch(msg) {
|
||||||
|
|
||||||
|
case WM_CREATE: {
|
||||||
|
createApplicationMenu(hwnd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WM_COMMAND:
|
||||||
|
menuClicked(LOWORD(wParam));
|
||||||
|
break;
|
||||||
|
|
||||||
case WM_CLOSE: {
|
case WM_CLOSE: {
|
||||||
DestroyWindow( app->window );
|
DestroyWindow( app->window );
|
||||||
break;
|
break;
|
||||||
@@ -171,30 +207,28 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get pixel density
|
// update DPI
|
||||||
HDC hDC = GetDC(NULL);
|
getDPIForWindow(app);
|
||||||
double DPIScaleX = GetDeviceCaps(hDC, 88)/96.0;
|
double DPIScaleX = app->dpix/96.0;
|
||||||
double DPIScaleY = GetDeviceCaps(hDC, 90)/96.0;
|
double DPIScaleY = app->dpiy/96.0;
|
||||||
ReleaseDC(NULL, hDC);
|
|
||||||
|
|
||||||
RECT rcClient, rcWind;
|
RECT rcWind;
|
||||||
POINT ptDiff;
|
POINT ptDiff;
|
||||||
GetClientRect(hwnd, &rcClient);
|
|
||||||
GetWindowRect(hwnd, &rcWind);
|
GetWindowRect(hwnd, &rcWind);
|
||||||
|
|
||||||
int widthExtra = (rcWind.right - rcWind.left) - rcClient.right;
|
int widthExtra = (rcWind.right - rcWind.left);
|
||||||
int heightExtra = (rcWind.bottom - rcWind.top) - rcClient.bottom;
|
int heightExtra = (rcWind.bottom - rcWind.top);
|
||||||
|
|
||||||
LPMINMAXINFO mmi = (LPMINMAXINFO) lParam;
|
LPMINMAXINFO mmi = (LPMINMAXINFO) lParam;
|
||||||
if (app->minWidth > 0 && app->minHeight > 0) {
|
if (app->minWidth > 0 && app->minHeight > 0) {
|
||||||
mmi->ptMinTrackSize.x = app->minWidth * DPIScaleX + widthExtra;
|
mmi->ptMinTrackSize.x = app->minWidth * DPIScaleX;
|
||||||
mmi->ptMinTrackSize.y = app->minHeight * DPIScaleY + heightExtra;
|
mmi->ptMinTrackSize.y = app->minHeight * DPIScaleY;
|
||||||
}
|
}
|
||||||
if (app->maxWidth > 0 && app->maxHeight > 0) {
|
if (app->maxWidth > 0 && app->maxHeight > 0) {
|
||||||
mmi->ptMaxSize.x = app->maxWidth * DPIScaleX + widthExtra;
|
mmi->ptMaxSize.x = app->maxWidth * DPIScaleX;
|
||||||
mmi->ptMaxSize.y = app->maxHeight * DPIScaleY + heightExtra;
|
mmi->ptMaxSize.y = app->maxHeight * DPIScaleY;
|
||||||
mmi->ptMaxTrackSize.x = app->maxWidth * DPIScaleX + widthExtra;
|
mmi->ptMaxTrackSize.x = app->maxWidth * DPIScaleX;
|
||||||
mmi->ptMaxTrackSize.y = app->maxHeight * DPIScaleY + heightExtra;
|
mmi->ptMaxTrackSize.y = app->maxHeight * DPIScaleY;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -294,7 +328,6 @@ void completed(struct Application* app) {
|
|||||||
messageFromWindowCallback(readyMessage.c_str());
|
messageFromWindowCallback(readyMessage.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
bool initWebView2(struct Application *app, int debugEnabled, messageCallback cb) {
|
bool initWebView2(struct Application *app, int debugEnabled, messageCallback cb) {
|
||||||
|
|
||||||
@@ -384,6 +417,7 @@ bool initWebView2(struct Application *app, int debugEnabled, messageCallback cb)
|
|||||||
}
|
}
|
||||||
app->webviewController = controller;
|
app->webviewController = controller;
|
||||||
app->webview = webview;
|
app->webview = webview;
|
||||||
|
|
||||||
// Resize WebView to fit the bounds of the parent window
|
// Resize WebView to fit the bounds of the parent window
|
||||||
RECT bounds;
|
RECT bounds;
|
||||||
GetClientRect(app->window, &bounds);
|
GetClientRect(app->window, &bounds);
|
||||||
@@ -395,7 +429,64 @@ bool initWebView2(struct Application *app, int debugEnabled, messageCallback cb)
|
|||||||
LPCWSTR html = (LPCWSTR) cstrToLPWSTR((char*)assets[0]);
|
LPCWSTR html = (LPCWSTR) cstrToLPWSTR((char*)assets[0]);
|
||||||
app->webview->Navigate(html);
|
app->webview->Navigate(html);
|
||||||
|
|
||||||
|
if( app->webviewIsTranparent ) {
|
||||||
|
wchar_t szBuff[64];
|
||||||
|
ICoreWebView2Controller2 *wc2;
|
||||||
|
wc2 = nullptr;
|
||||||
|
app->webviewController->QueryInterface(IID_ICoreWebView2Controller2, (void**)&wc2);
|
||||||
|
|
||||||
|
COREWEBVIEW2_COLOR wvColor;
|
||||||
|
wvColor.R = app->backgroundColour.R;
|
||||||
|
wvColor.G = app->backgroundColour.G;
|
||||||
|
wvColor.B = app->backgroundColour.B;
|
||||||
|
wvColor.A = app->backgroundColour.A == 0 ? 0 : 255;
|
||||||
|
if( app->windowBackgroundIsTranslucent ) {
|
||||||
|
wvColor.A = 0;
|
||||||
|
}
|
||||||
|
HRESULT result = wc2->put_DefaultBackgroundColor(wvColor);
|
||||||
|
if (!SUCCEEDED(result))
|
||||||
|
{
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):
|
||||||
|
{
|
||||||
|
MessageBox(
|
||||||
|
app->window,
|
||||||
|
L"Couldn't find Edge installation. "
|
||||||
|
"Do you have a version installed that's compatible with this "
|
||||||
|
"WebView2 SDK version?",
|
||||||
|
nullptr, MB_OK);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case HRESULT_FROM_WIN32(ERROR_FILE_EXISTS):
|
||||||
|
{
|
||||||
|
MessageBox(
|
||||||
|
app->window, L"User data folder cannot be created because a file with the same name already exists.", nullptr, MB_OK);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case E_ACCESSDENIED:
|
||||||
|
{
|
||||||
|
MessageBox(
|
||||||
|
app->window, L"Unable to create user data folder, Access Denied.", nullptr, MB_OK);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case E_FAIL:
|
||||||
|
{
|
||||||
|
MessageBox(
|
||||||
|
app->window, L"Edge runtime unable to start", nullptr, MB_OK);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
MessageBox(app->window, L"Failed to create WebView2 environment", nullptr, MB_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
messageFromWindowCallback("Ej{\"name\":\"wails:launched\",\"data\":[]}");
|
messageFromWindowCallback("Ej{\"name\":\"wails:launched\",\"data\":[]}");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,61 +583,6 @@ void Run(struct Application* app, int argc, char **argv) {
|
|||||||
// Add webview2
|
// Add webview2
|
||||||
initWebView2(app, debug, initialCallback);
|
initWebView2(app, debug, initialCallback);
|
||||||
|
|
||||||
if( app->webviewIsTranparent ) {
|
|
||||||
wchar_t szBuff[64];
|
|
||||||
ICoreWebView2Controller2 *wc2;
|
|
||||||
wc2 = nullptr;
|
|
||||||
app->webviewController->QueryInterface(IID_ICoreWebView2Controller2, (void**)&wc2);
|
|
||||||
|
|
||||||
COREWEBVIEW2_COLOR wvColor;
|
|
||||||
wvColor.R = app->backgroundColour.R;
|
|
||||||
wvColor.G = app->backgroundColour.G;
|
|
||||||
wvColor.B = app->backgroundColour.B;
|
|
||||||
wvColor.A = app->backgroundColour.A == 0 ? 0 : 255;
|
|
||||||
if( app->windowBackgroundIsTranslucent ) {
|
|
||||||
wvColor.A = 0;
|
|
||||||
}
|
|
||||||
HRESULT result = wc2->put_DefaultBackgroundColor(wvColor);
|
|
||||||
if (!SUCCEEDED(result))
|
|
||||||
{
|
|
||||||
switch (result)
|
|
||||||
{
|
|
||||||
case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):
|
|
||||||
{
|
|
||||||
MessageBox(
|
|
||||||
app->window,
|
|
||||||
L"Couldn't find Edge installation. "
|
|
||||||
"Do you have a version installed that's compatible with this "
|
|
||||||
"WebView2 SDK version?",
|
|
||||||
nullptr, MB_OK);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case HRESULT_FROM_WIN32(ERROR_FILE_EXISTS):
|
|
||||||
{
|
|
||||||
MessageBox(
|
|
||||||
app->window, L"User data folder cannot be created because a file with the same name already exists.", nullptr, MB_OK);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case E_ACCESSDENIED:
|
|
||||||
{
|
|
||||||
MessageBox(
|
|
||||||
app->window, L"Unable to create user data folder, Access Denied.", nullptr, MB_OK);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case E_FAIL:
|
|
||||||
{
|
|
||||||
MessageBox(
|
|
||||||
app->window, L"Edge runtime unable to start", nullptr, MB_OK);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
MessageBox(app->window, L"Failed to create WebView2 environment", nullptr, MB_OK);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main event loop
|
// Main event loop
|
||||||
MSG msg;
|
MSG msg;
|
||||||
|
|||||||
@@ -13,9 +13,30 @@ extern void DisableWindowIcon(struct Application* app);
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
import (
|
||||||
|
"github.com/ztrue/tracerr"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup the global caches
|
||||||
|
var globalCheckboxCache = NewCheckboxCache()
|
||||||
|
var globalRadioGroupCache = NewRadioGroupCache()
|
||||||
|
var globalRadioGroupMap = NewRadioGroupMap()
|
||||||
|
var globalApplicationMenu *Menu
|
||||||
|
|
||||||
|
type menuType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
appMenuType menuType = "ApplicationMenu"
|
||||||
|
contextMenuType
|
||||||
|
trayMenuType
|
||||||
|
)
|
||||||
|
|
||||||
func (a *Application) processPlatformSettings() error {
|
func (a *Application) processPlatformSettings() error {
|
||||||
|
|
||||||
|
menuManager = a.menuManager
|
||||||
config := a.config.Windows
|
config := a.config.Windows
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -34,12 +55,9 @@ func (a *Application) processPlatformSettings() error {
|
|||||||
C.DisableWindowIcon(a.app)
|
C.DisableWindowIcon(a.app)
|
||||||
}
|
}
|
||||||
|
|
||||||
//// Process menu
|
// Unfortunately, we need to store this in the package variable so the C callback can see it
|
||||||
////applicationMenu := options.GetApplicationMenu(a.config)
|
applicationMenu = a.menuManager.GetProcessedApplicationMenu()
|
||||||
//applicationMenu := a.menuManager.GetApplicationMenuJSON()
|
|
||||||
//if applicationMenu != "" {
|
|
||||||
// C.SetApplicationMenu(a.app, a.string2CString(applicationMenu))
|
|
||||||
//}
|
|
||||||
//
|
//
|
||||||
//// Process tray
|
//// Process tray
|
||||||
//trays, err := a.menuManager.GetTrayMenus()
|
//trays, err := a.menuManager.GetTrayMenus()
|
||||||
@@ -70,3 +88,98 @@ func (a *Application) processPlatformSettings() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) updateApplicationMenu() {
|
||||||
|
applicationMenu = c.app.menuManager.GetProcessedApplicationMenu()
|
||||||
|
createApplicationMenu(uintptr(C.GetWindowHandle(c.app.app)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Application Menu
|
||||||
|
----------------
|
||||||
|
There's only 1 application menu and this is where we create it. This method
|
||||||
|
is called from C after the window is created and the WM_CREATE message has
|
||||||
|
been sent.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func checkFatal(err error) {
|
||||||
|
if err != nil {
|
||||||
|
tracerr.PrintSourceColor(err)
|
||||||
|
globalRadioGroupCache.Dump()
|
||||||
|
globalRadioGroupMap.Dump()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export createApplicationMenu
|
||||||
|
func createApplicationMenu(hwnd uintptr) {
|
||||||
|
|
||||||
|
if applicationMenu == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
window := win32Window(hwnd)
|
||||||
|
|
||||||
|
if globalApplicationMenu != nil {
|
||||||
|
checkFatal(globalApplicationMenu.Destroy())
|
||||||
|
}
|
||||||
|
|
||||||
|
globalApplicationMenu, err = createMenu(applicationMenu, appMenuType)
|
||||||
|
checkFatal(err)
|
||||||
|
|
||||||
|
err = setWindowMenu(window, globalApplicationMenu.menu)
|
||||||
|
checkFatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This method is called by C when a menu item is pressed
|
||||||
|
*/
|
||||||
|
|
||||||
|
//export menuClicked
|
||||||
|
func menuClicked(id uint32) {
|
||||||
|
win32MenuID := win32MenuItemID(id)
|
||||||
|
//println("Got click from menu id", win32MenuID)
|
||||||
|
|
||||||
|
// Get the menu from the cache
|
||||||
|
menuItemDetails := getMenuCacheEntry(win32MenuID)
|
||||||
|
wailsMenuID := wailsMenuItemID(menuItemDetails.item.ID)
|
||||||
|
|
||||||
|
//println("Got click from menu id", win32MenuID, "- wails menu ID", wailsMenuID)
|
||||||
|
//spew.Dump(menuItemDetails)
|
||||||
|
|
||||||
|
switch menuItemDetails.item.Type {
|
||||||
|
case menu.CheckboxType:
|
||||||
|
|
||||||
|
// Determine if the menu is set or not
|
||||||
|
res, _, err := win32GetMenuState.Call(uintptr(menuItemDetails.parent), uintptr(id), uintptr(MF_BYCOMMAND))
|
||||||
|
if int(res) == -1 {
|
||||||
|
checkFatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
flag := MF_CHECKED
|
||||||
|
if uint32(res) == MF_CHECKED {
|
||||||
|
flag = MF_UNCHECKED
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, menuid := range globalCheckboxCache.win32MenuIDsForWailsMenuID(wailsMenuID) {
|
||||||
|
//println("setting menuid", menuid, "with flag", flag)
|
||||||
|
menuItemDetails := getMenuCacheEntry(menuid)
|
||||||
|
res, _, err = win32CheckMenuItem.Call(uintptr(menuItemDetails.parent), uintptr(menuid), uintptr(flag))
|
||||||
|
if int(res) == -1 {
|
||||||
|
checkFatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case menu.RadioType:
|
||||||
|
err := selectRadioItemFromWailsMenuID(wailsMenuID, win32MenuID)
|
||||||
|
checkFatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the click error - it's not fatal
|
||||||
|
err := menuManager.ProcessClick(menuItemDetails.item.ID, "", string(menuItemDetails.menuType), "")
|
||||||
|
if err != nil {
|
||||||
|
println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ struct Application{
|
|||||||
// placeholders
|
// placeholders
|
||||||
char* bindings;
|
char* bindings;
|
||||||
char* initialCode;
|
char* initialCode;
|
||||||
|
|
||||||
|
// DPI
|
||||||
|
UINT dpix;
|
||||||
|
UINT dpiy;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define ON_MAIN_THREAD(code) dispatch( [=]{ code; } )
|
#define ON_MAIN_THREAD(code) dispatch( [=]{ code; } )
|
||||||
@@ -80,6 +84,8 @@ extern "C" {
|
|||||||
void DisableWindowIcon(struct Application* app);
|
void DisableWindowIcon(struct Application* app);
|
||||||
void messageFromWindowCallback(const char *);
|
void messageFromWindowCallback(const char *);
|
||||||
void* GetWindowHandle(struct Application*);
|
void* GetWindowHandle(struct Application*);
|
||||||
|
void createApplicationMenu(HWND hwnd);
|
||||||
|
void menuClicked(UINT id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"github.com/leaanthony/webview2runtime"
|
"github.com/leaanthony/webview2runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
const minimumRuntimeVersion string = "91.0.864.48"
|
const MinimumRuntimeVersion string = "91.0.864.48"
|
||||||
|
|
||||||
type installationStatus int
|
type installationStatus int
|
||||||
|
|
||||||
@@ -14,21 +14,21 @@ const (
|
|||||||
installed
|
installed
|
||||||
)
|
)
|
||||||
|
|
||||||
func Process() error {
|
func Process() (*webview2runtime.Info, error) {
|
||||||
installStatus := needsInstalling
|
installStatus := needsInstalling
|
||||||
installedVersion := webview2runtime.GetInstalledVersion()
|
installedVersion := webview2runtime.GetInstalledVersion()
|
||||||
if installedVersion != nil {
|
if installedVersion != nil {
|
||||||
installStatus = installed
|
installStatus = installed
|
||||||
updateRequired, err := installedVersion.IsOlderThan(minimumRuntimeVersion)
|
updateRequired, err := installedVersion.IsOlderThan(MinimumRuntimeVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = webview2runtime.Error(err.Error(), "Error")
|
_ = webview2runtime.Error(err.Error(), "Error")
|
||||||
return err
|
return installedVersion, err
|
||||||
}
|
}
|
||||||
// Installed and does not require updating
|
// Installed and does not require updating
|
||||||
if !updateRequired {
|
if !updateRequired {
|
||||||
return nil
|
return installedVersion, nil
|
||||||
}
|
}
|
||||||
installStatus = needsUpdating
|
|
||||||
}
|
}
|
||||||
return doInstallationStrategy(installStatus)
|
return installedVersion, doInstallationStrategy(installStatus)
|
||||||
}
|
}
|
||||||
|
|||||||
93
v2/internal/ffenestri/windows_checkboxes.go
Normal file
93
v2/internal/ffenestri/windows_checkboxes.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
//+build windows
|
||||||
|
|
||||||
|
package ffenestri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/leaanthony/slicer"
|
||||||
|
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"text/tabwriter"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Checkbox Cache
|
||||||
|
--------------
|
||||||
|
The checkbox cache keeps a list of IDs that are associated with the same checkbox menu item.
|
||||||
|
This can happen when a checkbox is used in an application menu and a tray menu, eg "start at login".
|
||||||
|
The cache is used to bulk toggle the menu items when one is clicked.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
type CheckboxCache struct {
|
||||||
|
cache map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckboxCache() *CheckboxCache {
|
||||||
|
return &CheckboxCache{
|
||||||
|
cache: make(map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CheckboxCache) Dump() {
|
||||||
|
// Start a new tabwriter
|
||||||
|
w := new(tabwriter.Writer)
|
||||||
|
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
|
||||||
|
|
||||||
|
println("---------------- Checkbox", c, "Dump ----------------")
|
||||||
|
for _, processedMenu := range c.cache {
|
||||||
|
println("Menu", processedMenu)
|
||||||
|
for wailsMenuItemID, win32menus := range processedMenu {
|
||||||
|
println(" WailsMenu: ", wailsMenuItemID)
|
||||||
|
menus := slicer.String()
|
||||||
|
for _, win32menu := range win32menus {
|
||||||
|
menus.Add(fmt.Sprintf("%v", win32menu))
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(w, "%s\t%s\n", wailsMenuItemID, menus.Join(", "))
|
||||||
|
_ = w.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CheckboxCache) addToCheckboxCache(menu *menumanager.ProcessedMenu, item wailsMenuItemID, menuID win32MenuItemID) {
|
||||||
|
|
||||||
|
// Get map for menu
|
||||||
|
if c.cache[menu] == nil {
|
||||||
|
c.cache[menu] = make(map[wailsMenuItemID][]win32MenuItemID)
|
||||||
|
}
|
||||||
|
menuMap := c.cache[menu]
|
||||||
|
|
||||||
|
// Ensure we have a slice
|
||||||
|
if menuMap[item] == nil {
|
||||||
|
menuMap[item] = []win32MenuItemID{}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
menuMap[item] = append(menuMap[item], menuID)
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CheckboxCache) removeMenuFromCheckboxCache(menu *menumanager.ProcessedMenu) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
delete(c.cache, menu)
|
||||||
|
c.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// win32MenuIDsForWailsMenuID returns all win32menuids that are used for a wails menu item id across
|
||||||
|
// all menus
|
||||||
|
func (c *CheckboxCache) win32MenuIDsForWailsMenuID(item wailsMenuItemID) []win32MenuItemID {
|
||||||
|
c.mutex.Lock()
|
||||||
|
result := []win32MenuItemID{}
|
||||||
|
for _, menu := range c.cache {
|
||||||
|
ids := menu[item]
|
||||||
|
if ids != nil {
|
||||||
|
result = append(result, ids...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mutex.Unlock()
|
||||||
|
return result
|
||||||
|
}
|
||||||
28
v2/internal/ffenestri/windows_errorhandler_debug.go
Normal file
28
v2/internal/ffenestri/windows_errorhandler_debug.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
//+build windows,debug
|
||||||
|
|
||||||
|
package ffenestri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/ztrue/tracerr"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func wall(err error, inputs ...interface{}) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pc, _, _, _ := runtime.Caller(1)
|
||||||
|
funcName := runtime.FuncForPC(pc).Name()
|
||||||
|
splitName := strings.Split(funcName, ".")
|
||||||
|
message := "[" + splitName[len(splitName)-1] + "]"
|
||||||
|
if len(inputs) > 0 {
|
||||||
|
params := []string{}
|
||||||
|
for _, param := range inputs {
|
||||||
|
params = append(params, fmt.Sprintf("%v", param))
|
||||||
|
}
|
||||||
|
message += "(" + strings.Join(params, " ") + ")"
|
||||||
|
}
|
||||||
|
return tracerr.Errorf(message)
|
||||||
|
}
|
||||||
47
v2/internal/ffenestri/windows_errorhandler_production.go
Normal file
47
v2/internal/ffenestri/windows_errorhandler_production.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// +build windows,!debug
|
||||||
|
|
||||||
|
package ffenestri
|
||||||
|
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func wall(err error, inputs ...interface{}) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pc, _, _, _ := runtime.Caller(1)
|
||||||
|
funcName := runtime.FuncForPC(pc).Name()
|
||||||
|
splitName := strings.Split(funcName, ".")
|
||||||
|
message := "[" + splitName[len(splitName)-1] + "]"
|
||||||
|
if len(inputs) > 0 {
|
||||||
|
params := []string{}
|
||||||
|
for _, param := range inputs {
|
||||||
|
params = append(params, fmt.Sprintf("%v", param))
|
||||||
|
}
|
||||||
|
message += "(" + strings.Join(params, " ") + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
title, err := syscall.UTF16PtrFromString("Fatal Error")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := syscall.UTF16PtrFromString("There has been a fatal error. Details:\n" + message)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var flags uint32 = windows.MB_ICONERROR | windows.MB_OK
|
||||||
|
|
||||||
|
_, err = windows.MessageBox(0, text, title, flags|windows.MB_SYSTEMMODAL)
|
||||||
|
os.Exit(1)
|
||||||
|
return err
|
||||||
|
}
|
||||||
191
v2/internal/ffenestri/windows_menu.go
Normal file
191
v2/internal/ffenestri/windows_menu.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
//+build windows
|
||||||
|
|
||||||
|
package ffenestri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||||
|
)
|
||||||
|
|
||||||
|
//-------------------- Types ------------------------
|
||||||
|
|
||||||
|
type win32MenuItemID uint32
|
||||||
|
type win32Menu uintptr
|
||||||
|
type win32Window uintptr
|
||||||
|
type wailsMenuItemID string // The internal menu ID
|
||||||
|
|
||||||
|
type Menu struct {
|
||||||
|
wailsMenu *menumanager.WailsMenu
|
||||||
|
menu win32Menu
|
||||||
|
menuType menuType
|
||||||
|
|
||||||
|
// A list of all checkbox and radio menuitems we
|
||||||
|
// create for this menu
|
||||||
|
checkboxes []win32MenuItemID
|
||||||
|
radioboxes []win32MenuItemID
|
||||||
|
initiallySelectedRadioItems []win32MenuItemID
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMenu(wailsMenu *menumanager.WailsMenu, menuType menuType) (*Menu, error) {
|
||||||
|
|
||||||
|
mainMenu, err := createWin32Menu()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &Menu{
|
||||||
|
wailsMenu: wailsMenu,
|
||||||
|
menu: mainMenu,
|
||||||
|
menuType: menuType,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process top level menus
|
||||||
|
for _, toplevelmenu := range applicationMenu.Menu.Items {
|
||||||
|
err := result.processMenuItem(result.menu, toplevelmenu)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = result.processRadioGroups()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Menu) processMenuItem(parent win32Menu, menuItem *menumanager.ProcessedMenuItem) error {
|
||||||
|
|
||||||
|
// Ignore hidden items
|
||||||
|
if menuItem.Hidden {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the flags for this menu item
|
||||||
|
flags := uintptr(calculateFlags(menuItem))
|
||||||
|
|
||||||
|
switch menuItem.Type {
|
||||||
|
case menu.SubmenuType:
|
||||||
|
submenu, err := createWin32PopupMenu()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, submenuItem := range menuItem.SubMenu.Items {
|
||||||
|
err = m.processMenuItem(submenu, submenuItem)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = appendWin32MenuItem(parent, flags, uintptr(submenu), menuItem.Label)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case menu.TextType, menu.CheckboxType, menu.RadioType:
|
||||||
|
win32ID := addMenuCacheEntry(parent, m.menuType, menuItem, m.wailsMenu.Menu)
|
||||||
|
//label := fmt.Sprintf("%s (%d)", menuItem.Label, win32ID)
|
||||||
|
label := menuItem.Label
|
||||||
|
err := appendWin32MenuItem(parent, flags, uintptr(win32ID), label)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if menuItem.Type == menu.CheckboxType {
|
||||||
|
// We need to maintain a list of this menu's checkboxes
|
||||||
|
m.checkboxes = append(m.checkboxes, win32ID)
|
||||||
|
globalCheckboxCache.addToCheckboxCache(m.wailsMenu.Menu, wailsMenuItemID(menuItem.ID), win32ID)
|
||||||
|
}
|
||||||
|
if menuItem.Type == menu.RadioType {
|
||||||
|
// We need to maintain a list of this menu's radioitems
|
||||||
|
m.radioboxes = append(m.radioboxes, win32ID)
|
||||||
|
globalRadioGroupMap.addRadioGroupMapping(m.wailsMenu.Menu, wailsMenuItemID(menuItem.ID), win32ID)
|
||||||
|
if menuItem.Checked {
|
||||||
|
m.initiallySelectedRadioItems = append(m.initiallySelectedRadioItems, win32ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case menu.SeparatorType:
|
||||||
|
err := appendWin32MenuItem(parent, flags, 0, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Menu) processRadioGroups() error {
|
||||||
|
|
||||||
|
for _, rg := range applicationMenu.RadioGroups {
|
||||||
|
startWailsMenuID := wailsMenuItemID(rg.Members[0])
|
||||||
|
endWailsMenuID := wailsMenuItemID(rg.Members[len(rg.Members)-1])
|
||||||
|
|
||||||
|
startIDs := globalRadioGroupMap.getRadioGroupMapping(startWailsMenuID)
|
||||||
|
endIDs := globalRadioGroupMap.getRadioGroupMapping(endWailsMenuID)
|
||||||
|
|
||||||
|
var radioGroupMaps = []*radioGroupStartEnd{}
|
||||||
|
for index := range startIDs {
|
||||||
|
startID := startIDs[index]
|
||||||
|
endID := endIDs[index]
|
||||||
|
thisRadioGroup := &radioGroupStartEnd{
|
||||||
|
startID: startID,
|
||||||
|
endID: endID,
|
||||||
|
}
|
||||||
|
radioGroupMaps = append(radioGroupMaps, thisRadioGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set this for each member
|
||||||
|
for _, member := range rg.Members {
|
||||||
|
id := wailsMenuItemID(member)
|
||||||
|
globalRadioGroupCache.addToRadioGroupCache(m.wailsMenu.Menu, id, radioGroupMaps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable all initially checked radio items
|
||||||
|
for _, win32MenuID := range m.initiallySelectedRadioItems {
|
||||||
|
menuItemDetails := getMenuCacheEntry(win32MenuID)
|
||||||
|
wailsMenuID := wailsMenuItemID(menuItemDetails.item.ID)
|
||||||
|
err := selectRadioItemFromWailsMenuID(wailsMenuID, win32MenuID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Menu) Destroy() error {
|
||||||
|
|
||||||
|
// Release the MenuIDs
|
||||||
|
releaseMenuIDsForProcessedMenu(m.wailsMenu.Menu)
|
||||||
|
|
||||||
|
// Unload this menu's checkboxes from the cache
|
||||||
|
globalCheckboxCache.removeMenuFromCheckboxCache(m.wailsMenu.Menu)
|
||||||
|
|
||||||
|
// Unload this menu's radio groups from the cache
|
||||||
|
globalRadioGroupCache.removeMenuFromRadioBoxCache(m.wailsMenu.Menu)
|
||||||
|
|
||||||
|
globalRadioGroupMap.removeMenuFromRadioGroupMapping(m.wailsMenu.Menu)
|
||||||
|
|
||||||
|
// Delete menu
|
||||||
|
return destroyWin32Menu(m.menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
var flagMap = map[menu.Type]uint32{
|
||||||
|
menu.TextType: MF_STRING,
|
||||||
|
menu.SeparatorType: MF_SEPARATOR,
|
||||||
|
menu.SubmenuType: MF_STRING | MF_POPUP,
|
||||||
|
menu.CheckboxType: MF_STRING,
|
||||||
|
menu.RadioType: MF_STRING,
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateFlags(menuItem *menumanager.ProcessedMenuItem) uint32 {
|
||||||
|
result := flagMap[menuItem.Type]
|
||||||
|
|
||||||
|
if menuItem.Disabled {
|
||||||
|
result |= MF_DISABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
if menuItem.Type == menu.CheckboxType && menuItem.Checked {
|
||||||
|
result |= MF_CHECKED
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
74
v2/internal/ffenestri/windows_menu_cache.go
Normal file
74
v2/internal/ffenestri/windows_menu_cache.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
//+build windows
|
||||||
|
|
||||||
|
package ffenestri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/leaanthony/idgen"
|
||||||
|
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
MenuCache
|
||||||
|
---------
|
||||||
|
When windows calls back to Go (when an item is clicked), we need to
|
||||||
|
be able to retrieve information about the menu item:
|
||||||
|
- The menu that the menuitem is part of (parent)
|
||||||
|
- The original processed menu item
|
||||||
|
- The type of the menu (application, context or tray)
|
||||||
|
|
||||||
|
This cache is built up when a menu is created.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Make this like the other caches
|
||||||
|
|
||||||
|
type menuCacheEntry struct {
|
||||||
|
parent win32Menu
|
||||||
|
menuType menuType
|
||||||
|
item *menumanager.ProcessedMenuItem
|
||||||
|
processedMenu *menumanager.ProcessedMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
var idGenerator = idgen.New()
|
||||||
|
|
||||||
|
var menuCache = map[win32MenuItemID]*menuCacheEntry{}
|
||||||
|
var menuCacheLock sync.RWMutex
|
||||||
|
var wailsMenuIDtoWin32IDMap = map[wailsMenuItemID]win32MenuItemID{}
|
||||||
|
|
||||||
|
// This releases the menuIDs back to the id generator
|
||||||
|
var winIDsOwnedByProcessedMenu = map[*menumanager.ProcessedMenu][]win32MenuItemID{}
|
||||||
|
|
||||||
|
func releaseMenuIDsForProcessedMenu(processedMenu *menumanager.ProcessedMenu) {
|
||||||
|
for _, menuID := range winIDsOwnedByProcessedMenu[processedMenu] {
|
||||||
|
idGenerator.ReleaseID(uint(menuID))
|
||||||
|
}
|
||||||
|
delete(winIDsOwnedByProcessedMenu, processedMenu)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addMenuCacheEntry(parent win32Menu, typ menuType, wailsMenuItem *menumanager.ProcessedMenuItem, processedMenu *menumanager.ProcessedMenu) win32MenuItemID {
|
||||||
|
menuCacheLock.Lock()
|
||||||
|
defer menuCacheLock.Unlock()
|
||||||
|
id, err := idGenerator.NewID()
|
||||||
|
checkFatal(err)
|
||||||
|
menuID := win32MenuItemID(id)
|
||||||
|
menuCache[menuID] = &menuCacheEntry{
|
||||||
|
parent: parent,
|
||||||
|
menuType: typ,
|
||||||
|
item: wailsMenuItem,
|
||||||
|
processedMenu: processedMenu,
|
||||||
|
}
|
||||||
|
// save the mapping
|
||||||
|
wailsMenuIDtoWin32IDMap[wailsMenuItemID(wailsMenuItem.ID)] = menuID
|
||||||
|
// keep track of menuids owned by this menu (so we can release the ids)
|
||||||
|
winIDsOwnedByProcessedMenu[processedMenu] = append(winIDsOwnedByProcessedMenu[processedMenu], menuID)
|
||||||
|
return menuID
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMenuCacheEntry(id win32MenuItemID) *menuCacheEntry {
|
||||||
|
menuCacheLock.Lock()
|
||||||
|
defer menuCacheLock.Unlock()
|
||||||
|
return menuCache[id]
|
||||||
|
}
|
||||||
194
v2/internal/ffenestri/windows_radiogroup.go
Normal file
194
v2/internal/ffenestri/windows_radiogroup.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
//+build windows
|
||||||
|
|
||||||
|
package ffenestri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/leaanthony/slicer"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Radio Groups
|
||||||
|
------------
|
||||||
|
Radio groups are stored by the ProcessedMenu as a list of menu ids.
|
||||||
|
Windows only cares about the start and end ids of the group so we
|
||||||
|
preprocess the radio groups and store this data in a radioGroupMap.
|
||||||
|
When a radio button is clicked, we use the menu id to read in the
|
||||||
|
radio group data and call CheckMenuRadioItem to update the group.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
type radioGroupStartEnd struct {
|
||||||
|
startID win32MenuItemID
|
||||||
|
endID win32MenuItemID
|
||||||
|
}
|
||||||
|
|
||||||
|
type RadioGroupCache struct {
|
||||||
|
cache map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]*radioGroupStartEnd
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRadioGroupCache() *RadioGroupCache {
|
||||||
|
return &RadioGroupCache{
|
||||||
|
cache: make(map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]*radioGroupStartEnd),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RadioGroupCache) Dump() {
|
||||||
|
// Start a new tabwriter
|
||||||
|
w := new(tabwriter.Writer)
|
||||||
|
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
|
||||||
|
|
||||||
|
println("---------------- RadioGroupCache", c, "Dump ----------------")
|
||||||
|
for menu, processedMenu := range c.cache {
|
||||||
|
println("Menu", menu)
|
||||||
|
_, _ = fmt.Fprintf(w, "Wails ID \tWindows ID Pairs\n")
|
||||||
|
for wailsMenuItemID, radioGroupStartEnd := range processedMenu {
|
||||||
|
menus := slicer.String()
|
||||||
|
for _, se := range radioGroupStartEnd {
|
||||||
|
menus.Add(fmt.Sprintf("[%d -> %d]", se.startID, se.endID))
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(w, "%s\t%s\n", wailsMenuItemID, menus.Join(", "))
|
||||||
|
_ = w.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RadioGroupCache) addToRadioGroupCache(menu *menumanager.ProcessedMenu, item wailsMenuItemID, radioGroupMaps []*radioGroupStartEnd) {
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
|
||||||
|
// Get map for menu
|
||||||
|
if c.cache[menu] == nil {
|
||||||
|
c.cache[menu] = make(map[wailsMenuItemID][]*radioGroupStartEnd)
|
||||||
|
}
|
||||||
|
menuMap := c.cache[menu]
|
||||||
|
|
||||||
|
// Ensure we have a slice
|
||||||
|
if menuMap[item] == nil {
|
||||||
|
menuMap[item] = []*radioGroupStartEnd{}
|
||||||
|
}
|
||||||
|
|
||||||
|
menuMap[item] = radioGroupMaps
|
||||||
|
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RadioGroupCache) removeMenuFromRadioBoxCache(menu *menumanager.ProcessedMenu) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
delete(c.cache, menu)
|
||||||
|
c.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RadioGroupCache) getRadioGroupMappings(wailsMenuID wailsMenuItemID) []*radioGroupStartEnd {
|
||||||
|
c.mutex.Lock()
|
||||||
|
result := []*radioGroupStartEnd{}
|
||||||
|
for _, menugroups := range c.cache {
|
||||||
|
groups := menugroups[wailsMenuID]
|
||||||
|
if groups != nil {
|
||||||
|
result = append(result, groups...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mutex.Unlock()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type RadioGroupMap struct {
|
||||||
|
cache map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRadioGroupMap() *RadioGroupMap {
|
||||||
|
return &RadioGroupMap{
|
||||||
|
cache: make(map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RadioGroupMap) Dump() {
|
||||||
|
// Start a new tabwriter
|
||||||
|
w := new(tabwriter.Writer)
|
||||||
|
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
|
||||||
|
|
||||||
|
println("---------------- RadioGroupMap", c, "Dump ----------------")
|
||||||
|
for _, processedMenu := range c.cache {
|
||||||
|
_, _ = fmt.Fprintf(w, "Menu\tWails ID \tWindows IDs\n")
|
||||||
|
for wailsMenuItemID, win32menus := range processedMenu {
|
||||||
|
menus := slicer.String()
|
||||||
|
for _, win32menu := range win32menus {
|
||||||
|
menus.Add(fmt.Sprintf("%v", win32menu))
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(w, "%p\t%s\t%s\n", processedMenu, wailsMenuItemID, menus.Join(", "))
|
||||||
|
_ = w.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RadioGroupMap) addRadioGroupMapping(menu *menumanager.ProcessedMenu, item wailsMenuItemID, win32ID win32MenuItemID) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
|
||||||
|
// Get map for menu
|
||||||
|
if m.cache[menu] == nil {
|
||||||
|
m.cache[menu] = make(map[wailsMenuItemID][]win32MenuItemID)
|
||||||
|
}
|
||||||
|
menuMap := m.cache[menu]
|
||||||
|
|
||||||
|
// Ensure we have a slice
|
||||||
|
if menuMap[item] == nil {
|
||||||
|
menuMap[item] = []win32MenuItemID{}
|
||||||
|
}
|
||||||
|
|
||||||
|
menuMap[item] = append(menuMap[item], win32ID)
|
||||||
|
|
||||||
|
m.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RadioGroupMap) removeMenuFromRadioGroupMapping(menu *menumanager.ProcessedMenu) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
delete(m.cache, menu)
|
||||||
|
m.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RadioGroupMap) getRadioGroupMapping(wailsMenuID wailsMenuItemID) []win32MenuItemID {
|
||||||
|
m.mutex.Lock()
|
||||||
|
result := []win32MenuItemID{}
|
||||||
|
for _, menuids := range m.cache {
|
||||||
|
ids := menuids[wailsMenuID]
|
||||||
|
if ids != nil {
|
||||||
|
result = append(result, ids...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mutex.Unlock()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectRadioItemFromWailsMenuID(wailsMenuID wailsMenuItemID, win32MenuID win32MenuItemID) error {
|
||||||
|
radioItemGroups := globalRadioGroupCache.getRadioGroupMappings(wailsMenuID)
|
||||||
|
// Figure out offset into group
|
||||||
|
var offset win32MenuItemID = 0
|
||||||
|
for _, radioItemGroup := range radioItemGroups {
|
||||||
|
if win32MenuID >= radioItemGroup.startID && win32MenuID <= radioItemGroup.endID {
|
||||||
|
offset = win32MenuID - radioItemGroup.startID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, radioItemGroup := range radioItemGroups {
|
||||||
|
selectedMenuID := radioItemGroup.startID + offset
|
||||||
|
menuItemDetails := getMenuCacheEntry(selectedMenuID)
|
||||||
|
if menuItemDetails != nil {
|
||||||
|
if menuItemDetails.parent != 0 {
|
||||||
|
err := selectRadioItem(selectedMenuID, radioItemGroup.startID, radioItemGroup.endID, menuItemDetails.parent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
129
v2/internal/ffenestri/windows_win32.go
Normal file
129
v2/internal/ffenestri/windows_win32.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
//+build windows
|
||||||
|
|
||||||
|
package ffenestri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DLL stuff
|
||||||
|
user32 = windows.NewLazySystemDLL("User32.dll")
|
||||||
|
win32CreateMenu = user32.NewProc("CreateMenu")
|
||||||
|
win32DestroyMenu = user32.NewProc("DestroyMenu")
|
||||||
|
win32CreatePopupMenu = user32.NewProc("CreatePopupMenu")
|
||||||
|
win32AppendMenuW = user32.NewProc("AppendMenuW")
|
||||||
|
win32SetMenu = user32.NewProc("SetMenu")
|
||||||
|
win32CheckMenuItem = user32.NewProc("CheckMenuItem")
|
||||||
|
win32GetMenuState = user32.NewProc("GetMenuState")
|
||||||
|
win32CheckMenuRadioItem = user32.NewProc("CheckMenuRadioItem")
|
||||||
|
applicationMenu *menumanager.WailsMenu
|
||||||
|
menuManager *menumanager.Manager
|
||||||
|
)
|
||||||
|
|
||||||
|
const MF_BITMAP uint32 = 0x00000004
|
||||||
|
const MF_CHECKED uint32 = 0x00000008
|
||||||
|
const MF_DISABLED uint32 = 0x00000002
|
||||||
|
const MF_ENABLED uint32 = 0x00000000
|
||||||
|
const MF_GRAYED uint32 = 0x00000001
|
||||||
|
const MF_MENUBARBREAK uint32 = 0x00000020
|
||||||
|
const MF_MENUBREAK uint32 = 0x00000040
|
||||||
|
const MF_OWNERDRAW uint32 = 0x00000100
|
||||||
|
const MF_POPUP uint32 = 0x00000010
|
||||||
|
const MF_SEPARATOR uint32 = 0x00000800
|
||||||
|
const MF_STRING uint32 = 0x00000000
|
||||||
|
const MF_UNCHECKED uint32 = 0x00000000
|
||||||
|
const MF_BYCOMMAND uint32 = 0x00000000
|
||||||
|
const MF_BYPOSITION uint32 = 0x00000400
|
||||||
|
|
||||||
|
const WM_SIZE = 5
|
||||||
|
const WM_GETMINMAXINFO = 36
|
||||||
|
|
||||||
|
type Win32Rect struct {
|
||||||
|
Left int32
|
||||||
|
Top int32
|
||||||
|
Right int32
|
||||||
|
Bottom int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------- win32 calls -----------------------
|
||||||
|
|
||||||
|
func createWin32Menu() (win32Menu, error) {
|
||||||
|
res, _, err := win32CreateMenu.Call()
|
||||||
|
if res == 0 {
|
||||||
|
return 0, wall(err)
|
||||||
|
}
|
||||||
|
return win32Menu(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func destroyWin32Menu(menu win32Menu) error {
|
||||||
|
res, _, err := win32DestroyMenu.Call(uintptr(menu))
|
||||||
|
if res == 0 {
|
||||||
|
return wall(err, "Menu:", menu)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createWin32PopupMenu() (win32Menu, error) {
|
||||||
|
res, _, err := win32CreatePopupMenu.Call()
|
||||||
|
if res == 0 {
|
||||||
|
return 0, wall(err)
|
||||||
|
}
|
||||||
|
return win32Menu(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendWin32MenuItem(menu win32Menu, flags uintptr, submenuOrID uintptr, label string) error {
|
||||||
|
menuText, err := windows.UTF16PtrFromString(label)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res, _, err := win32AppendMenuW.Call(
|
||||||
|
uintptr(menu),
|
||||||
|
flags,
|
||||||
|
submenuOrID,
|
||||||
|
uintptr(unsafe.Pointer(menuText)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return wall(err, "Menu", menu, "Flags", flags, "submenuOrID", submenuOrID, "label", label)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setWindowMenu(window win32Window, menu win32Menu) error {
|
||||||
|
res, _, err := win32SetMenu.Call(uintptr(window), uintptr(menu))
|
||||||
|
if res == 0 {
|
||||||
|
return wall(err, "window", window, "menu", menu)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectRadioItem(selectedMenuID, startMenuItemID, endMenuItemID win32MenuItemID, parent win32Menu) error {
|
||||||
|
res, _, err := win32CheckMenuRadioItem.Call(uintptr(parent), uintptr(startMenuItemID), uintptr(endMenuItemID), uintptr(selectedMenuID), uintptr(MF_BYCOMMAND))
|
||||||
|
if int(res) == 0 {
|
||||||
|
return wall(err, selectedMenuID, startMenuItemID, endMenuItemID, parent)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//func getWindowRect(window win32Window) (*Win32Rect, error) {
|
||||||
|
// var windowRect Win32Rect
|
||||||
|
// res, _, err := win32GetWindowRect.Call(uintptr(window), uintptr(unsafe.Pointer(&windowRect)))
|
||||||
|
// if res == 0 {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// return &windowRect, nil
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func getClientRect(window win32Window) (*Win32Rect, error) {
|
||||||
|
// var clientRect Win32Rect
|
||||||
|
// res, _, err := win32GetClientRect.Call(uintptr(window), uintptr(unsafe.Pointer(&clientRect)))
|
||||||
|
// if res == 0 {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// return &clientRect, nil
|
||||||
|
//}
|
||||||
|
//
|
||||||
@@ -12,7 +12,8 @@ class wv2ComHandler
|
|||||||
: public ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler,
|
: public ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler,
|
||||||
public ICoreWebView2CreateCoreWebView2ControllerCompletedHandler,
|
public ICoreWebView2CreateCoreWebView2ControllerCompletedHandler,
|
||||||
public ICoreWebView2WebMessageReceivedEventHandler,
|
public ICoreWebView2WebMessageReceivedEventHandler,
|
||||||
public ICoreWebView2PermissionRequestedEventHandler
|
public ICoreWebView2PermissionRequestedEventHandler,
|
||||||
|
public ICoreWebView2AcceleratorKeyPressedEventHandler
|
||||||
{
|
{
|
||||||
|
|
||||||
struct Application *app;
|
struct Application *app;
|
||||||
@@ -44,6 +45,7 @@ class wv2ComHandler
|
|||||||
ICoreWebView2 *webview;
|
ICoreWebView2 *webview;
|
||||||
::EventRegistrationToken token;
|
::EventRegistrationToken token;
|
||||||
controller->get_CoreWebView2(&webview);
|
controller->get_CoreWebView2(&webview);
|
||||||
|
controller->add_AcceleratorKeyPressed(this, &token);
|
||||||
webview->add_WebMessageReceived(this, &token);
|
webview->add_WebMessageReceived(this, &token);
|
||||||
webview->add_PermissionRequested(this, &token);
|
webview->add_PermissionRequested(this, &token);
|
||||||
|
|
||||||
@@ -51,6 +53,39 @@ class wv2ComHandler
|
|||||||
return S_OK;
|
return S_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is our keyboard callback method
|
||||||
|
HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2Controller *controller, ICoreWebView2AcceleratorKeyPressedEventArgs * args) {
|
||||||
|
COREWEBVIEW2_KEY_EVENT_KIND kind;
|
||||||
|
args->get_KeyEventKind(&kind);
|
||||||
|
if (kind == COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN ||
|
||||||
|
kind == COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN)
|
||||||
|
{
|
||||||
|
// UINT key;
|
||||||
|
// args->get_VirtualKey(&key);
|
||||||
|
// printf("Got key: %d\n", key);
|
||||||
|
args->put_Handled(TRUE);
|
||||||
|
// Check if the key is one we want to handle.
|
||||||
|
// if (std::function<void()> action =
|
||||||
|
// m_appWindow->GetAcceleratorKeyFunction(key))
|
||||||
|
// {
|
||||||
|
// // Keep the browser from handling this key, whether it's autorepeated or
|
||||||
|
// // not.
|
||||||
|
// CHECK_FAILURE(args->put_Handled(TRUE));
|
||||||
|
//
|
||||||
|
// // Filter out autorepeated keys.
|
||||||
|
// COREWEBVIEW2_PHYSICAL_KEY_STATUS status;
|
||||||
|
// CHECK_FAILURE(args->get_PhysicalKeyStatus(&status));
|
||||||
|
// if (!status.WasKeyDown)
|
||||||
|
// {
|
||||||
|
// // Perform the action asynchronously to avoid blocking the
|
||||||
|
// // browser process's event queue.
|
||||||
|
// m_appWindow->RunAsync(action);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
// This is called when JS posts a message back to webkit
|
// This is called when JS posts a message back to webkit
|
||||||
HRESULT STDMETHODCALLTYPE Invoke(
|
HRESULT STDMETHODCALLTYPE Invoke(
|
||||||
ICoreWebView2 *sender, ICoreWebView2WebMessageReceivedEventArgs *args) {
|
ICoreWebView2 *sender, ICoreWebView2WebMessageReceivedEventArgs *args) {
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ func (m *Manager) GetApplicationMenuJSON() string {
|
|||||||
return m.applicationMenuJSON
|
return m.applicationMenuJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetProcessedApplicationMenu() *WailsMenu {
|
||||||
|
return m.processedApplicationMenu
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateApplicationMenu reprocesses the application menu to pick up structure
|
// UpdateApplicationMenu reprocesses the application menu to pick up structure
|
||||||
// changes etc
|
// changes etc
|
||||||
// Returns the JSON representation of the updated menu
|
// Returns the JSON representation of the updated menu
|
||||||
@@ -36,8 +40,9 @@ func (m *Manager) UpdateApplicationMenu() (string, error) {
|
|||||||
func (m *Manager) processApplicationMenu() error {
|
func (m *Manager) processApplicationMenu() error {
|
||||||
|
|
||||||
// Process the menu
|
// Process the menu
|
||||||
processedApplicationMenu := NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu)
|
m.processedApplicationMenu = NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu)
|
||||||
applicationMenuJSON, err := processedApplicationMenu.AsJSON()
|
m.processRadioGroups(m.processedApplicationMenu, m.applicationMenuItemMap)
|
||||||
|
applicationMenuJSON, err := m.processedApplicationMenu.AsJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import (
|
|||||||
type Manager struct {
|
type Manager struct {
|
||||||
|
|
||||||
// The application menu.
|
// The application menu.
|
||||||
applicationMenu *menu.Menu
|
applicationMenu *menu.Menu
|
||||||
applicationMenuJSON string
|
applicationMenuJSON string
|
||||||
|
processedApplicationMenu *WailsMenu
|
||||||
|
|
||||||
// Our application menu mappings
|
// Our application menu mappings
|
||||||
applicationMenuItemMap *MenuItemMap
|
applicationMenuItemMap *MenuItemMap
|
||||||
@@ -21,6 +22,9 @@ type Manager struct {
|
|||||||
// Tray menu stores
|
// Tray menu stores
|
||||||
trayMenus map[string]*TrayMenu
|
trayMenus map[string]*TrayMenu
|
||||||
trayMenuPointers map[*menu.TrayMenu]string
|
trayMenuPointers map[*menu.TrayMenu]string
|
||||||
|
|
||||||
|
// Radio groups
|
||||||
|
radioGroups map[*menu.MenuItem][]*menu.MenuItem
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager() *Manager {
|
func NewManager() *Manager {
|
||||||
@@ -30,6 +34,7 @@ func NewManager() *Manager {
|
|||||||
contextMenuPointers: make(map[*menu.ContextMenu]string),
|
contextMenuPointers: make(map[*menu.ContextMenu]string),
|
||||||
trayMenus: make(map[string]*TrayMenu),
|
trayMenus: make(map[string]*TrayMenu),
|
||||||
trayMenuPointers: make(map[*menu.TrayMenu]string),
|
trayMenuPointers: make(map[*menu.TrayMenu]string),
|
||||||
|
radioGroups: make(map[*menu.MenuItem][]*menu.MenuItem),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +77,14 @@ func (m *Manager) ProcessClick(menuID string, data string, menuType string, pare
|
|||||||
menuItem.Checked = !menuItem.Checked
|
menuItem.Checked = !menuItem.Checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if menuItem.Type == menu.RadioType {
|
||||||
|
println("Toggle radio")
|
||||||
|
// Get my radio group
|
||||||
|
for _, radioMenuItem := range m.radioGroups[menuItem] {
|
||||||
|
radioMenuItem.Checked = (radioMenuItem == menuItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if menuItem.Click == nil {
|
if menuItem.Click == nil {
|
||||||
// No callback
|
// No callback
|
||||||
return fmt.Errorf("No callback for menu '%s'", menuItem.Label)
|
return fmt.Errorf("No callback for menu '%s'", menuItem.Label)
|
||||||
@@ -88,3 +101,16 @@ func (m *Manager) ProcessClick(menuID string, data string, menuType string, pare
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) processRadioGroups(processedMenu *WailsMenu, itemMap *MenuItemMap) {
|
||||||
|
for _, group := range processedMenu.RadioGroups {
|
||||||
|
radioGroupMenuItems := []*menu.MenuItem{}
|
||||||
|
for _, member := range group.Members {
|
||||||
|
item := m.getMenuItemByID(itemMap, member)
|
||||||
|
radioGroupMenuItems = append(radioGroupMenuItems, item)
|
||||||
|
}
|
||||||
|
for _, radioGroupMenuItem := range radioGroupMenuItems {
|
||||||
|
m.radioGroups[radioGroupMenuItem] = radioGroupMenuItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/internal/system/operatingsystem"
|
"github.com/wailsapp/wails/v2/internal/system/operatingsystem"
|
||||||
"github.com/wailsapp/wails/v2/internal/system/packagemanager"
|
"github.com/wailsapp/wails/v2/internal/system/packagemanager"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Info holds information about the current operating system,
|
// Info holds information about the current operating system,
|
||||||
@@ -107,14 +105,3 @@ func checkDocker() *packagemanager.Dependancy {
|
|||||||
External: false,
|
External: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAppleSilicon returns true if the app is running on Apple Silicon
|
|
||||||
// Credit: https://www.yellowduck.be/posts/detecting-apple-silicon-via-go/
|
|
||||||
func IsAppleSilicon() bool {
|
|
||||||
r, err := syscall.Sysctl("sysctl.proc_translated")
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return r == "\x00\x00\x00" || r == "\x01\x00\x00"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package system
|
|||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/internal/system/packagemanager"
|
"github.com/wailsapp/wails/v2/internal/system/packagemanager"
|
||||||
|
|
||||||
@@ -44,3 +45,14 @@ func (i *Info) discover() error {
|
|||||||
i.Dependencies = append(i.Dependencies, checkUPX())
|
i.Dependencies = append(i.Dependencies, checkUPX())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsAppleSilicon returns true if the app is running on Apple Silicon
|
||||||
|
// Credit: https://www.yellowduck.be/posts/detecting-apple-silicon-via-go/
|
||||||
|
func IsAppleSilicon() bool {
|
||||||
|
r, err := syscall.Sysctl("sysctl.proc_translated")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return r == "\x00\x00\x00" || r == "\x01\x00\x00"
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,3 +27,10 @@ func (i *Info) discover() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsAppleSilicon returns true if the app is running on Apple Silicon
|
||||||
|
// Credit: https://www.yellowduck.be/posts/detecting-apple-silicon-via-go/
|
||||||
|
// NOTE: Not applicable to linux
|
||||||
|
func IsAppleSilicon() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,3 +43,10 @@ func (i *Info) discover() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsAppleSilicon returns true if the app is running on Apple Silicon
|
||||||
|
// Credit: https://www.yellowduck.be/posts/detecting-apple-silicon-via-go/
|
||||||
|
// NOTE: Not applicable to windows
|
||||||
|
func IsAppleSilicon() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
<assemblyIdentity type="win32" name="MyApplication" version="1.0.0.0" processorArchitecture="amd64"/>
|
<assemblyIdentity type="win32" name="MyApplication" version="1.0.0.0" processorArchitecture="amd64"/>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
<asmv3:application>
|
<asmv3:application>
|
||||||
<asmv3:windowsSettings>
|
<asmv3:windowsSettings>
|
||||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||||
|
|||||||
11
v2/pkg/menu/windows.go
Normal file
11
v2/pkg/menu/windows.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package menu
|
||||||
|
|
||||||
|
// DefaultWindowsMenu returns a default menu including the default
|
||||||
|
// Application and Edit menus. Use `.Append()` to add to it.
|
||||||
|
func DefaultWindowsMenu() *Menu {
|
||||||
|
return NewMenuFromItems(
|
||||||
|
FileMenu(),
|
||||||
|
EditMenu(),
|
||||||
|
WindowMenu(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -107,14 +107,14 @@ func GetApplicationMenu(appoptions *App) *menu.Menu {
|
|||||||
if appoptions.Mac != nil {
|
if appoptions.Mac != nil {
|
||||||
result = appoptions.Mac.Menu
|
result = appoptions.Mac.Menu
|
||||||
}
|
}
|
||||||
//case "linux":
|
//case "linux":
|
||||||
// if appoptions.Linux != nil {
|
// if appoptions.Linux != nil {
|
||||||
// result = appoptions.Linux.TrayMenu
|
// result = appoptions.Linux.TrayMenu
|
||||||
// }
|
// }
|
||||||
//case "windows":
|
case "windows":
|
||||||
// if appoptions.Windows != nil {
|
if appoptions.Windows != nil {
|
||||||
// result = appoptions.Windows.TrayMenu
|
result = appoptions.Windows.Menu
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if result == nil {
|
if result == nil {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package windows
|
package windows
|
||||||
|
|
||||||
// Options are options specific to Mac
|
import "github.com/wailsapp/wails/v2/pkg/menu"
|
||||||
|
|
||||||
|
// Options are options specific to Windows
|
||||||
type Options struct {
|
type Options struct {
|
||||||
WebviewIsTransparent bool
|
WebviewIsTransparent bool
|
||||||
WindowBackgroundIsTranslucent bool
|
WindowBackgroundIsTranslucent bool
|
||||||
DisableWindowIcon bool
|
DisableWindowIcon bool
|
||||||
|
Menu *menu.Menu
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user