mirror of
https://github.com/taigrr/wails.git
synced 2026-04-07 23:51:34 -07:00
Compare commits
11 Commits
v2-alpha-x
...
v2.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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) {
|
||||
// Perform your setup here
|
||||
b.runtime = runtime
|
||||
runtime.Window.SetTitle("{{.ProjectName}}")
|
||||
}
|
||||
|
||||
// shutdown is called at application termination
|
||||
|
||||
@@ -20,7 +20,6 @@ func NewApp() *App {
|
||||
func (b *App) startup(runtime *wails.Runtime) {
|
||||
// Perform your setup here
|
||||
b.runtime = runtime
|
||||
runtime.Window.SetTitle("{{.ProjectName}}")
|
||||
}
|
||||
|
||||
// shutdown is called at application termination
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package main
|
||||
|
||||
var version = "v2.0.0-alpha.71"
|
||||
var version = "v2.0.0-alpha.72"
|
||||
|
||||
@@ -33,6 +33,7 @@ require (
|
||||
github.com/tidwall/sjson v1.1.7
|
||||
github.com/wzshiming/ctc v1.2.3
|
||||
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/mod v0.4.1 // indirect
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897
|
||||
|
||||
@@ -107,6 +107,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/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/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/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
@@ -166,6 +168,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/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY=
|
||||
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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()
|
||||
|
||||
// Process the application menu
|
||||
menuManager.SetApplicationMenu(options.GetApplicationMenu(appoptions))
|
||||
appMenu := options.GetApplicationMenu(appoptions)
|
||||
menuManager.SetApplicationMenu(appMenu)
|
||||
|
||||
// Process context menus
|
||||
contextMenus := options.GetContextMenus(appoptions)
|
||||
|
||||
@@ -284,8 +284,8 @@ func (c *Client) DarkModeEnabled(callbackID string) {
|
||||
}
|
||||
|
||||
// SetApplicationMenu sets the application menu
|
||||
func (c *Client) SetApplicationMenu(applicationMenuJSON string) {
|
||||
C.SetApplicationMenu(c.app.app, c.app.string2CString(applicationMenuJSON))
|
||||
func (c *Client) SetApplicationMenu(_ string) {
|
||||
c.updateApplicationMenu()
|
||||
}
|
||||
|
||||
// SetTrayMenu sets the tray menu
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// License included in README.md
|
||||
|
||||
#include "ffenestri_windows.h"
|
||||
#include "shellscalingapi.h"
|
||||
#include "wv2ComHandler_windows.h"
|
||||
#include <functional>
|
||||
#include <atomic>
|
||||
@@ -42,6 +43,30 @@ char* LPWSTRToCstr(LPWSTR input) {
|
||||
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) {
|
||||
|
||||
// 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
|
||||
result->previousPlacement = { sizeof(result->previousPlacement) };
|
||||
|
||||
// DPI
|
||||
result->dpix = result->dpiy = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -142,6 +170,14 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
|
||||
switch(msg) {
|
||||
|
||||
case WM_CREATE: {
|
||||
createApplicationMenu(hwnd);
|
||||
break;
|
||||
}
|
||||
case WM_COMMAND:
|
||||
menuClicked(LOWORD(wParam));
|
||||
break;
|
||||
|
||||
case WM_CLOSE: {
|
||||
DestroyWindow( app->window );
|
||||
break;
|
||||
@@ -171,30 +207,28 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// get pixel density
|
||||
HDC hDC = GetDC(NULL);
|
||||
double DPIScaleX = GetDeviceCaps(hDC, 88)/96.0;
|
||||
double DPIScaleY = GetDeviceCaps(hDC, 90)/96.0;
|
||||
ReleaseDC(NULL, hDC);
|
||||
// update DPI
|
||||
getDPIForWindow(app);
|
||||
double DPIScaleX = app->dpix/96.0;
|
||||
double DPIScaleY = app->dpiy/96.0;
|
||||
|
||||
RECT rcClient, rcWind;
|
||||
RECT rcWind;
|
||||
POINT ptDiff;
|
||||
GetClientRect(hwnd, &rcClient);
|
||||
GetWindowRect(hwnd, &rcWind);
|
||||
|
||||
int widthExtra = (rcWind.right - rcWind.left) - rcClient.right;
|
||||
int heightExtra = (rcWind.bottom - rcWind.top) - rcClient.bottom;
|
||||
int widthExtra = (rcWind.right - rcWind.left);
|
||||
int heightExtra = (rcWind.bottom - rcWind.top);
|
||||
|
||||
LPMINMAXINFO mmi = (LPMINMAXINFO) lParam;
|
||||
if (app->minWidth > 0 && app->minHeight > 0) {
|
||||
mmi->ptMinTrackSize.x = app->minWidth * DPIScaleX + widthExtra;
|
||||
mmi->ptMinTrackSize.y = app->minHeight * DPIScaleY + heightExtra;
|
||||
mmi->ptMinTrackSize.x = app->minWidth * DPIScaleX;
|
||||
mmi->ptMinTrackSize.y = app->minHeight * DPIScaleY;
|
||||
}
|
||||
if (app->maxWidth > 0 && app->maxHeight > 0) {
|
||||
mmi->ptMaxSize.x = app->maxWidth * DPIScaleX + widthExtra;
|
||||
mmi->ptMaxSize.y = app->maxHeight * DPIScaleY + heightExtra;
|
||||
mmi->ptMaxTrackSize.x = app->maxWidth * DPIScaleX + widthExtra;
|
||||
mmi->ptMaxTrackSize.y = app->maxHeight * DPIScaleY + heightExtra;
|
||||
mmi->ptMaxSize.x = app->maxWidth * DPIScaleX;
|
||||
mmi->ptMaxSize.y = app->maxHeight * DPIScaleY;
|
||||
mmi->ptMaxTrackSize.x = app->maxWidth * DPIScaleX;
|
||||
mmi->ptMaxTrackSize.y = app->maxHeight * DPIScaleY;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -384,6 +418,7 @@ bool initWebView2(struct Application *app, int debugEnabled, messageCallback cb)
|
||||
}
|
||||
app->webviewController = controller;
|
||||
app->webview = webview;
|
||||
|
||||
// Resize WebView to fit the bounds of the parent window
|
||||
RECT bounds;
|
||||
GetClientRect(app->window, &bounds);
|
||||
@@ -395,7 +430,64 @@ bool initWebView2(struct Application *app, int debugEnabled, messageCallback cb)
|
||||
LPCWSTR html = (LPCWSTR) cstrToLPWSTR((char*)assets[0]);
|
||||
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\":[]}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -492,61 +584,6 @@ void Run(struct Application* app, int argc, char **argv) {
|
||||
// Add webview2
|
||||
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
|
||||
MSG msg;
|
||||
|
||||
@@ -13,9 +13,30 @@ extern void DisableWindowIcon(struct Application* app);
|
||||
|
||||
*/
|
||||
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 {
|
||||
|
||||
menuManager = a.menuManager
|
||||
config := a.config.Windows
|
||||
if config == nil {
|
||||
return nil
|
||||
@@ -34,12 +55,9 @@ func (a *Application) processPlatformSettings() error {
|
||||
C.DisableWindowIcon(a.app)
|
||||
}
|
||||
|
||||
//// Process menu
|
||||
////applicationMenu := options.GetApplicationMenu(a.config)
|
||||
//applicationMenu := a.menuManager.GetApplicationMenuJSON()
|
||||
//if applicationMenu != "" {
|
||||
// C.SetApplicationMenu(a.app, a.string2CString(applicationMenu))
|
||||
//}
|
||||
// Unfortunately, we need to store this in the package variable so the C callback can see it
|
||||
applicationMenu = a.menuManager.GetProcessedApplicationMenu()
|
||||
|
||||
//
|
||||
//// Process tray
|
||||
//trays, err := a.menuManager.GetTrayMenus()
|
||||
@@ -70,3 +88,98 @@ func (a *Application) processPlatformSettings() error {
|
||||
|
||||
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
|
||||
char* bindings;
|
||||
char* initialCode;
|
||||
|
||||
// DPI
|
||||
UINT dpix;
|
||||
UINT dpiy;
|
||||
};
|
||||
|
||||
#define ON_MAIN_THREAD(code) dispatch( [=]{ code; } )
|
||||
@@ -80,6 +84,8 @@ extern "C" {
|
||||
void DisableWindowIcon(struct Application* app);
|
||||
void messageFromWindowCallback(const char *);
|
||||
void* GetWindowHandle(struct Application*);
|
||||
void createApplicationMenu(HWND hwnd);
|
||||
void menuClicked(UINT id);
|
||||
}
|
||||
|
||||
#endif
|
||||
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) removeMenuFromChechboxCache(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
|
||||
}
|
||||
188
v2/internal/ffenestri/windows_menu.go
Normal file
188
v2/internal/ffenestri/windows_menu.go
Normal file
@@ -0,0 +1,188 @@
|
||||
//+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 {
|
||||
|
||||
// Unload this menu's checkboxes from the cache
|
||||
globalCheckboxCache.removeMenuFromChechboxCache(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
|
||||
}
|
||||
61
v2/internal/ffenestri/windows_menu_cache.go
Normal file
61
v2/internal/ffenestri/windows_menu_cache.go
Normal file
@@ -0,0 +1,61 @@
|
||||
//+build windows
|
||||
|
||||
package ffenestri
|
||||
|
||||
import (
|
||||
"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
|
||||
}
|
||||
|
||||
// windowsMenuIDCounter keeps track of the unique windows menu IDs
|
||||
var windowsMenuIDCounter uint32
|
||||
|
||||
var menuCache = map[win32MenuItemID]*menuCacheEntry{}
|
||||
var menuCacheLock sync.RWMutex
|
||||
var wailsMenuIDtoWin32IDMap = map[wailsMenuItemID]win32MenuItemID{}
|
||||
|
||||
func addMenuCacheEntry(parent win32Menu, typ menuType, wailsMenuItem *menumanager.ProcessedMenuItem, processedMenu *menumanager.ProcessedMenu) win32MenuItemID {
|
||||
menuCacheLock.Lock()
|
||||
defer menuCacheLock.Unlock()
|
||||
menuID := win32MenuItemID(windowsMenuIDCounter)
|
||||
windowsMenuIDCounter++
|
||||
menuCache[menuID] = &menuCacheEntry{
|
||||
parent: parent,
|
||||
menuType: typ,
|
||||
item: wailsMenuItem,
|
||||
processedMenu: processedMenu,
|
||||
}
|
||||
// save the mapping
|
||||
wailsMenuIDtoWin32IDMap[wailsMenuItemID(wailsMenuItem.ID)] = 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
|
||||
//}
|
||||
//
|
||||
@@ -23,6 +23,10 @@ func (m *Manager) GetApplicationMenuJSON() string {
|
||||
return m.applicationMenuJSON
|
||||
}
|
||||
|
||||
func (m *Manager) GetProcessedApplicationMenu() *WailsMenu {
|
||||
return m.processedApplicationMenu
|
||||
}
|
||||
|
||||
// UpdateApplicationMenu reprocesses the application menu to pick up structure
|
||||
// changes etc
|
||||
// Returns the JSON representation of the updated menu
|
||||
@@ -36,8 +40,9 @@ func (m *Manager) UpdateApplicationMenu() (string, error) {
|
||||
func (m *Manager) processApplicationMenu() error {
|
||||
|
||||
// Process the menu
|
||||
processedApplicationMenu := NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu)
|
||||
applicationMenuJSON, err := processedApplicationMenu.AsJSON()
|
||||
m.processedApplicationMenu = NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu)
|
||||
m.processRadioGroups(m.processedApplicationMenu, m.applicationMenuItemMap)
|
||||
applicationMenuJSON, err := m.processedApplicationMenu.AsJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ import (
|
||||
type Manager struct {
|
||||
|
||||
// The application menu.
|
||||
applicationMenu *menu.Menu
|
||||
applicationMenuJSON string
|
||||
applicationMenu *menu.Menu
|
||||
applicationMenuJSON string
|
||||
processedApplicationMenu *WailsMenu
|
||||
|
||||
// Our application menu mappings
|
||||
applicationMenuItemMap *MenuItemMap
|
||||
@@ -21,6 +22,9 @@ type Manager struct {
|
||||
// Tray menu stores
|
||||
trayMenus map[string]*TrayMenu
|
||||
trayMenuPointers map[*menu.TrayMenu]string
|
||||
|
||||
// Radio groups
|
||||
radioGroups map[*menu.MenuItem][]*menu.MenuItem
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
@@ -30,6 +34,7 @@ func NewManager() *Manager {
|
||||
contextMenuPointers: make(map[*menu.ContextMenu]string),
|
||||
trayMenus: make(map[string]*TrayMenu),
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
// No callback
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/system/operatingsystem"
|
||||
"github.com/wailsapp/wails/v2/internal/system/packagemanager"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Info holds information about the current operating system,
|
||||
@@ -107,14 +105,3 @@ func checkDocker() *packagemanager.Dependancy {
|
||||
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 (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/system/packagemanager"
|
||||
|
||||
@@ -44,3 +45,14 @@ func (i *Info) discover() error {
|
||||
i.Dependencies = append(i.Dependencies, checkUPX())
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 {
|
||||
result = appoptions.Mac.Menu
|
||||
}
|
||||
//case "linux":
|
||||
// if appoptions.Linux != nil {
|
||||
// result = appoptions.Linux.TrayMenu
|
||||
// }
|
||||
//case "windows":
|
||||
// if appoptions.Windows != nil {
|
||||
// result = appoptions.Windows.TrayMenu
|
||||
// }
|
||||
//case "linux":
|
||||
// if appoptions.Linux != nil {
|
||||
// result = appoptions.Linux.TrayMenu
|
||||
// }
|
||||
case "windows":
|
||||
if appoptions.Windows != nil {
|
||||
result = appoptions.Windows.Menu
|
||||
}
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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 {
|
||||
WebviewIsTransparent bool
|
||||
WindowBackgroundIsTranslucent bool
|
||||
DisableWindowIcon bool
|
||||
Menu *menu.Menu
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user