diff --git a/.gitignore b/.gitignore index 7da685641..bc604b5dd 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ v2/test/kitchensink/frontend/package.json.md5 v2/cmd/wails/internal/commands/initialise/templates/testtemplates/ .env /website/static/img/.cache.json + +v2/internal/frontend/desktop/darwin/test.xcodeproj +TODO.md \ No newline at end of file diff --git a/v2/examples/README.md b/v2/examples/README.md new file mode 100644 index 000000000..ec66688b8 --- /dev/null +++ b/v2/examples/README.md @@ -0,0 +1,15 @@ +

+ Wails logo
+

+ +

+Various example applications for Wails. +

+ +## Table of Contents + +- [Systray](./systray/README.md) + +## Contributing + +Please create a directory with your example, add a link to this README and raise a PR. diff --git a/v2/examples/common/fonts/OFL.txt b/v2/examples/common/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/examples/common/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/examples/common/fonts/nunito-v16-latin-regular.woff2 b/v2/examples/common/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/examples/common/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/examples/common/images/logo-universal.png b/v2/examples/common/images/logo-universal.png new file mode 100644 index 000000000..99ac71f5a Binary files /dev/null and b/v2/examples/common/images/logo-universal.png differ diff --git a/v2/examples/systray/.gitignore b/v2/examples/systray/.gitignore new file mode 100644 index 000000000..9fc08b5e3 --- /dev/null +++ b/v2/examples/systray/.gitignore @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/wailsjs diff --git a/v2/examples/systray/README.md b/v2/examples/systray/README.md new file mode 100644 index 000000000..c99dfcb95 --- /dev/null +++ b/v2/examples/systray/README.md @@ -0,0 +1,17 @@ +# System Tray + +This example shows how to create a system tray using an experimental programmatic API. + +## Running + +As this example outputs text to the console, it is recommended to build using `wails build -debug`. + +## Supported Platforms + +- [x] Windows +- [ ] macOS +- [ ] Linux + + + + diff --git a/v2/examples/systray/build/appicon.png b/v2/examples/systray/build/appicon.png new file mode 100644 index 000000000..63617fe4f Binary files /dev/null and b/v2/examples/systray/build/appicon.png differ diff --git a/v2/examples/systray/build/darwin/Info.plist b/v2/examples/systray/build/darwin/Info.plist new file mode 100644 index 000000000..303145e39 --- /dev/null +++ b/v2/examples/systray/build/darwin/Info.plist @@ -0,0 +1,14 @@ + + + CFBundlePackageTypeAPPL + CFBundleName{{.Info.ProductName}} + CFBundleExecutable{{.Name}} + CFBundleIdentifiercom.wails.{{.Name}} + CFBundleVersion{{.Info.ProductVersion}} + CFBundleGetInfoString{{.Info.Comments}} + CFBundleShortVersionString{{.Info.ProductVersion}} + CFBundleIconFileiconfile + LSMinimumSystemVersion10.13.0 + NSHighResolutionCapabletrue + NSHumanReadableCopyright{{.Info.Copyright}} + \ No newline at end of file diff --git a/v2/examples/systray/build/windows/icon.ico b/v2/examples/systray/build/windows/icon.ico new file mode 100644 index 000000000..f33479841 Binary files /dev/null and b/v2/examples/systray/build/windows/icon.ico differ diff --git a/v2/examples/systray/build/windows/info.json b/v2/examples/systray/build/windows/info.json new file mode 100644 index 000000000..c23c173c9 --- /dev/null +++ b/v2/examples/systray/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/v2/examples/systray/build/windows/installer/project.nsi b/v2/examples/systray/build/windows/installer/project.nsi new file mode 100644 index 000000000..3b1588e0c --- /dev/null +++ b/v2/examples/systray/build/windows/installer/project.nsi @@ -0,0 +1,101 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the ProjectInfo file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" +## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" +## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" +## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/v2/examples/systray/build/windows/installer/wails_tools.nsh b/v2/examples/systray/build/windows/installer/wails_tools.nsh new file mode 100644 index 000000000..66dc209a3 --- /dev/null +++ b/v2/examples/systray/build/windows/installer/wails_tools.nsh @@ -0,0 +1,171 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "{{.Name}}" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "{{.Info.CompanyName}}" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "{{.Info.ProductName}}" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "{{.Info.Copyright}}" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "tmp\MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend \ No newline at end of file diff --git a/v2/examples/systray/build/windows/wails.exe.manifest b/v2/examples/systray/build/windows/wails.exe.manifest new file mode 100644 index 000000000..17e1a2387 --- /dev/null +++ b/v2/examples/systray/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/v2/examples/systray/frontend/index.html b/v2/examples/systray/frontend/index.html new file mode 100644 index 000000000..5451f1e30 --- /dev/null +++ b/v2/examples/systray/frontend/index.html @@ -0,0 +1,59 @@ + + + + + + Systray Example + + + +
+ +

Systray Example

+
+ + diff --git a/v2/examples/systray/frontend/package-lock.json b/v2/examples/systray/frontend/package-lock.json new file mode 100644 index 000000000..1f1e962f0 --- /dev/null +++ b/v2/examples/systray/frontend/package-lock.json @@ -0,0 +1,852 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "devDependencies": { + "vite": "^2.9.9" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.77.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz", + "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/vite": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz", + "integrity": "sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": ">=2.59.0 <2.78.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } + } + } + }, + "dependencies": { + "@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "dev": true, + "optional": true + }, + "esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "requires": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "dev": true, + "optional": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "postcss": { + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "rollup": { + "version": "2.77.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz", + "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "vite": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz", + "integrity": "sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==", + "dev": true, + "requires": { + "esbuild": "^0.14.27", + "fsevents": "~2.3.2", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": ">=2.59.0 <2.78.0" + } + } + } +} diff --git a/v2/examples/systray/frontend/package.json b/v2/examples/systray/frontend/package.json new file mode 100644 index 000000000..4ac881798 --- /dev/null +++ b/v2/examples/systray/frontend/package.json @@ -0,0 +1,13 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^2.9.9" + } +} \ No newline at end of file diff --git a/v2/examples/systray/go.mod b/v2/examples/systray/go.mod new file mode 100644 index 000000000..0ba256649 --- /dev/null +++ b/v2/examples/systray/go.mod @@ -0,0 +1,35 @@ +module github.com/wailsapp/examples/systray + +go 1.18 + +require github.com/wailsapp/wails/v2 v2.0.0 + +require ( + github.com/bep/debounce v1.2.1 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.9.0 // indirect + github.com/labstack/gommon v0.3.1 // indirect + github.com/leaanthony/go-ansi-parser v1.0.1 // indirect + github.com/leaanthony/gosod v1.0.3 // indirect + github.com/leaanthony/slicer v1.5.0 // indirect + github.com/mattn/go-colorable v0.1.11 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/samber/lo v1.27.1 // indirect + github.com/tkrajina/go-reflector v0.5.5 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.1 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect + golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/text v0.3.7 // indirect +) + +replace github.com/wailsapp/wails/v2 v2.0.0 => ../.. diff --git a/v2/examples/systray/go.sum b/v2/examples/systray/go.sum new file mode 100644 index 000000000..f37843098 --- /dev/null +++ b/v2/examples/systray/go.sum @@ -0,0 +1,82 @@ +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY= +github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= +github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= +github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4= +github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM= +github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ= +github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= +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/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc= +github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.27.1 h1:sTXwkRiIFIQG+G0HeAvOEnGjqWeWtI9cg5/n51KrxPg= +github.com/samber/lo v1.27.1/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= +github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ= +github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/v2/examples/systray/iconDarkMode.png b/v2/examples/systray/iconDarkMode.png new file mode 100644 index 000000000..8d72ee490 Binary files /dev/null and b/v2/examples/systray/iconDarkMode.png differ diff --git a/v2/examples/systray/iconLightMode.png b/v2/examples/systray/iconLightMode.png new file mode 100644 index 000000000..e8d341875 Binary files /dev/null and b/v2/examples/systray/iconLightMode.png differ diff --git a/v2/examples/systray/main.go b/v2/examples/systray/main.go new file mode 100644 index 000000000..d675ec4d1 --- /dev/null +++ b/v2/examples/systray/main.go @@ -0,0 +1,216 @@ +package main + +import ( + "context" + "embed" + "fmt" + "github.com/wailsapp/wails/v2/pkg/application" + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/windows" + "github.com/wailsapp/wails/v2/pkg/runtime" + "time" +) + +//go:embed all:frontend/dist +var assets embed.FS + +//go:embed iconLightMode.png +var lightModeIcon []byte + +//go:embed iconDarkMode.png +var darkModeIcon []byte + +func main() { + + var runtimeContext context.Context + + // Create a new Wails application using the current options + mainApp := application.NewWithOptions(&options.App{ + Assets: assets, + StartHidden: true, + HideWindowOnClose: true, + OnStartup: func(ctx context.Context) { + runtimeContext = ctx + }, + Windows: &windows.Options{ + BackdropType: windows.Acrylic, + WindowIsTranslucent: true, + WebviewIsTransparent: true, + DisableWindowIcon: true, + }, + }) + + // ------------------------------------ + // Create a systray for the application + // Currently we only support PNG for icons + + var systray *application.SystemTray + var showWindow = func() { + // Show the window + // In a future version of this API, it will be possible to + // create windows programmatically and be able to show/hide + // them from the systray with something like: + // + // myWindow := mainApp.NewWindow(...) + // mainApp.NewSystemTray(&options.SystemTray{ + // OnLeftClick: func() { + // myWindow.SetVisibility(!myWindow.IsVisible()) + // } + // }) + runtime.Show(runtimeContext) + } + systray = mainApp.NewSystemTray(&options.SystemTray{ + // This is the icon used when the system in using light mode + LightModeIcon: &options.SystemTrayIcon{ + Data: lightModeIcon, + }, + // This is the icon used when the system in using dark mode + DarkModeIcon: &options.SystemTrayIcon{ + Data: darkModeIcon, + }, + Tooltip: "Systray Example", + OnLeftClick: showWindow, + OnMenuClose: func() { + // Add the left click call after 500ms + // We do this because the left click fires right + // after the menu closes, and we don't want to show + // the window on menu close. + go func() { + time.Sleep(500 * time.Millisecond) + systray.OnLeftClick(showWindow) + }() + }, + OnMenuOpen: func() { + // Remove the left click callback + systray.OnLeftClick(func() {}) + }, + }) + + // --------------------------------------------------- + // Menu items are created in the order they are added. + // This is a contrived example to show what can be done + // with menus. + + // This is a menuitem we will show/hide at runtime + visibleNotVisible := menu.Label("visible?").Show() + + counter := 0 + icons := [][]byte{lightModeIcon, darkModeIcon} + iconCounter := 0 + + disabledEnabledMenu := menu.Label("disabled").Disable().OnClick(func(c *menu.CallbackData) { + println("Disabled item clicked!") + }) + + // This checkbox menuitem will print the current checked state to the console when clicked. + // When a checkbox item is clicked, the state of the `Checked` variable is toggled. + // The UI automatically reflects the current state, even if this item is used multiple times. + mycheckbox := menu.Label("checked").SetChecked(true).OnClick(func(c *menu.CallbackData) { + println("My checked state is: ", c.MenuItem.Checked) + }) + + // This radio callback will be used by all the radio items. + // The CallbackData has a pointer back to the menuitem, so we can determine + // which item was selected + radioCallback := func(data *menu.CallbackData) { + println("Radio item clicked:", data.MenuItem.Label) + } + + // We create 3 radio items , with the first being selected. They all share a callback. + radio1 := menu.Radio("Radio 1", true, nil, radioCallback) + radio2 := menu.Radio("Radio 2", false, nil, radioCallback) + radio3 := menu.Radio("Radio 3", false, nil, radioCallback) + + // Now we set the menu of the systray. + // This would likely be created in a different function/file + systray.SetMenu(menu.NewMenuFromItems( + + visibleNotVisible, + // This menu item changes its label when clicked. + menu.Label("Click Me!").OnClick(func(c *menu.CallbackData) { + counter++ + c.MenuItem.SetLabel(fmt.Sprintf("Clicked %d times", counter)) + systray.Update() + }), + + // We add a checkbox + menu.Separator(), + mycheckbox, + + // Next we create 2 radio groups containing the same menu items. + // It is perfectly fine to reuse radio item groups - the state and UI will + // stay in sync. Warning: Using the same radio item in different groups will + // lead to unspecified behaviour! + menu.Separator(), + radio1, + radio2, + radio3, + + menu.Separator(), + mycheckbox, + + menu.Label("Toggle items!").OnClick(func(c *menu.CallbackData) { + + iconCounter++ + + // Swap light and dark mode icons + systray.SetIcons(&options.SystemTrayIcon{ + Data: icons[iconCounter%2], + }, &options.SystemTrayIcon{ + Data: icons[(iconCounter+1)%2], + }) + + // Do some toggling + if iconCounter%2 == 0 { + visibleNotVisible.Show() + disabledEnabledMenu.Disable() + } else { + visibleNotVisible.Hide() + disabledEnabledMenu.Enable() + } + + // Update the menu + err := systray.Update() + if err != nil { + panic(err) + } + }), + + // We create a checkbox item that is initially unchecked. + menu.Label("unchecked").SetChecked(false).OnClick(func(c *menu.CallbackData) { + println("My checked state is: ", c.MenuItem.Checked) + systray.SetTooltip("My updated tooltip!") + }), + + // This menu item will toggle between enabled and disabled each time the "Toggle items!" menu + // option is selected + disabledEnabledMenu, + + // We now add a submenu, reusing the checkbox item and submenu we created earlier + menu.SubMenu("submenu", menu.NewMenuFromItems( + mycheckbox, + menu.Label("submenu item").OnClick(func(data *menu.CallbackData) { + println("submenu item clicked") + }), + menu.Separator(), + radio1, + radio2, + radio3, + )), + menu.Separator(), + menu.Label("quit").OnClick(func(_ *menu.CallbackData) { + println("Quitting application") + mainApp.Quit() + }), + )) + + println("Check out the system tray!") + + // Now we run the application + err := mainApp.Run() + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/examples/systray/wails.json b/v2/examples/systray/wails.json new file mode 100644 index 000000000..f532e0088 --- /dev/null +++ b/v2/examples/systray/wails.json @@ -0,0 +1,12 @@ +{ + "name": "systray", + "outputfilename": "systray", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "Lea Anthony", + "email": "lea.anthony@gmail.com" + } +} diff --git a/v2/go.mod b/v2/go.mod index 328334556..6e1a20fe1 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -44,6 +44,7 @@ require ( github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/samber/lo v1.27.1 github.com/stretchr/testify v1.7.1 + golang.org/x/image v0.0.0-20201208152932-35266b937fa6 golang.org/x/tools v0.1.12 ) @@ -83,7 +84,6 @@ require ( github.com/yuin/goldmark-emoji v1.0.1 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.h b/v2/internal/frontend/desktop/darwin/AppDelegate.h index e2dd841c9..56399ee9f 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.h +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.h @@ -17,6 +17,7 @@ @property bool startHidden; @property bool startFullscreen; @property (retain) WailsWindow* mainWindow; +@property int activationPolicy; @end diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.m b/v2/internal/frontend/desktop/darwin/AppDelegate.m index 6d46deae4..0f47caf67 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.m +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.m @@ -9,6 +9,7 @@ #import #import "AppDelegate.h" +#import "message.h" @implementation AppDelegate - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { @@ -25,6 +26,8 @@ } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + processNotification(0); // Notify Go + [NSApp setActivationPolicy:self.activationPolicy]; [NSApp activateIgnoringOtherApps:YES]; if ( self.startFullscreen ) { NSWindowCollectionBehavior behaviour = [self.mainWindow collectionBehavior]; diff --git a/v2/internal/frontend/desktop/darwin/Application.h b/v2/internal/frontend/desktop/darwin/Application.h index 94859f770..86138dda1 100644 --- a/v2/internal/frontend/desktop/darwin/Application.h +++ b/v2/internal/frontend/desktop/darwin/Application.h @@ -18,7 +18,7 @@ #define WindowStartsFullscreen 3 WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int debug, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight); -void Run(void*, const char* url); +void Run(void *inctx, const char* url, int activationPolicy); void SetTitle(void* ctx, const char *title); void Center(void* ctx); @@ -62,6 +62,16 @@ void AppendSubmenu(void* parent, void* child); void AppendRole(void *inctx, void *inMenu, int role); void SetAsApplicationMenu(void *inctx, void *inMenu); void UpdateApplicationMenu(void *inctx); +void SetMenuItemChecked(void* nsMenuItem, int checked); + +/* Tray Menu */ +void NewNSStatusItem(int id, int length); +void SetTrayMenu(void *nsStatusItem, void* nsMenu); +void SetTrayMenuLabel(void *nsStatusItem, const char *label); +void SetTrayImage(void *nsStatusItem, void *imageData, int imageDataLength, int template, int position); + +/* MenuItems */ +void SetMenuItemLabel(void *nsStatusItem, const char *label); void SetAbout(void *inctx, const char* title, const char* description, void* imagedata, int datalen); void* AppendMenuItem(void* inctx, void* nsmenu, const char* label, const char* shortcutKey, int modifiers, int disabled, int checked, int menuItemID); @@ -72,4 +82,7 @@ void ReleaseContext(void *inctx); NSString* safeInit(const char* input); + +int ScalingFactor(void *ctx); + #endif /* Application_h */ diff --git a/v2/internal/frontend/desktop/darwin/Application.m b/v2/internal/frontend/desktop/darwin/Application.m index 6b413036c..9ee3144ab 100644 --- a/v2/internal/frontend/desktop/darwin/Application.m +++ b/v2/internal/frontend/desktop/darwin/Application.m @@ -12,6 +12,7 @@ #import "AppDelegate.h" #import "WailsMenu.h" #import "WailsMenuItem.h" +#import "message.h" WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int debug, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight) { @@ -20,7 +21,7 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in WailsContext *result = [WailsContext new]; result.debug = debug; - + if ( windowStartState == WindowStartsFullscreen ) { fullscreen = 1; } @@ -28,7 +29,7 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in [result CreateWindow:width :height :frameless :resizable :fullscreen :fullSizeContent :hideTitleBar :titlebarAppearsTransparent :hideTitle :useToolbar :hideToolbarSeparator :webviewIsTransparent :hideWindowOnClose :safeInit(appearance) :windowIsTranslucent :minWidth :minHeight :maxWidth :maxHeight]; [result SetTitle:safeInit(title)]; [result Center]; - + switch( windowStartState ) { case WindowStartsMaximised: [result.mainWindow zoom:nil]; @@ -170,6 +171,10 @@ void ToggleMaximise(void* inctx) { ); } +void SetMenuItemChecked(void* nsMenuItem, int checked) { + [(NSMenuItem*)nsMenuItem setState:(checked == 0 ? NSOffState : NSOnState)]; +} + const char* GetSize(void *inctx) { WailsContext *ctx = (__bridge WailsContext*) inctx; NSRect frame = [ctx.mainWindow frame]; @@ -304,6 +309,55 @@ void AppendRole(void *inctx, void *inMenu, int role) { [menu appendRole :ctx :role]; } +void NewNSStatusItem(int id, int length) { + dispatch_async(dispatch_get_main_queue(), ^{ + + NSStatusBar *statusBar = [NSStatusBar systemStatusBar]; + // Map Go to Cocoa length. 0 = NSVariableStatusItemLength. + CGFloat length = NSVariableStatusItemLength; + if( length == 1 ) { + length = NSSquareStatusItemLength; + } + NSStatusItem *result = [[statusBar statusItemWithLength:length] retain]; + objectCreated(id,result); + + }); +} + +void DeleteStatusItem(void *_nsStatusItem) { + NSStatusItem *nsStatusItem = (NSStatusItem*) _nsStatusItem; + [nsStatusItem release]; +} + +void on_main_thread(void (^l)(void)) { + dispatch_async(dispatch_get_main_queue(), l); +} + +void SetTrayMenuLabel(void *_nsStatusItem, const char *label) { + on_main_thread(^{ + NSStatusItem *nsStatusItem = (NSStatusItem*) _nsStatusItem; + nsStatusItem.button.title = safeInit(label); + free((void*)label); + }); +} + +void SetTrayMenu(void *nsStatusItem, void* nsMenu) { + ON_MAIN_THREAD( + [(NSStatusItem*)nsStatusItem setMenu:(NSMenu *)nsMenu]; + ) +} + + +/**** Menu Item ****/ + +void SetMenuItemLabel(void *_nsMenuItem, const char *label) { + on_main_thread(^{ + NSMenuItem *nsMenuItem = (NSMenuItem*) _nsMenuItem; + [ nsMenuItem setTitle:safeInit(label) ]; + free((void*)label); + }); +} + void* NewMenu(const char *name) { NSString *title = @""; if (name != nil) { @@ -328,8 +382,8 @@ void SetAsApplicationMenu(void *inctx, void *inMenu) { void UpdateApplicationMenu(void *inctx) { WailsContext *ctx = (__bridge WailsContext*) inctx; ON_MAIN_THREAD( - NSApplication *app = [NSApplication sharedApplication]; - [app setMainMenu:ctx.applicationMenu]; + NSApplication *app = [NSApplication sharedApplication]; + [app setMainMenu:ctx.applicationMenu]; ) } @@ -354,7 +408,7 @@ void UpdateMenuItem(void* nsmenuitem, int checked) { ON_MAIN_THREAD( WailsMenuItem *menuItem = (__bridge WailsMenuItem*) nsmenuitem; [menuItem setState:(checked == 1?NSControlStateValueOn:NSControlStateValueOff)]; - ) + ) } @@ -363,12 +417,39 @@ void AppendSeparator(void* inMenu) { [menu AppendSeparator]; } +void SetTrayImage(void *nsStatusItem, void *imageData, int imageDataLength, int template, int position) { + ON_MAIN_THREAD( + NSStatusItem *statusItem = (NSStatusItem*) nsStatusItem; + NSData *nsdata = [NSData dataWithBytes:imageData length:imageDataLength]; + NSImage *image = [[[NSImage alloc] initWithData:nsdata] autorelease]; + if(template) { + image.template = true; + } + image.size = NSMakeSize(22.0, 22.0); + statusItem.button.image = image; + + // Swap NSNoImage and NSImageLeading because we wanted NSImageLeading to be default in Go + int actualPosition = position; + if( position == 7) { + actualPosition = 0; + } else if (position == 0) { + actualPosition = 7; + } + [statusItem.button setImagePosition:actualPosition]; + + ) +} +int ScalingFactor(void *ctx) { + CGFloat scale = [((WailsContext*)ctx).mainWindow backingScaleFactor]; + return (int)scale; +} -void Run(void *inctx, const char* url) { +void Run(void *inctx, const char* url, int activationPolicy) { WailsContext *ctx = (__bridge WailsContext*) inctx; NSApplication *app = [NSApplication sharedApplication]; AppDelegate* delegate = [AppDelegate new]; + delegate.activationPolicy = activationPolicy; [app setDelegate:(id)delegate]; ctx.appdelegate = delegate; delegate.mainWindow = ctx.mainWindow; diff --git a/v2/internal/frontend/desktop/darwin/WailsContext.h b/v2/internal/frontend/desktop/darwin/WailsContext.h index 0db6e61d9..3a25740e8 100644 --- a/v2/internal/frontend/desktop/darwin/WailsContext.h +++ b/v2/internal/frontend/desktop/darwin/WailsContext.h @@ -23,6 +23,7 @@ @property NSSize userMinSize; @property NSSize userMaxSize; +@property int activationPolicy; - (BOOL) canBecomeKeyWindow; - (void) applyWindowConstraints; diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go index a1eff8e8d..14d545d9f 100644 --- a/v2/internal/frontend/desktop/darwin/frontend.go +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -3,15 +3,6 @@ package darwin -/* -#cgo CFLAGS: -x objective-c -#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit -#import -#import "Application.h" -#import "WailsContext.h" - -#include -*/ import "C" import ( "bytes" @@ -31,14 +22,33 @@ import ( "github.com/wailsapp/wails/v2/internal/frontend" "github.com/wailsapp/wails/v2/internal/frontend/assetserver" "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/options" ) +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit +#import +#import "Application.h" +#import "WailsContext.h" + +#include +*/ +import "C" + const startURL = "wails://wails/" +type NotificationType uint8 + +const ( + ApplicationDidFinishLaunching NotificationType = 0 +) + var messageBuffer = make(chan string, 100) var requestBuffer = make(chan *request, 100) var callbackBuffer = make(chan uint, 10) +var notificationBuffer = make(chan NotificationType, 10) type Frontend struct { @@ -57,6 +67,19 @@ type Frontend struct { mainWindow *Window bindings *binding.Bindings dispatcher frontend.Dispatcher + trayMenus map[*menu.TrayMenu]*NSTrayMenu + + applicationDidFinishLaunching bool + notificationCallbacks map[NotificationType][]func() + trayMenusBuffer []*menu.TrayMenu +} + +func (f *Frontend) RunMainLoop() { + C.RunMainLoop() +} + +func (f *Frontend) WindowClose() { + C.ReleaseContext(f.mainWindow.context) } func (f *Frontend) RunMainLoop() { @@ -69,11 +92,13 @@ func (f *Frontend) WindowClose() { func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend { result := &Frontend{ - frontendOptions: appoptions, - logger: myLogger, - bindings: appBindings, - dispatcher: dispatcher, - ctx: ctx, + frontendOptions: appoptions, + logger: myLogger, + bindings: appBindings, + dispatcher: dispatcher, + ctx: ctx, + trayMenus: make(map[*menu.TrayMenu]*NSTrayMenu), + notificationCallbacks: make(map[NotificationType][]func()), } result.startURL, _ = url.Parse(startURL) @@ -101,10 +126,17 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. go result.startMessageProcessor() go result.startCallbackProcessor() + go result.startNotificationsProcessor() + + result.registerNotificationCallback(ApplicationDidFinishLaunching, result.processTrayMenus) return result } +func (f *Frontend) registerNotificationCallback(notificationType NotificationType, callback func()) { + f.notificationCallbacks[notificationType] = append(f.notificationCallbacks[notificationType], callback) +} + func (f *Frontend) startMessageProcessor() { for message := range messageBuffer { f.processMessage(message) @@ -123,6 +155,11 @@ func (f *Frontend) startCallbackProcessor() { } } } +func (f *Frontend) startNotificationsProcessor() { + for notification := range notificationBuffer { + f.handleNotification(notification) + } +} func (f *Frontend) WindowReload() { f.ExecJS("runtime.WindowReload();") @@ -160,7 +197,11 @@ func (f *Frontend) Run(ctx context.Context) error { f.frontendOptions.OnStartup(f.ctx) } }() - mainWindow.Run(f.startURL.String()) + var activationPolicy C.int + if f.frontendOptions != nil && f.frontendOptions.Mac != nil { + activationPolicy = C.int(f.frontendOptions.Mac.ActivationPolicy) + } + mainWindow.Run(f.startURL.String(), activationPolicy) return nil } @@ -444,6 +485,11 @@ func processMessage(message *C.char) { messageBuffer <- goMessage } +//export processNotification +func processNotification(notification NotificationType) { + notificationBuffer <- notification +} + //export processURLRequest func processURLRequest(ctx unsafe.Pointer, requestId C.ulonglong, url *C.char, method *C.char, headers *C.char, body unsafe.Pointer, bodyLen C.int) { var goBody []byte @@ -465,3 +511,19 @@ func processURLRequest(ctx unsafe.Pointer, requestId C.ulonglong, url *C.char, m func processCallback(callbackID uint) { callbackBuffer <- callbackID } + +func (f *Frontend) handleNotification(notification NotificationType) { + switch notification { + case ApplicationDidFinishLaunching: + f.applicationDidFinishLaunching = true + for _, callback := range f.notificationCallbacks[notification] { + go callback() + } + } +} + +func (f *Frontend) processTrayMenus() { + for _, trayMenu := range f.trayMenusBuffer { + f.mainWindow.TrayMenuAdd(trayMenu) + } +} diff --git a/v2/internal/frontend/desktop/darwin/menu.go b/v2/internal/frontend/desktop/darwin/menu.go index 8afb63fb9..f22097b01 100644 --- a/v2/internal/frontend/desktop/darwin/menu.go +++ b/v2/internal/frontend/desktop/darwin/menu.go @@ -14,12 +14,86 @@ package darwin */ import "C" import ( + "fmt" + "sync" "unsafe" + "github.com/google/uuid" + "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/menu/keys" ) +var createNSObjectMap = make(map[uint32]chan unsafe.Pointer) +var createNSObjectMapLock sync.RWMutex + +func waitNSObjectCreate(id uint32, fn func()) unsafe.Pointer { + waitchan := make(chan unsafe.Pointer) + createNSObjectMapLock.Lock() + createNSObjectMap[id] = waitchan + createNSObjectMapLock.Unlock() + fn() + result := <-waitchan + createNSObjectMapLock.Lock() + createNSObjectMap[id] = nil + createNSObjectMapLock.Unlock() + return result +} + +//export objectCreated +func objectCreated(id uint32, pointer unsafe.Pointer) { + createNSObjectMapLock.Lock() + createNSObjectMap[id] <- pointer + createNSObjectMapLock.Unlock() +} + +func NewNSTrayMenu(context unsafe.Pointer, trayMenu *menu.TrayMenu, scalingFactor int) *NSTrayMenu { + c := NewCalloc() + defer c.Free() + + id := uuid.New().ID() + nsStatusItem := waitNSObjectCreate(id, func() { + C.NewNSStatusItem(C.int(id), C.int(trayMenu.Sizing)) + }) + result := &NSTrayMenu{ + context: context, + nsStatusItem: nsStatusItem, + scalingFactor: scalingFactor, + } + + result.SetLabel(trayMenu.Label) + result.SetMenu(trayMenu.Menu) + result.SetImage(trayMenu.Image) + + return result +} + +func (n *NSTrayMenu) SetImage(image *menu.TrayImage) { + if image == nil { + return + } + bitmap := image.GetBestBitmap(n.scalingFactor, false) + if bitmap == nil { + fmt.Printf("[Warning] No TrayMenu Image available for scaling factor %dx\n", n.scalingFactor) + return + } + C.SetTrayImage(n.nsStatusItem, + unsafe.Pointer(&bitmap[0]), + C.int(len(bitmap)), + bool2Cint(image.IsTemplate), + C.int(image.Position), + ) +} + +func (n *NSTrayMenu) SetMenu(menu *menu.Menu) { + if menu == nil { + return + } + theMenu := NewNSMenu(n.context, "") + processMenu(theMenu, menu) + C.SetTrayMenu(n.nsStatusItem, theMenu.nsmenu) +} + type NSMenu struct { context unsafe.Pointer nsmenu unsafe.Pointer @@ -53,6 +127,15 @@ type MenuItem struct { radioGroupMembers []*MenuItem } +func (m *MenuItem) SetChecked(value bool) { + C.SetMenuItemChecked(m.nsmenuitem, bool2Cint(value)) +} + +func (m *MenuItem) SetLabel(label string) { + cLabel := C.CString(label) + C.SetMenuItemLabel(m.nsmenuitem, cLabel) +} + func (m *NSMenu) AddMenuItem(menuItem *menu.MenuItem) *MenuItem { c := NewCalloc() defer c.Free() @@ -69,6 +152,7 @@ func (m *NSMenu) AddMenuItem(menuItem *menu.MenuItem) *MenuItem { result.id = createMenuItemID(result) result.nsmenuitem = C.AppendMenuItem(m.context, m.nsmenu, c.String(menuItem.Label), key, modifier, bool2Cint(menuItem.Disabled), bool2Cint(menuItem.Checked), C.int(result.id)) + menuItem.Impl = result return result } diff --git a/v2/internal/frontend/desktop/darwin/message.h b/v2/internal/frontend/desktop/darwin/message.h index b3405f4ab..be4248c1d 100644 --- a/v2/internal/frontend/desktop/darwin/message.h +++ b/v2/internal/frontend/desktop/darwin/message.h @@ -20,6 +20,8 @@ void processMessageDialogResponse(int); void processOpenFileDialogResponse(const char*); void processSaveFileDialogResponse(const char*); void processCallback(int); +void processNotification(int); +void objectCreated(int, void*); #ifdef __cplusplus } diff --git a/v2/internal/frontend/desktop/darwin/traymenu.go b/v2/internal/frontend/desktop/darwin/traymenu.go new file mode 100644 index 000000000..6539b359e --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/traymenu.go @@ -0,0 +1,44 @@ +//go:build darwin +// +build darwin + +package darwin + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit +#import +#import "Application.h" +#import "WailsContext.h" + +#include +*/ +import "C" +import ( + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/menu" +) + +func (f *Frontend) TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl { + nsTrayMenu := f.mainWindow.TrayMenuAdd(trayMenu) + f.trayMenus[trayMenu] = nsTrayMenu + return nsTrayMenu +} + +type NSTrayMenu struct { + context unsafe.Pointer + nsStatusItem unsafe.Pointer // NSStatusItem + scalingFactor int +} + +func (n *NSTrayMenu) SetLabel(label string) { + if label == "" { + return + } + cLabel := C.CString(label) + C.SetTrayMenuLabel(n.nsStatusItem, cLabel) +} + +func (w *Window) TrayMenuAdd(trayMenu *menu.TrayMenu) *NSTrayMenu { + return NewNSTrayMenu(w.context, trayMenu, ScalingFactor(w)) +} diff --git a/v2/internal/frontend/desktop/darwin/window.go b/v2/internal/frontend/desktop/darwin/window.go index 88d0e3aa9..5d09765f3 100644 --- a/v2/internal/frontend/desktop/darwin/window.go +++ b/v2/internal/frontend/desktop/darwin/window.go @@ -40,6 +40,10 @@ func bool2Cint(value bool) C.int { return C.int(0) } +func ScalingFactor(window *Window) int { + return int(C.ScalingFactor(window.context)) +} + func NewWindow(frontendOptions *options.App, debugMode bool) *Window { c := NewCalloc() @@ -122,9 +126,9 @@ func (w *Window) Center() { C.Center(w.context) } -func (w *Window) Run(url string) { +func (w *Window) Run(url string, activationPolicy C.int) { _url := C.CString(url) - C.Run(w.context, _url) + C.Run(w.context, _url, activationPolicy) C.free(unsafe.Pointer(_url)) } diff --git a/v2/internal/frontend/desktop/windows/frontend.go b/v2/internal/frontend/desktop/windows/frontend.go index 93cfbec59..ee610c9d1 100644 --- a/v2/internal/frontend/desktop/windows/frontend.go +++ b/v2/internal/frontend/desktop/windows/frontend.go @@ -373,11 +373,11 @@ func (f *Frontend) ScreenGetAll() ([]Screen, error) { } func (f *Frontend) Show() { - f.mainWindow.Show() + f.WindowShow() } func (f *Frontend) Hide() { - f.mainWindow.Hide() + f.WindowHide() } func (f *Frontend) WindowIsMaximised() bool { diff --git a/v2/internal/frontend/desktop/windows/png.go b/v2/internal/frontend/desktop/windows/png.go new file mode 100644 index 000000000..87e7a8d96 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/png.go @@ -0,0 +1,36 @@ +package windows + +import ( + "bufio" + "bytes" + "golang.org/x/image/draw" + "image" + "image/png" +) + +func ResizePNG(in []byte, size int) ([]byte, error) { + imagedata, _, err := image.Decode(bytes.NewReader(in)) + if err != nil { + return nil, err + } + // Scale image + rect := image.Rect(0, 0, size, size) + rawdata := image.NewRGBA(rect) + scale := draw.CatmullRom + scale.Scale(rawdata, rect, imagedata, imagedata.Bounds(), draw.Over, nil) + + // Convert back to PNG + icondata := new(bytes.Buffer) + writer := bufio.NewWriter(icondata) + err = png.Encode(writer, rawdata) + if err != nil { + return nil, err + } + err = writer.Flush() + if err != nil { + return nil, err + } + + // Save image data + return icondata.Bytes(), nil +} diff --git a/v2/internal/frontend/desktop/windows/traymenu.go b/v2/internal/frontend/desktop/windows/traymenu.go new file mode 100644 index 000000000..a2efee650 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/traymenu.go @@ -0,0 +1,79 @@ +//go:build windows +// +build windows + +package windows + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32" + "github.com/wailsapp/wails/v2/pkg/menu" + "log" + "sync" + "unsafe" +) + +var uids uint32 +var lock sync.RWMutex + +func newUID() uint32 { + lock.Lock() + result := uids + uids++ + lock.Unlock() + return result + +} + +type Win32TrayMenu struct { + hwnd uintptr + uid uint32 + icon uintptr +} + +func (w *Win32TrayMenu) SetLabel(label string) {} + +func (w *Win32TrayMenu) SetMenu(menu *menu.Menu) {} + +func (w *Win32TrayMenu) SetImage(image *menu.TrayImage) { + data := w.newNotifyIconData() + bitmap := image.GetBestBitmap(1, false) + icon, err := win32.CreateIconFromResourceEx(uintptr(unsafe.Pointer(&bitmap[0])), uint32(len(bitmap)), true, 0x30000, 0, 0, 0) + if err != nil { + log.Fatal(err.Error()) + } + data.UFlags |= win32.NIF_ICON + data.HIcon = icon + if _, err := win32.NotifyIcon(win32.NIM_MODIFY, data); err != nil { + log.Fatal(err.Error()) + } +} + +func (f *Frontend) NewWin32TrayMenu(trayMenu *menu.TrayMenu) *Win32TrayMenu { + + result := &Win32TrayMenu{ + hwnd: f.mainWindow.Handle(), + uid: newUID(), + } + + data := result.newNotifyIconData() + data.UFlags |= win32.NIF_MESSAGE | win32.NIF_ICON + data.UCallbackMessage = win32.WM_APP + result.uid + if _, err := win32.NotifyIcon(win32.NIM_ADD, data); err != nil { + log.Fatal(err.Error()) + } + + return result +} + +func (w *Win32TrayMenu) newNotifyIconData() *win32.NOTIFYICONDATA { + var data win32.NOTIFYICONDATA + data.CbSize = uint32(unsafe.Sizeof(data)) + data.UFlags = win32.NIF_GUID + data.HWnd = w.hwnd + data.UID = w.uid + return &data +} + +func (f *Frontend) TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl { + win32TrayMenu := f.NewWin32TrayMenu(trayMenu) + return win32TrayMenu +} diff --git a/v2/internal/frontend/desktop/windows/win32/consts.go b/v2/internal/frontend/desktop/windows/win32/consts.go index 98d869c72..cb6e6d0f1 100644 --- a/v2/internal/frontend/desktop/windows/win32/consts.go +++ b/v2/internal/frontend/desktop/windows/win32/consts.go @@ -13,16 +13,26 @@ type HANDLE uintptr type HMONITOR HANDLE var ( - moduser32 = syscall.NewLazyDLL("user32.dll") - procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") - procGetWindowLong = moduser32.NewProc("GetWindowLongW") - procSetClassLong = moduser32.NewProc("SetClassLongW") - procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW") - procShowWindow = moduser32.NewProc("ShowWindow") - procIsWindowVisible = moduser32.NewProc("IsWindowVisible") - procGetWindowRect = moduser32.NewProc("GetWindowRect") - procGetMonitorInfo = moduser32.NewProc("GetMonitorInfoW") - procMonitorFromWindow = moduser32.NewProc("MonitorFromWindow") + moduser32 = syscall.NewLazyDLL("user32.dll") + procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") + procGetWindowLong = moduser32.NewProc("GetWindowLongW") + procSetClassLong = moduser32.NewProc("SetClassLongW") + procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW") + procShowWindow = moduser32.NewProc("ShowWindow") + procIsWindowVisible = moduser32.NewProc("IsWindowVisible") + procGetWindowRect = moduser32.NewProc("GetWindowRect") + procGetMonitorInfo = moduser32.NewProc("GetMonitorInfoW") + procMonitorFromWindow = moduser32.NewProc("MonitorFromWindow") + procLookupIconIdFromDirectoryEx = moduser32.NewProc("LookupIconIdFromDirectoryEx") + procCreateIconFromResourceEx = moduser32.NewProc("CreateIconFromResourceEx") + procCreateIconIndirect = moduser32.NewProc("CreateIconIndirect") + procLoadImageW = moduser32.NewProc("LoadImageW") + procBringWindowToTop = moduser32.NewProc("BringWindowToTop") +) + +var ( + modshell32 = syscall.NewLazyDLL("shell32.dll") + procShellNotifyIcon = modshell32.NewProc("Shell_NotifyIconW") ) var ( moddwmapi = syscall.NewLazyDLL("dwmapi.dll") @@ -41,3 +51,209 @@ func IsWindowsVersionAtLeast(major, minor, buildNumber int) bool { windowsVersion.Minor >= minor && windowsVersion.Build >= buildNumber } + +const ( + WM_APP = 32768 + WM_ACTIVATE = 6 + WM_ACTIVATEAPP = 28 + WM_AFXFIRST = 864 + WM_AFXLAST = 895 + WM_ASKCBFORMATNAME = 780 + WM_CANCELJOURNAL = 75 + WM_CANCELMODE = 31 + WM_CAPTURECHANGED = 533 + WM_CHANGECBCHAIN = 781 + WM_CHAR = 258 + WM_CHARTOITEM = 47 + WM_CHILDACTIVATE = 34 + WM_CLEAR = 771 + WM_CLOSE = 16 + WM_COMMAND = 273 + WM_COMMNOTIFY = 68 /* OBSOLETE */ + WM_COMPACTING = 65 + WM_COMPAREITEM = 57 + WM_CONTEXTMENU = 123 + WM_COPY = 769 + WM_COPYDATA = 74 + WM_CREATE = 1 + WM_CTLCOLORBTN = 309 + WM_CTLCOLORDLG = 310 + WM_CTLCOLOREDIT = 307 + WM_CTLCOLORLISTBOX = 308 + WM_CTLCOLORMSGBOX = 306 + WM_CTLCOLORSCROLLBAR = 311 + WM_CTLCOLORSTATIC = 312 + WM_CUT = 768 + WM_DEADCHAR = 259 + WM_DELETEITEM = 45 + WM_DESTROY = 2 + WM_DESTROYCLIPBOARD = 775 + WM_DEVICECHANGE = 537 + WM_DEVMODECHANGE = 27 + WM_DISPLAYCHANGE = 126 + WM_DRAWCLIPBOARD = 776 + WM_DRAWITEM = 43 + WM_DROPFILES = 563 + WM_ENABLE = 10 + WM_ENDSESSION = 22 + WM_ENTERIDLE = 289 + WM_ENTERMENULOOP = 529 + WM_ENTERSIZEMOVE = 561 + WM_ERASEBKGND = 20 + WM_EXITMENULOOP = 530 + WM_EXITSIZEMOVE = 562 + WM_FONTCHANGE = 29 + WM_GETDLGCODE = 135 + WM_GETFONT = 49 + WM_GETHOTKEY = 51 + WM_GETICON = 127 + WM_GETMINMAXINFO = 36 + WM_GETTEXT = 13 + WM_GETTEXTLENGTH = 14 + WM_HANDHELDFIRST = 856 + WM_HANDHELDLAST = 863 + WM_HELP = 83 + WM_HOTKEY = 786 + WM_HSCROLL = 276 + WM_HSCROLLCLIPBOARD = 782 + WM_ICONERASEBKGND = 39 + WM_INITDIALOG = 272 + WM_INITMENU = 278 + WM_INITMENUPOPUP = 279 + WM_INPUT = 0x00FF + WM_INPUTLANGCHANGE = 81 + WM_INPUTLANGCHANGEREQUEST = 80 + WM_KEYDOWN = 256 + WM_KEYUP = 257 + WM_KILLFOCUS = 8 + WM_MDIACTIVATE = 546 + WM_MDICASCADE = 551 + WM_MDICREATE = 544 + WM_MDIDESTROY = 545 + WM_MDIGETACTIVE = 553 + WM_MDIICONARRANGE = 552 + WM_MDIMAXIMIZE = 549 + WM_MDINEXT = 548 + WM_MDIREFRESHMENU = 564 + WM_MDIRESTORE = 547 + WM_MDISETMENU = 560 + WM_MDITILE = 550 + WM_MEASUREITEM = 44 + WM_GETOBJECT = 0x003D + WM_CHANGEUISTATE = 0x0127 + WM_UPDATEUISTATE = 0x0128 + WM_QUERYUISTATE = 0x0129 + WM_UNINITMENUPOPUP = 0x0125 + WM_MENURBUTTONUP = 290 + WM_MENUCOMMAND = 0x0126 + WM_MENUGETOBJECT = 0x0124 + WM_MENUDRAG = 0x0123 + WM_APPCOMMAND = 0x0319 + WM_MENUCHAR = 288 + WM_MENUSELECT = 287 + WM_MOVE = 3 + WM_MOVING = 534 + WM_NCACTIVATE = 134 + WM_NCCALCSIZE = 131 + WM_NCCREATE = 129 + WM_NCDESTROY = 130 + WM_NCHITTEST = 132 + WM_NCLBUTTONDBLCLK = 163 + WM_NCLBUTTONDOWN = 161 + WM_NCLBUTTONUP = 162 + WM_NCMBUTTONDBLCLK = 169 + WM_NCMBUTTONDOWN = 167 + WM_NCMBUTTONUP = 168 + WM_NCXBUTTONDOWN = 171 + WM_NCXBUTTONUP = 172 + WM_NCXBUTTONDBLCLK = 173 + WM_NCMOUSEHOVER = 0x02A0 + WM_NCMOUSELEAVE = 0x02A2 + WM_NCMOUSEMOVE = 160 + WM_NCPAINT = 133 + WM_NCRBUTTONDBLCLK = 166 + WM_NCRBUTTONDOWN = 164 + WM_NCRBUTTONUP = 165 + WM_NEXTDLGCTL = 40 + WM_NEXTMENU = 531 + WM_NOTIFY = 78 + WM_NOTIFYFORMAT = 85 + WM_NULL = 0 + WM_PAINT = 15 + WM_PAINTCLIPBOARD = 777 + WM_PAINTICON = 38 + WM_PALETTECHANGED = 785 + WM_PALETTEISCHANGING = 784 + WM_PARENTNOTIFY = 528 + WM_PASTE = 770 + WM_PENWINFIRST = 896 + WM_PENWINLAST = 911 + WM_POWER = 72 + WM_PRINT = 791 + WM_PRINTCLIENT = 792 + WM_QUERYDRAGICON = 55 + WM_QUERYENDSESSION = 17 + WM_QUERYNEWPALETTE = 783 + WM_QUERYOPEN = 19 + WM_QUEUESYNC = 35 + WM_QUIT = 18 + WM_RENDERALLFORMATS = 774 + WM_RENDERFORMAT = 773 + WM_SETCURSOR = 32 + WM_SETFOCUS = 7 + WM_SETFONT = 48 + WM_SETHOTKEY = 50 + WM_SETICON = 128 + WM_SETREDRAW = 11 + WM_SETTEXT = 12 + WM_SETTINGCHANGE = 26 + WM_SHOWWINDOW = 24 + WM_SIZE = 5 + WM_SIZECLIPBOARD = 779 + WM_SIZING = 532 + WM_SPOOLERSTATUS = 42 + WM_STYLECHANGED = 125 + WM_STYLECHANGING = 124 + WM_SYSCHAR = 262 + WM_SYSCOLORCHANGE = 21 + WM_SYSCOMMAND = 274 + WM_SYSDEADCHAR = 263 + WM_SYSKEYDOWN = 260 + WM_SYSKEYUP = 261 + WM_TCARD = 82 + WM_THEMECHANGED = 794 + WM_TIMECHANGE = 30 + WM_TIMER = 275 + WM_UNDO = 772 + WM_USER = 1024 + WM_USERCHANGED = 84 + WM_VKEYTOITEM = 46 + WM_VSCROLL = 277 + WM_VSCROLLCLIPBOARD = 778 + WM_WINDOWPOSCHANGED = 71 + WM_WINDOWPOSCHANGING = 70 + WM_WININICHANGE = 26 + WM_KEYFIRST = 256 + WM_KEYLAST = 264 + WM_SYNCPAINT = 136 + WM_MOUSEACTIVATE = 33 + WM_MOUSEMOVE = 512 + WM_LBUTTONDOWN = 513 + WM_LBUTTONUP = 514 + WM_LBUTTONDBLCLK = 515 + WM_RBUTTONDOWN = 516 + WM_RBUTTONUP = 517 + WM_RBUTTONDBLCLK = 518 + WM_MBUTTONDOWN = 519 + WM_MBUTTONUP = 520 + WM_MBUTTONDBLCLK = 521 + WM_MOUSEWHEEL = 522 + WM_MOUSEFIRST = 512 + WM_XBUTTONDOWN = 523 + WM_XBUTTONUP = 524 + WM_XBUTTONDBLCLK = 525 + WM_MOUSELAST = 525 + WM_MOUSEHOVER = 0x2A1 + WM_MOUSELEAVE = 0x2A3 + WM_CLIPBOARDUPDATE = 0x031D +) diff --git a/v2/internal/frontend/desktop/windows/win32/tray.go b/v2/internal/frontend/desktop/windows/win32/tray.go new file mode 100644 index 000000000..62f1a79cf --- /dev/null +++ b/v2/internal/frontend/desktop/windows/win32/tray.go @@ -0,0 +1,133 @@ +package win32 + +import ( + "golang.org/x/sys/windows" + "unsafe" +) + +const ( + NIF_MESSAGE = 0x00000001 + NIF_ICON = 0x00000002 + NIF_TIP = 0x00000004 + NIF_STATE = 0x00000008 + NIF_INFO = 0x00000010 + NIF_GUID = 0x00000020 + NIF_REALTIME = 0x00000040 + NIF_SHOWTIP = 0x00000080 + NIM_ADD = 0x00000000 + NIM_MODIFY = 0x00000001 + NIM_DELETE = 0x00000002 + NIM_SETFOCUS = 0x00000003 + NIM_SETVERSION = 0x00000004 + NIS_HIDDEN = 0x00000001 + NIS_SHAREDICON = 0x00000002 + NIN_BALLOONSHOW = 0x0402 + NIN_BALLOONTIMEOUT = 0x0404 + NIN_BALLOONUSERCLICK = 0x0405 + NIIF_NONE = 0x00000000 + NIIF_INFO = 0x00000001 + NIIF_WARNING = 0x00000002 + NIIF_ERROR = 0x00000003 + NIIF_USER = 0x00000004 + NIIF_NOSOUND = 0x00000010 + NIIF_LARGE_ICON = 0x00000020 + NIIF_RESPECT_QUIET_TIME = 0x00000080 + NIIF_ICON_MASK = 0x0000000F +) + +type NOTIFYICONDATA struct { + CbSize uint32 + HWnd uintptr + UID uint32 + UFlags uint32 + UCallbackMessage uint32 + HIcon uintptr + SzTip [128]uint16 + DwState uint32 + DwStateMask uint32 + SzInfo [256]uint16 + UVersion uint32 + SzInfoTitle [64]uint16 + DwInfoFlags uint32 + GUIDItem windows.GUID + HBalloonIcon uintptr +} + +func NotifyIcon(msg uint32, lpData *NOTIFYICONDATA) (int32, error) { + r, _, err := procShellNotifyIcon.Call( + uintptr(msg), + uintptr(unsafe.Pointer(lpData))) + if r == 0 { + return 0, err + } + return int32(r), nil +} + +func LookupIconIdFromDirectoryEx(presbits uintptr, isIcon bool, cxDesired int, cyDesired int, flags uint) (int32, error) { + var icon uint32 = 0 + if isIcon { + icon = 1 + } + r, _, err := procLookupIconIdFromDirectoryEx.Call( + presbits, + uintptr(icon), + uintptr(cxDesired), + uintptr(cyDesired), + uintptr(flags), + ) + if r == 0 { + return 0, err + } + return int32(r), nil +} + +func CreateIconIndirect(data uintptr) (uintptr, error) { + r, _, err := procCreateIconIndirect.Call( + data, + ) + + if r == 0 { + return 0, err + } + return r, nil +} + +func CreateIconFromResourceEx(presbits uintptr, dwResSize uint32, isIcon bool, version uint32, cxDesired int, cyDesired int, flags uint) (uintptr, error) { + icon := 0 + if isIcon { + icon = 1 + } + r, _, err := procCreateIconFromResourceEx.Call( + presbits, + uintptr(dwResSize), + uintptr(icon), + uintptr(version), + uintptr(cxDesired), + uintptr(cyDesired), + uintptr(flags), + ) + + if r == 0 { + return 0, err + } + return r, nil +} + +func LoadImage( + hInst uintptr, + name *uint16, + type_ uint32, + cx, cy int32, + fuLoad uint32) (uintptr, error) { + r, _, err := procLoadImageW.Call( + hInst, + uintptr(unsafe.Pointer(name)), + uintptr(type_), + uintptr(cx), + uintptr(cy), + uintptr(fuLoad)) + if r == 0 { + return 0, err + } + return r, nil +} diff --git a/v2/internal/frontend/desktop/windows/win32/window.go b/v2/internal/frontend/desktop/windows/win32/window.go index 1d6c9438c..ff6df5ec3 100644 --- a/v2/internal/frontend/desktop/windows/win32/window.go +++ b/v2/internal/frontend/desktop/windows/win32/window.go @@ -218,3 +218,10 @@ func GetMonitorInfo(hMonitor HMONITOR, lmpi *MONITORINFO) bool { ) return ret != 0 } + +func BringWindowToTop(hwnd uintptr) bool { + ret, _, _ := procBringWindowToTop.Call( + hwnd, + ) + return ret != 0 +} diff --git a/v2/internal/frontend/frontend.go b/v2/internal/frontend/frontend.go index cbf80346c..2bfc126b3 100644 --- a/v2/internal/frontend/frontend.go +++ b/v2/internal/frontend/frontend.go @@ -121,4 +121,7 @@ type Frontend interface { // Browser BrowserOpenURL(url string) + + // Tray Menu + TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl } diff --git a/v2/internal/frontend/options/frontend.go b/v2/internal/frontend/options/frontend.go new file mode 100644 index 000000000..aaab4459a --- /dev/null +++ b/v2/internal/frontend/options/frontend.go @@ -0,0 +1,9 @@ +package options + +import "github.com/wailsapp/wails/v2/pkg/options" + +// Frontend contains options for creating the Frontend +type Frontend struct { + options.App + HasMainWindow bool +} diff --git a/v2/internal/menumanager/contextmenu.go b/v2/internal/menumanager/contextmenu.go index 77c47891c..b87b476b7 100644 --- a/v2/internal/menumanager/contextmenu.go +++ b/v2/internal/menumanager/contextmenu.go @@ -1,12 +1,15 @@ package menumanager -import ( - "encoding/json" - "fmt" - - "github.com/wailsapp/wails/v2/pkg/menu" -) +import "github.com/wailsapp/wails/v2/pkg/menu" +// +//import ( +// "encoding/json" +// "fmt" +// +// "github.com/wailsapp/wails/v2/pkg/menu" +//) +// type ContextMenu struct { ID string ProcessedMenu *WailsMenu @@ -14,48 +17,49 @@ type ContextMenu struct { menu *menu.Menu } -func (t *ContextMenu) AsJSON() (string, error) { - data, err := json.Marshal(t) - if err != nil { - return "", err - } - return string(data), nil -} - -func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu { - - result := &ContextMenu{ - ID: contextMenu.ID, - menu: contextMenu.Menu, - menuItemMap: NewMenuItemMap(), - } - - result.menuItemMap.AddMenu(contextMenu.Menu) - result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu) - - return result -} - -func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) { - - newContextMenu := NewContextMenu(contextMenu) - - // Save the references - m.contextMenus[contextMenu.ID] = newContextMenu - m.contextMenuPointers[contextMenu] = contextMenu.ID -} - -func (m *Manager) UpdateContextMenu(contextMenu *menu.ContextMenu) (string, error) { - contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu] - if !contextMenuKnown { - return "", fmt.Errorf("unknown Context Menu '%s'. Please add the context menu using AddContextMenu()", contextMenu.ID) - } - - // Create the updated context menu - updatedContextMenu := NewContextMenu(contextMenu) - - // Save the reference - m.contextMenus[contextMenuID] = updatedContextMenu - - return updatedContextMenu.AsJSON() -} +// +//func (t *ContextMenu) AsJSON() (string, error) { +// data, err := json.Marshal(t) +// if err != nil { +// return "", err +// } +// return string(data), nil +//} +// +//func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu { +// +// result := &ContextMenu{ +// ID: contextMenu.ID, +// menu: contextMenu.Menu, +// menuItemMap: NewMenuItemMap(), +// } +// +// result.menuItemMap.AddMenu(contextMenu.Menu) +// result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu) +// +// return result +//} +// +//func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) { +// +// newContextMenu := NewContextMenu(contextMenu) +// +// // Save the references +// m.contextMenus[contextMenu.ID] = newContextMenu +// m.contextMenuPointers[contextMenu] = contextMenu.ID +//} +// +//func (m *Manager) UpdateContextMenu(contextMenu *menu.ContextMenu) (string, error) { +// contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu] +// if !contextMenuKnown { +// return "", fmt.Errorf("unknown Context Menu '%s'. Please add the context menu using AddContextMenu()", contextMenu.ID) +// } +// +// // Create the updated context menu +// updatedContextMenu := NewContextMenu(contextMenu) +// +// // Save the reference +// m.contextMenus[contextMenuID] = updatedContextMenu +// +// return updatedContextMenu.AsJSON() +//} diff --git a/v2/internal/menumanager/menumanager.go b/v2/internal/menumanager/menumanager.go index ea7939415..92756edff 100644 --- a/v2/internal/menumanager/menumanager.go +++ b/v2/internal/menumanager/menumanager.go @@ -2,6 +2,7 @@ package menumanager import ( "fmt" + "github.com/wailsapp/wails/v2/pkg/menu" ) diff --git a/v2/internal/menumanager/traymenu.go b/v2/internal/menumanager/traymenu.go index aed5b05ac..ebbbe7c3b 100644 --- a/v2/internal/menumanager/traymenu.go +++ b/v2/internal/menumanager/traymenu.go @@ -1,15 +1,11 @@ package menumanager import ( - "encoding/json" - "fmt" "strconv" - "strings" "sync" "github.com/leaanthony/go-ansi-parser" - "github.com/pkg/errors" "github.com/wailsapp/wails/v2/pkg/menu" ) @@ -32,7 +28,7 @@ type TrayMenu struct { FontName string Disabled bool Tooltip string `json:",omitempty"` - Image string + Image []byte MacTemplateImage bool RGBA string menuItemMap *MenuItemMap @@ -42,181 +38,182 @@ type TrayMenu struct { StyledLabel []*ansi.StyledText `json:",omitempty"` } -func (t *TrayMenu) AsJSON() (string, error) { - data, err := json.Marshal(t) - if err != nil { - return "", err - } - return string(data), nil -} - -func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu { - - // Parse ANSI text - var styledLabel []*ansi.StyledText - tempLabel := trayMenu.Label - if strings.Contains(tempLabel, "\033[") { - parsedLabel, err := ansi.Parse(tempLabel) - if err == nil { - styledLabel = parsedLabel - } - } - - result := &TrayMenu{ - Label: trayMenu.Label, - FontName: trayMenu.FontName, - FontSize: trayMenu.FontSize, - Disabled: trayMenu.Disabled, - Tooltip: trayMenu.Tooltip, - Image: trayMenu.Image, - MacTemplateImage: trayMenu.MacTemplateImage, - menu: trayMenu.Menu, - RGBA: trayMenu.RGBA, - menuItemMap: NewMenuItemMap(), - trayMenu: trayMenu, - StyledLabel: styledLabel, - } - - result.menuItemMap.AddMenu(trayMenu.Menu) - result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu) - - return result -} - -func (m *Manager) OnTrayMenuOpen(id string) { - trayMenu, ok := m.trayMenus[id] - if !ok { - return - } - if trayMenu.trayMenu.OnOpen == nil { - return - } - go trayMenu.trayMenu.OnOpen() -} - -func (m *Manager) OnTrayMenuClose(id string) { - trayMenu, ok := m.trayMenus[id] - if !ok { - return - } - if trayMenu.trayMenu.OnClose == nil { - return - } - go trayMenu.trayMenu.OnClose() -} - -func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) { - newTrayMenu := NewTrayMenu(trayMenu) - - // Hook up a new ID - trayID := generateTrayID() - newTrayMenu.ID = trayID - - // Save the references - m.trayMenus[trayID] = newTrayMenu - m.trayMenuPointers[trayMenu] = trayID - - return newTrayMenu.AsJSON() -} - -func (m *Manager) GetTrayID(trayMenu *menu.TrayMenu) (string, error) { - trayID, exists := m.trayMenuPointers[trayMenu] - if !exists { - return "", fmt.Errorf("Unable to find menu ID for tray menu!") - } - return trayID, nil -} - -// SetTrayMenu updates or creates a menu -func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) { - trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] - if !trayMenuKnown { - return m.AddTrayMenu(trayMenu) - } - - // Create the updated tray menu - updatedTrayMenu := NewTrayMenu(trayMenu) - updatedTrayMenu.ID = trayID - - // Save the reference - m.trayMenus[trayID] = updatedTrayMenu - - return updatedTrayMenu.AsJSON() -} - -func (m *Manager) GetTrayMenus() ([]string, error) { - result := []string{} - for _, trayMenu := range m.trayMenus { - JSON, err := trayMenu.AsJSON() - if err != nil { - return nil, err - } - result = append(result, JSON) - } - - return result, nil -} - -func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) { - trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] - if !trayMenuKnown { - return "", fmt.Errorf("[UpdateTrayMenuLabel] unknown tray id for tray %s", trayMenu.Label) - } - - type LabelUpdate struct { - ID string - Label string `json:",omitempty"` - FontName string `json:",omitempty"` - FontSize int - RGBA string `json:",omitempty"` - Disabled bool - Tooltip string `json:",omitempty"` - Image string `json:",omitempty"` - MacTemplateImage bool - StyledLabel []*ansi.StyledText `json:",omitempty"` - } - - // Parse ANSI text - var styledLabel []*ansi.StyledText - tempLabel := trayMenu.Label - if strings.Contains(tempLabel, "\033[") { - parsedLabel, err := ansi.Parse(tempLabel) - if err == nil { - styledLabel = parsedLabel - } - } - - update := &LabelUpdate{ - ID: trayID, - Label: trayMenu.Label, - FontName: trayMenu.FontName, - FontSize: trayMenu.FontSize, - Disabled: trayMenu.Disabled, - Tooltip: trayMenu.Tooltip, - Image: trayMenu.Image, - MacTemplateImage: trayMenu.MacTemplateImage, - RGBA: trayMenu.RGBA, - StyledLabel: styledLabel, - } - - data, err := json.Marshal(update) - if err != nil { - return "", errors.Wrap(err, "[UpdateTrayMenuLabel] ") - } - - return string(data), nil - -} - -func (m *Manager) GetContextMenus() ([]string, error) { - result := []string{} - for _, contextMenu := range m.contextMenus { - JSON, err := contextMenu.AsJSON() - if err != nil { - return nil, err - } - result = append(result, JSON) - } - - return result, nil -} +// +//func (t *TrayMenu) AsJSON() (string, error) { +// data, err := json.Marshal(t) +// if err != nil { +// return "", err +// } +// return string(data), nil +//} +// +//func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu { +// +// // Parse ANSI text +// var styledLabel []*ansi.StyledText +// tempLabel := trayMenu.Label +// if strings.Contains(tempLabel, "\033[") { +// parsedLabel, err := ansi.Parse(tempLabel) +// if err == nil { +// styledLabel = parsedLabel +// } +// } +// +// result := &TrayMenu{ +// Label: trayMenu.Label, +// FontName: trayMenu.FontName, +// FontSize: trayMenu.FontSize, +// Disabled: trayMenu.Disabled, +// Tooltip: trayMenu.Tooltip, +// Image: trayMenu.Image, +// MacTemplateImage: trayMenu.MacTemplateImage, +// menu: trayMenu.Menu, +// RGBA: trayMenu.RGBA, +// menuItemMap: NewMenuItemMap(), +// trayMenu: trayMenu, +// StyledLabel: styledLabel, +// } +// +// result.menuItemMap.AddMenu(trayMenu.Menu) +// result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu) +// +// return result +//} +// +//func (m *Manager) OnTrayMenuOpen(id string) { +// trayMenu, ok := m.trayMenus[id] +// if !ok { +// return +// } +// if trayMenu.trayMenu.OnOpen == nil { +// return +// } +// go trayMenu.trayMenu.OnOpen() +//} +// +//func (m *Manager) OnTrayMenuClose(id string) { +// trayMenu, ok := m.trayMenus[id] +// if !ok { +// return +// } +// if trayMenu.trayMenu.OnClose == nil { +// return +// } +// go trayMenu.trayMenu.OnClose() +//} +// +//func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) { +// newTrayMenu := NewTrayMenu(trayMenu) +// +// // Hook up a new ID +// trayID := generateTrayID() +// newTrayMenu.ID = trayID +// +// // Save the references +// m.trayMenus[trayID] = newTrayMenu +// m.trayMenuPointers[trayMenu] = trayID +// +// return newTrayMenu.AsJSON() +//} +// +//func (m *Manager) GetTrayID(trayMenu *menu.TrayMenu) (string, error) { +// trayID, exists := m.trayMenuPointers[trayMenu] +// if !exists { +// return "", fmt.Errorf("Unable to find menu ID for tray menu!") +// } +// return trayID, nil +//} +// +//// SetTrayMenu updates or creates a menu +//func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) { +// trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] +// if !trayMenuKnown { +// return m.AddTrayMenu(trayMenu) +// } +// +// // Create the updated tray menu +// updatedTrayMenu := NewTrayMenu(trayMenu) +// updatedTrayMenu.ID = trayID +// +// // Save the reference +// m.trayMenus[trayID] = updatedTrayMenu +// +// return updatedTrayMenu.AsJSON() +//} +// +//func (m *Manager) GetTrayMenus() ([]string, error) { +// result := []string{} +// for _, trayMenu := range m.trayMenus { +// JSON, err := trayMenu.AsJSON() +// if err != nil { +// return nil, err +// } +// result = append(result, JSON) +// } +// +// return result, nil +//} +// +//func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) { +// trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] +// if !trayMenuKnown { +// return "", fmt.Errorf("[UpdateTrayMenuLabel] unknown tray id for tray %s", trayMenu.Label) +// } +// +// type LabelUpdate struct { +// ID string +// Label string `json:",omitempty"` +// FontName string `json:",omitempty"` +// FontSize int +// RGBA string `json:",omitempty"` +// Disabled bool +// Tooltip string `json:",omitempty"` +// Image []byte `json:",omitempty"` +// MacTemplateImage bool +// StyledLabel []*ansi.StyledText `json:",omitempty"` +// } +// +// // Parse ANSI text +// var styledLabel []*ansi.StyledText +// tempLabel := trayMenu.Label +// if strings.Contains(tempLabel, "\033[") { +// parsedLabel, err := ansi.Parse(tempLabel) +// if err == nil { +// styledLabel = parsedLabel +// } +// } +// +// update := &LabelUpdate{ +// ID: trayID, +// Label: trayMenu.Label, +// FontName: trayMenu.FontName, +// FontSize: trayMenu.FontSize, +// Disabled: trayMenu.Disabled, +// Tooltip: trayMenu.Tooltip, +// Image: trayMenu.Image, +// MacTemplateImage: trayMenu.MacTemplateImage, +// RGBA: trayMenu.RGBA, +// StyledLabel: styledLabel, +// } +// +// data, err := json.Marshal(update) +// if err != nil { +// return "", errors.Wrap(err, "[UpdateTrayMenuLabel] ") +// } +// +// return string(data), nil +// +//} +// +//func (m *Manager) GetContextMenus() ([]string, error) { +// result := []string{} +// for _, contextMenu := range m.contextMenus { +// JSON, err := contextMenu.AsJSON() +// if err != nil { +// return nil, err +// } +// result = append(result, JSON) +// } +// +// return result, nil +//} diff --git a/v2/internal/platform/menu/manager.go b/v2/internal/platform/menu/manager.go new file mode 100644 index 000000000..28050294f --- /dev/null +++ b/v2/internal/platform/menu/manager.go @@ -0,0 +1,145 @@ +package menu + +import ( + "github.com/wailsapp/wails/v2/pkg/menu" +) + +// MenuManager manages the menus for the application +var MenuManager = NewManager() + +type radioGroup []*menu.MenuItem + +// Click updates the radio group state based on the item clicked +func (g *radioGroup) Click(item *menu.MenuItem) { + for _, radioGroupItem := range *g { + if radioGroupItem != item { + radioGroupItem.Checked = false + } + } +} + +type processedMenu struct { + + // the menu we processed + menu *menu.Menu + + // updateMenuItemCallback is called when the menu item needs to be updated in the UI + updateMenuItemCallback func(*menu.MenuItem) + + // items is a map of all menu items in this menu + items map[*menu.MenuItem]struct{} + + // radioGroups tracks which radiogroup a menu item belongs to + radioGroups map[*menu.MenuItem][]*radioGroup +} + +func newProcessedMenu(topLevelMenu *menu.Menu, updateMenuItemCallback func(*menu.MenuItem)) *processedMenu { + result := &processedMenu{ + updateMenuItemCallback: updateMenuItemCallback, + menu: topLevelMenu, + items: make(map[*menu.MenuItem]struct{}), + radioGroups: make(map[*menu.MenuItem][]*radioGroup), + } + result.process(topLevelMenu.Items) + return result +} + +func (p *processedMenu) process(items []*menu.MenuItem) { + var currentRadioGroup radioGroup + for index, item := range items { + // Save the reference to the top level menu for this item + p.items[item] = struct{}{} + + // If this is a radio item, add it to the radio group + if item.Type == menu.RadioType { + currentRadioGroup = append(currentRadioGroup, item) + } + + // If this is not a radio item, or we are processing the last item in the menu, + // then we need to add the current radio group to the map if it has items + if item.Type != menu.RadioType || index == len(items)-1 { + if len(currentRadioGroup) > 0 { + p.addRadioGroup(currentRadioGroup) + currentRadioGroup = nil + } + } + + // Process the submenu + if item.SubMenu != nil { + p.process(item.SubMenu.Items) + } + } +} + +func (p *processedMenu) processClick(item *menu.MenuItem) { + // If this item is not in our menu, then we can't process it + if _, ok := p.items[item]; !ok { + return + } + + // If this is a radio item, then we need to update the radio group + if item.Type == menu.RadioType { + // Get the radio groups for this item + radioGroups := p.radioGroups[item] + // Iterate each radio group this item belongs to and set the checked state + // of all items apart from the one that was clicked to false + for _, thisRadioGroup := range radioGroups { + thisRadioGroup.Click(item) + for _, thisRadioGroupItem := range *thisRadioGroup { + p.updateMenuItemCallback(thisRadioGroupItem) + } + } + } + + if item.Type == menu.CheckboxType { + p.updateMenuItemCallback(item) + } + +} + +func (p *processedMenu) addRadioGroup(r radioGroup) { + for _, item := range r { + p.radioGroups[item] = append(p.radioGroups[item], &r) + } +} + +type Manager struct { + menus map[*menu.Menu]*processedMenu +} + +func NewManager() *Manager { + return &Manager{ + menus: make(map[*menu.Menu]*processedMenu), + } +} + +func (m *Manager) AddMenu(menu *menu.Menu, updateMenuItemCallback func(*menu.MenuItem)) { + m.menus[menu] = newProcessedMenu(menu, updateMenuItemCallback) +} + +func (m *Manager) ProcessClick(item *menu.MenuItem) { + + // if menuitem is a checkbox, then we need to toggle the state + if item.Type == menu.CheckboxType { + item.Checked = !item.Checked + } + + // Set the radio item to checked + if item.Type == menu.RadioType { + item.Checked = true + } + + for _, thisMenu := range m.menus { + thisMenu.processClick(item) + } + + if item.Click != nil { + item.Click(&menu.CallbackData{ + MenuItem: item, + }) + } +} + +func (m *Manager) RemoveMenu(data *menu.Menu) { + delete(m.menus, data) +} diff --git a/v2/internal/platform/menu/manager_test.go b/v2/internal/platform/menu/manager_test.go new file mode 100644 index 000000000..ed7f98881 --- /dev/null +++ b/v2/internal/platform/menu/manager_test.go @@ -0,0 +1,295 @@ +package menu_test + +import ( + "github.com/stretchr/testify/require" + platformMenu "github.com/wailsapp/wails/v2/internal/platform/menu" + "github.com/wailsapp/wails/v2/pkg/menu" + "testing" +) + +func TestManager_ProcessClick_Checkbox(t *testing.T) { + + checkbox := menu.Label("Checkbox").SetChecked(false) + menu1 := &menu.Menu{ + Items: []*menu.MenuItem{ + checkbox, + }, + } + menu2 := &menu.Menu{ + Items: []*menu.MenuItem{ + checkbox, + }, + } + menuWithNoCheckbox := &menu.Menu{ + Items: []*menu.MenuItem{ + menu.Label("No Checkbox"), + }, + } + clicked := false + + tests := []struct { + name string + inputs []*menu.Menu + startState bool + expectedState bool + expectedMenuUpdates map[*menu.Menu][]*menu.MenuItem + click func(*menu.CallbackData) + }{ + { + name: "should callback menu checkbox state when clicked (false -> true)", + inputs: []*menu.Menu{menu1}, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + }, + startState: false, + expectedState: true, + }, + { + name: "should callback multiple menus when checkbox state when clicked (false -> true)", + inputs: []*menu.Menu{menu1, menu2}, + startState: false, + expectedState: true, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + menu2: {checkbox}, + }, + }, + { + name: "should callback only for the menus that the checkbox is in (false -> true)", + inputs: []*menu.Menu{menu1, menuWithNoCheckbox}, + startState: false, + expectedState: true, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + }, + }, + { + name: "should callback menu checkbox state when clicked (true->false)", + inputs: []*menu.Menu{menu1}, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + }, + startState: true, + expectedState: false, + }, + { + name: "should callback multiple menus when checkbox state when clicked (true->false)", + inputs: []*menu.Menu{menu1, menu2}, + startState: true, + expectedState: false, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + menu2: {checkbox}, + }, + }, + { + name: "should callback only for the menus that the checkbox is in (true->false)", + inputs: []*menu.Menu{menu1, menuWithNoCheckbox}, + startState: true, + expectedState: false, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + }, + }, + { + name: "should callback no menus if checkbox not in them", + inputs: []*menu.Menu{menuWithNoCheckbox}, + startState: false, + expectedState: false, + expectedMenuUpdates: nil, + }, + { + name: "should call Click on the checkbox", + inputs: []*menu.Menu{menu1, menu2}, + startState: false, + expectedState: true, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + menu2: {checkbox}, + }, + click: func(data *menu.CallbackData) { + clicked = true + }, + }, + } + for _, tt := range tests { + + menusUpdated := map[*menu.Menu][]*menu.MenuItem{} + clicked = false + + var checkMenuItemStateInMenu func(menu *menu.Menu) + + checkMenuItemStateInMenu = func(menu *menu.Menu) { + for _, item := range menusUpdated[menu] { + if item == checkbox { + require.Equal(t, tt.expectedState, item.Checked) + } + if item.SubMenu != nil { + checkMenuItemStateInMenu(item.SubMenu) + } + } + } + + t.Run(tt.name, func(t *testing.T) { + m := platformMenu.NewManager() + checkbox.SetChecked(tt.startState) + checkbox.Click = tt.click + for _, thisMenu := range tt.inputs { + thisMenu := thisMenu + m.AddMenu(thisMenu, func(menuItem *menu.MenuItem) { + menusUpdated[thisMenu] = append(menusUpdated[thisMenu], menuItem) + }) + } + m.ProcessClick(checkbox) + + // Check the item has the correct state in all the menus + for thisMenu := range menusUpdated { + require.EqualValues(t, tt.expectedMenuUpdates[thisMenu], menusUpdated[thisMenu]) + } + + if tt.click != nil { + require.Equal(t, true, clicked) + } + }) + } +} + +func TestManager_ProcessClick_RadioGroups(t *testing.T) { + + radio1 := menu.Radio("Radio1", false, nil, nil) + radio2 := menu.Radio("Radio2", false, nil, nil) + radio3 := menu.Radio("Radio3", false, nil, nil) + radio4 := menu.Radio("Radio4", false, nil, nil) + radio5 := menu.Radio("Radio5", false, nil, nil) + radio6 := menu.Radio("Radio6", false, nil, nil) + + radioGroupOne := &menu.Menu{ + Items: []*menu.MenuItem{ + radio1, + radio2, + radio3, + }, + } + + radioGroupTwo := &menu.Menu{ + Items: []*menu.MenuItem{ + radio4, + radio5, + radio6, + }, + } + + radioGroupThree := &menu.Menu{ + Items: []*menu.MenuItem{ + radio1, + radio2, + radio3, + }, + } + + clicked := false + + tests := []struct { + name string + inputs []*menu.Menu + startState map[*menu.MenuItem]bool + selected *menu.MenuItem + expectedMenuUpdates map[*menu.Menu][]*menu.MenuItem + click func(*menu.CallbackData) + expectedState map[*menu.MenuItem]bool + }{ + { + name: "should only set the clicked radio item", + inputs: []*menu.Menu{radioGroupOne}, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + radioGroupOne: {radio1, radio2, radio3}, + }, + startState: map[*menu.MenuItem]bool{ + radio1: true, + radio2: false, + radio3: false, + }, + selected: radio2, + expectedState: map[*menu.MenuItem]bool{ + radio1: false, + radio2: true, + radio3: false, + }, + }, + { + name: "should not affect other radio groups or menus", + inputs: []*menu.Menu{radioGroupOne, radioGroupTwo}, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + radioGroupOne: {radio1, radio2, radio3}, + }, + startState: map[*menu.MenuItem]bool{ + radio1: true, + radio2: false, + radio3: false, + radio4: true, + radio5: false, + radio6: false, + }, + selected: radio2, + expectedState: map[*menu.MenuItem]bool{ + radio1: false, + radio2: true, + radio3: false, + radio4: true, + radio5: false, + radio6: false, + }, + }, + { + name: "menus with the same radio group should be updated", + inputs: []*menu.Menu{radioGroupOne, radioGroupThree}, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + radioGroupOne: {radio1, radio2, radio3}, + radioGroupThree: {radio1, radio2, radio3}, + }, + startState: map[*menu.MenuItem]bool{ + radio1: true, + radio2: false, + radio3: false, + }, + selected: radio2, + expectedState: map[*menu.MenuItem]bool{ + radio1: false, + radio2: true, + radio3: false, + }, + }, + } + for _, tt := range tests { + + menusUpdated := map[*menu.Menu][]*menu.MenuItem{} + clicked = false + + t.Run(tt.name, func(t *testing.T) { + m := platformMenu.NewManager() + + for item, value := range tt.startState { + item.SetChecked(value) + } + + tt.selected.Click = tt.click + for _, thisMenu := range tt.inputs { + thisMenu := thisMenu + m.AddMenu(thisMenu, func(menuItem *menu.MenuItem) { + menusUpdated[thisMenu] = append(menusUpdated[thisMenu], menuItem) + }) + } + m.ProcessClick(tt.selected) + require.Equal(t, tt.expectedMenuUpdates, menusUpdated) + + // Check the items have the correct state in all the menus + for item, expectedValue := range tt.expectedState { + require.Equal(t, expectedValue, item.Checked) + } + + if tt.click != nil { + require.Equal(t, true, clicked) + } + }) + } +} diff --git a/v2/internal/platform/menu/windows.go b/v2/internal/platform/menu/windows.go new file mode 100644 index 000000000..68ebbcb49 --- /dev/null +++ b/v2/internal/platform/menu/windows.go @@ -0,0 +1,9 @@ +//go:build windows + +package menu + +import "github.com/wailsapp/wails/v2/internal/platform/win32" + +type Menu struct { + menu win32.HMENU +} diff --git a/v2/internal/platform/systray.go b/v2/internal/platform/systray.go new file mode 100644 index 000000000..af79f427f --- /dev/null +++ b/v2/internal/platform/systray.go @@ -0,0 +1,31 @@ +package platform + +import ( + "github.com/wailsapp/wails/v2/internal/platform/systray" + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/options" +) +import "github.com/samber/lo" + +type SysTray interface { + // SetTitle sets the title of the tray menu + SetTitle(title string) + SetTooltip(tooltip string) error + Show() error + Hide() error + Run() error + Close() + SetMenu(menu *menu.Menu) error + SetIcons(lightModeIcon, darkModeIcon *options.SystemTrayIcon) error + Update() error + OnLeftClick(func()) + OnRightClick(func()) + OnLeftDoubleClick(func()) + OnRightDoubleClick(func()) + OnMenuClose(func()) + OnMenuOpen(func()) +} + +func NewSysTray() SysTray { + return lo.Must(systray.New()) +} diff --git a/v2/internal/platform/systray/menu_windows.go b/v2/internal/platform/systray/menu_windows.go new file mode 100644 index 000000000..b191a41c2 --- /dev/null +++ b/v2/internal/platform/systray/menu_windows.go @@ -0,0 +1,222 @@ +//go:build windows + +package systray + +import ( + "errors" + "fmt" + platformMenu "github.com/wailsapp/wails/v2/internal/platform/menu" + "github.com/wailsapp/wails/v2/internal/platform/win32" + "github.com/wailsapp/wails/v2/pkg/menu" +) + +type RadioGroupMember struct { + ID int + MenuItem *menu.MenuItem +} + +type RadioGroup []*RadioGroupMember + +func (r *RadioGroup) Add(id int, item *menu.MenuItem) { + *r = append(*r, &RadioGroupMember{ + ID: id, + MenuItem: item, + }) +} + +func (r *RadioGroup) Bounds() (int, int) { + p := *r + return p[0].ID, p[len(p)-1].ID +} + +func (r *RadioGroup) MenuID(item *menu.MenuItem) int { + for _, member := range *r { + if member.MenuItem == item { + return member.ID + } + } + panic("RadioGroup.MenuID: item not found:") +} + +type PopupMenu struct { + menu win32.PopupMenu + parent win32.HWND + menuMapping map[int]*menu.MenuItem + checkboxItems map[*menu.MenuItem][]int + radioGroups map[*menu.MenuItem][]*RadioGroup + menuData *menu.Menu + currentMenuID int + onMenuClose func() + onMenuOpen func() +} + +func (p *PopupMenu) buildMenu(parentMenu win32.PopupMenu, inputMenu *menu.Menu) error { + var currentRadioGroup RadioGroup + for _, item := range inputMenu.Items { + if item.Hidden { + continue + } + var ret bool + p.currentMenuID++ + itemID := p.currentMenuID + p.menuMapping[itemID] = item + + flags := win32.MF_STRING + if item.Disabled { + flags = flags | win32.MF_GRAYED + } + if item.Checked { + flags = flags | win32.MF_CHECKED + } + //if item.BarBreak { + // flags = flags | win32.MF_MENUBARBREAK + //} + if item.IsSeparator() { + flags = flags | win32.MF_SEPARATOR + } + + if item.IsCheckbox() { + p.checkboxItems[item] = append(p.checkboxItems[item], itemID) + } + if item.IsRadio() { + currentRadioGroup.Add(itemID, item) + } else { + if len(currentRadioGroup) > 0 { + for _, radioMember := range currentRadioGroup { + currentRadioGroup := currentRadioGroup + p.radioGroups[radioMember.MenuItem] = append(p.radioGroups[radioMember.MenuItem], ¤tRadioGroup) + } + currentRadioGroup = RadioGroup{} + } + } + + if item.SubMenu != nil { + flags = flags | win32.MF_POPUP + submenu := win32.CreatePopupMenu() + err := p.buildMenu(submenu, item.SubMenu) + if err != nil { + return err + } + itemID = int(submenu) + } + + var menuText = item.Label + if item.Accelerator != nil { + shortcut := win32.AcceleratorToShortcut(item.Accelerator) + menuText = fmt.Sprintf("%s\t%s", menuText, shortcut) + // Popup Menus don't appear to support accelerators and I'm not + // sure they make sense either + } + + ret = parentMenu.Append(uintptr(flags), uintptr(itemID), menuText) + if ret == false { + return errors.New("AppendMenu failed") + } + } + if len(currentRadioGroup) > 0 { + for _, radioMember := range currentRadioGroup { + currentRadioGroup := currentRadioGroup + p.radioGroups[radioMember.MenuItem] = append(p.radioGroups[radioMember.MenuItem], ¤tRadioGroup) + } + currentRadioGroup = RadioGroup{} + } + return nil +} + +func (p *PopupMenu) Update() error { + p.menu = win32.CreatePopupMenu() + p.menuMapping = make(map[int]*menu.MenuItem) + p.currentMenuID = win32.MenuItemMsgID + err := p.buildMenu(p.menu, p.menuData) + if err != nil { + return err + } + p.updateRadioGroups() + return nil +} + +func NewPopupMenu(parent win32.HWND, inputMenu *menu.Menu) (*PopupMenu, error) { + result := &PopupMenu{ + parent: parent, + menuData: inputMenu, + checkboxItems: make(map[*menu.MenuItem][]int), + radioGroups: make(map[*menu.MenuItem][]*RadioGroup), + } + err := result.Update() + platformMenu.MenuManager.AddMenu(inputMenu, result.UpdateMenuItem) + return result, err +} + +func (p *PopupMenu) ShowAtCursor() error { + x, y, ok := win32.GetCursorPos() + if ok == false { + return errors.New("GetCursorPos failed") + } + + if win32.SetForegroundWindow(p.parent) == false { + return errors.New("SetForegroundWindow failed") + } + + if p.onMenuOpen != nil { + p.onMenuOpen() + } + + if p.menu.Track(win32.TPM_LEFTALIGN, x, y-5, p.parent) == false { + return errors.New("TrackPopupMenu failed") + } + + if p.onMenuClose != nil { + p.onMenuClose() + } + + if win32.PostMessage(p.parent, win32.WM_NULL, 0, 0) == 0 { + return errors.New("PostMessage failed") + } + + return nil +} + +func (p *PopupMenu) ProcessCommand(cmdMsgID int) { + item := p.menuMapping[cmdMsgID] + platformMenu.MenuManager.ProcessClick(item) +} + +func (p *PopupMenu) Destroy() { + p.menu.Destroy() +} + +func (p *PopupMenu) UpdateMenuItem(item *menu.MenuItem) { + if item.IsCheckbox() { + for _, itemID := range p.checkboxItems[item] { + p.menu.Check(uintptr(itemID), item.Checked) + } + return + } + if item.IsRadio() && item.Checked == true { + p.updateRadioGroup(item) + } +} + +func (p *PopupMenu) updateRadioGroups() { + for menuItem := range p.radioGroups { + if menuItem.Checked { + p.updateRadioGroup(menuItem) + } + } +} + +func (p *PopupMenu) updateRadioGroup(item *menu.MenuItem) { + for _, radioGroup := range p.radioGroups[item] { + thisMenuID := radioGroup.MenuID(item) + startID, endID := radioGroup.Bounds() + p.menu.CheckRadio(startID, endID, thisMenuID) + } +} + +func (p *PopupMenu) OnMenuOpen(fn func()) { + p.onMenuOpen = fn +} + +func (p *PopupMenu) OnMenuClose(fn func()) { + p.onMenuClose = fn +} diff --git a/v2/internal/platform/systray/systray_windows.go b/v2/internal/platform/systray/systray_windows.go new file mode 100644 index 000000000..8e7b7dfe6 --- /dev/null +++ b/v2/internal/platform/systray/systray_windows.go @@ -0,0 +1,432 @@ +//go:build windows + +/* + * Based on code originally from https://github.com/tadvi/systray. Copyright (C) 2019 The Systray Authors. All Rights Reserved. + */ + +package systray + +import ( + "errors" + "github.com/samber/lo" + "github.com/wailsapp/wails/v2/internal/platform/win32" + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/options" + "syscall" + "unsafe" +) + +var ( + user32 = syscall.MustLoadDLL("user32.dll") + + DefWindowProc = user32.MustFindProc("DefWindowProcW") + RegisterClassEx = user32.MustFindProc("RegisterClassExW") + CreateWindowEx = user32.MustFindProc("CreateWindowExW") + + windowClasses = map[string]win32.HINSTANCE{} +) + +type Systray struct { + id uint32 + mhwnd win32.HWND // main window handle + hwnd win32.HWND + hinst win32.HINSTANCE + lclick func() + rclick func() + ldblclick func() + rdblclick func() + onMenuClose func() + onMenuOpen func() + + appIcon win32.HICON + lightModeIcon win32.HICON + darkModeIcon win32.HICON + currentIcon win32.HICON + + menu *PopupMenu + + quit chan struct{} + icon *options.SystemTrayIcon +} + +func (p *Systray) Close() { + err := p.Stop() + if err != nil { + println(err.Error()) + } +} + +func (p *Systray) Update() error { + // Delete old menu + if p.menu != nil { + p.menu.Destroy() + } + + return p.menu.Update() +} + +// SetTitle is unused on Windows +func (p *Systray) SetTitle(_ string) {} + +func New() (*Systray, error) { + ni := &Systray{} + + ni.lclick = func() { + if ni.menu != nil { + _ = ni.menu.ShowAtCursor() + } + } + ni.rclick = func() { + if ni.menu != nil { + _ = ni.menu.ShowAtCursor() + } + } + + MainClassName := "WailsSystray" + ni.hinst, _ = RegisterWindow(MainClassName, ni.WinProc) + + ni.mhwnd = win32.CreateWindowEx( + win32.WS_EX_CONTROLPARENT, + win32.MustStringToUTF16Ptr(MainClassName), + win32.MustStringToUTF16Ptr(""), + win32.WS_OVERLAPPEDWINDOW|win32.WS_CLIPSIBLINGS, + win32.CW_USEDEFAULT, + win32.CW_USEDEFAULT, + win32.CW_USEDEFAULT, + win32.CW_USEDEFAULT, + 0, + 0, + 0, + unsafe.Pointer(nil)) + + if ni.mhwnd == 0 { + return nil, errors.New("create main win failed") + } + + NotifyIconClassName := "NotifyIconForm" + _, err := RegisterWindow(NotifyIconClassName, ni.WinProc) + if err != nil { + return nil, err + } + + hwnd, _, _ := CreateWindowEx.Call( + 0, + uintptr(unsafe.Pointer(win32.MustStringToUTF16Ptr(NotifyIconClassName))), + 0, + 0, + 0, + 0, + 0, + 0, + uintptr(win32.HWND_MESSAGE), + 0, + 0, + 0) + if hwnd == 0 { + return nil, errors.New("create notify win failed") + } + + ni.hwnd = win32.HWND(hwnd) // Important to keep this inside struct. + + nid := win32.NOTIFYICONDATA{ + HWnd: win32.HWND(hwnd), + UFlags: win32.NIF_MESSAGE | win32.NIF_STATE, + DwState: win32.NIS_HIDDEN, + DwStateMask: win32.NIS_HIDDEN, + UCallbackMessage: win32.NotifyIconMessageId, + } + nid.CbSize = uint32(unsafe.Sizeof(nid)) + + if !win32.ShellNotifyIcon(win32.NIM_ADD, &nid) { + return nil, errors.New("shell notify create failed") + } + + nid.UVersion = win32.NOTIFYICON_VERSION + + if !win32.ShellNotifyIcon(win32.NIM_SETVERSION, &nid) { + return nil, errors.New("shell notify version failed") + } + + ni.appIcon = win32.LoadIconWithResourceID(0, uintptr(win32.IDI_APPLICATION)) + ni.lightModeIcon = ni.appIcon + ni.darkModeIcon = ni.appIcon + ni.id = nid.UID + return ni, nil +} + +func (p *Systray) HWND() win32.HWND { + return p.hwnd +} + +func (p *Systray) SetMenu(popupMenu *menu.Menu) (err error) { + p.menu, err = NewPopupMenu(p.hwnd, popupMenu) + p.menu.OnMenuClose(p.onMenuClose) + p.menu.OnMenuOpen(p.onMenuOpen) + return +} + +func (p *Systray) Stop() error { + nid := p.newNotifyIconData() + win32.PostQuitMessage(0) + if !win32.ShellNotifyIcon(win32.NIM_DELETE, &nid) { + return errors.New("shell notify delete failed") + } + return nil +} + +func (p *Systray) OnLeftClick(fn func()) { + if fn != nil { + p.lclick = fn + } +} + +func (p *Systray) OnRightClick(fn func()) { + if fn != nil { + p.rclick = fn + } +} + +func (p *Systray) OnLeftDoubleClick(fn func()) { + if fn != nil { + p.ldblclick = fn + } +} + +func (p *Systray) OnRightDoubleClick(fn func()) { + if fn != nil { + p.rdblclick = fn + } +} + +func (p *Systray) OnMenuClose(fn func()) { + if fn != nil { + p.onMenuClose = fn + } +} + +func (p *Systray) OnMenuOpen(fn func()) { + if fn != nil { + p.onMenuOpen = fn + } +} + +func (p *Systray) SetTooltip(tooltip string) error { + nid := p.newNotifyIconData() + nid.UFlags = win32.NIF_TIP + copy(nid.SzTip[:], win32.MustUTF16FromString(tooltip)) + + if !win32.ShellNotifyIcon(win32.NIM_MODIFY, &nid) { + return errors.New("shell notify tooltip failed") + } + return nil +} + +func (p *Systray) ShowMessage(title, msg string, bigIcon bool) error { + nid := p.newNotifyIconData() + if bigIcon == true { + nid.DwInfoFlags = win32.NIIF_USER + } + + nid.CbSize = uint32(unsafe.Sizeof(nid)) + + nid.UFlags = win32.NIF_INFO + copy(nid.SzInfoTitle[:], win32.MustUTF16FromString(title)) + copy(nid.SzInfo[:], win32.MustUTF16FromString(msg)) + + if !win32.ShellNotifyIcon(win32.NIM_MODIFY, &nid) { + return errors.New("shell notify tooltip failed") + } + return nil +} + +func (p *Systray) newNotifyIconData() win32.NOTIFYICONDATA { + nid := win32.NOTIFYICONDATA{ + UID: p.id, + HWnd: p.hwnd, + } + nid.CbSize = uint32(unsafe.Sizeof(nid)) + return nid +} + +func (p *Systray) Show() error { + return p.setVisible(true) +} + +func (p *Systray) Hide() error { + return p.setVisible(false) +} + +func (p *Systray) setVisible(visible bool) error { + nid := p.newNotifyIconData() + nid.UFlags = win32.NIF_STATE + nid.DwStateMask = win32.NIS_HIDDEN + if !visible { + nid.DwState = win32.NIS_HIDDEN + } + + if !win32.ShellNotifyIcon(win32.NIM_MODIFY, &nid) { + return errors.New("shell notify tooltip failed") + } + return nil +} + +func (p *Systray) SetIcons(lightModeIcon, darkModeIcon *options.SystemTrayIcon) error { + var newLightModeIcon, newDarkModeIcon win32.HICON + if lightModeIcon != nil && lightModeIcon.Data != nil { + newLightModeIcon = p.getIcon(lightModeIcon.Data) + } + if darkModeIcon != nil && darkModeIcon.Data != nil { + newDarkModeIcon = p.getIcon(darkModeIcon.Data) + } + p.lightModeIcon, _ = lo.Coalesce(newLightModeIcon, newDarkModeIcon, p.appIcon) + p.darkModeIcon, _ = lo.Coalesce(newDarkModeIcon, newLightModeIcon, p.appIcon) + return p.updateIcon() +} + +func (p *Systray) getIcon(icon []byte) win32.HICON { + result, err := win32.CreateHIconFromPNG(icon) + if err != nil { + result = p.appIcon + } + return result +} + +func (p *Systray) setIcon(hicon win32.HICON) error { + nid := p.newNotifyIconData() + nid.UFlags = win32.NIF_ICON + if hicon == 0 { + nid.HIcon = 0 + } else { + nid.HIcon = hicon + } + + if !win32.ShellNotifyIcon(win32.NIM_MODIFY, &nid) { + return errors.New("shell notify icon failed") + } + return nil +} + +func (p *Systray) WinProc(hwnd win32.HWND, msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case win32.NotifyIconMessageId: + switch lparam { + case win32.WM_LBUTTONUP: + if p.lclick != nil { + println("left click") + p.lclick() + } + case win32.WM_RBUTTONUP: + if p.rclick != nil { + println("right click") + p.rclick() + } + case win32.WM_LBUTTONDBLCLK: + if p.ldblclick != nil { + p.ldblclick() + } + case win32.WM_RBUTTONDBLCLK: + if p.rdblclick != nil { + p.rdblclick() + } + default: + //println(win32.WMMessageToString(lparam)) + } + case win32.WM_SETTINGCHANGE: + settingChanged := win32.UTF16PtrToString(lparam) + if settingChanged == "ImmersiveColorSet" { + err := p.updateIcon() + if err != nil { + println("update icon failed", err.Error()) + } + } + return 0 + case win32.WM_COMMAND: + cmdMsgID := int(wparam & 0xffff) + switch cmdMsgID { + default: + p.menu.ProcessCommand(cmdMsgID) + } + default: + //msg := int(wparam & 0xffff) + //println(win32.WMMessageToString(uintptr(msg))) + } + + result, _, _ := DefWindowProc.Call(uintptr(hwnd), uintptr(msg), wparam, lparam) + return result +} + +func (p *Systray) Run() error { + var msg win32.MSG + for { + rt := win32.GetMessage(&msg) + switch int(rt) { + case 0: + return nil + case -1: + return errors.New("run failed") + } + + if win32.IsDialogMessage(p.hwnd, &msg) == 0 { + win32.TranslateMessage(&msg) + win32.DispatchMessage(&msg) + } + } +} + +func (p *Systray) updateIcon() error { + + var newIcon win32.HICON + if win32.IsCurrentlyDarkMode() { + newIcon = p.darkModeIcon + } else { + newIcon = p.lightModeIcon + } + if p.currentIcon == newIcon { + return nil + } + p.currentIcon = newIcon + return p.setIcon(newIcon) +} + +func (p *Systray) updateTheme() { + //win32.SetTheme(p.hwnd, win32.IsCurrentlyDarkMode()) +} + +func RegisterWindow(name string, proc win32.WindowProc) (win32.HINSTANCE, error) { + instance, exists := windowClasses[name] + if exists { + return instance, nil + } + hinst := win32.GetModuleHandle(0) + if hinst == 0 { + return 0, errors.New("get module handle failed") + } + hicon := win32.LoadIconWithResourceID(0, uintptr(win32.IDI_APPLICATION)) + if hicon == 0 { + return 0, errors.New("load icon failed") + } + hcursor := win32.LoadCursorWithResourceID(0, uintptr(win32.IDC_ARROW)) + if hcursor == 0 { + return 0, errors.New("load cursor failed") + } + + hi := win32.HINSTANCE(hinst) + + var wc win32.WNDCLASSEX + wc.CbSize = uint32(unsafe.Sizeof(wc)) + wc.LpfnWndProc = syscall.NewCallback(proc) + wc.HInstance = win32.HINSTANCE(hinst) + wc.HIcon = hicon + wc.HCursor = hcursor + wc.HbrBackground = win32.COLOR_BTNFACE + 1 + wc.LpszClassName = win32.MustStringToUTF16Ptr(name) + + atom, _, e := RegisterClassEx.Call(uintptr(unsafe.Pointer(&wc))) + if atom == 0 { + println(e.Error()) + return 0, errors.New("register class failed") + } + + windowClasses[name] = hi + return hi, nil +} diff --git a/v2/internal/platform/win32/consts.go b/v2/internal/platform/win32/consts.go new file mode 100644 index 000000000..295a9c8db --- /dev/null +++ b/v2/internal/platform/win32/consts.go @@ -0,0 +1,856 @@ +package win32 + +import ( + "fmt" + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" + "golang.org/x/sys/windows" + "syscall" + "unsafe" +) + +var ( + modKernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetModuleHandle = modKernel32.NewProc("GetModuleHandleW") + + moduser32 = syscall.NewLazyDLL("user32.dll") + procRegisterClassEx = moduser32.NewProc("RegisterClassExW") + procLoadIcon = moduser32.NewProc("LoadIconW") + procLoadCursor = moduser32.NewProc("LoadCursorW") + procCreateWindowEx = moduser32.NewProc("CreateWindowExW") + procPostMessage = moduser32.NewProc("PostMessageW") + procGetCursorPos = moduser32.NewProc("GetCursorPos") + procSetForegroundWindow = moduser32.NewProc("SetForegroundWindow") + procCreatePopupMenu = moduser32.NewProc("CreatePopupMenu") + procTrackPopupMenu = moduser32.NewProc("TrackPopupMenu") + procDestroyMenu = moduser32.NewProc("DestroyMenu") + procAppendMenuW = moduser32.NewProc("AppendMenuW") + procCheckMenuItem = moduser32.NewProc("CheckMenuItem") + procCheckMenuRadioItem = moduser32.NewProc("CheckMenuRadioItem") + procCreateIconFromResourceEx = moduser32.NewProc("CreateIconFromResourceEx") + procGetMessageW = moduser32.NewProc("GetMessageW") + procIsDialogMessage = moduser32.NewProc("IsDialogMessageW") + procTranslateMessage = moduser32.NewProc("TranslateMessage") + procDispatchMessage = moduser32.NewProc("DispatchMessageW") + procPostQuitMessage = moduser32.NewProc("PostQuitMessage") + procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") + procSetWindowCompositionAttribute = moduser32.NewProc("SetWindowCompositionAttribute") + procGetKeyState = moduser32.NewProc("GetKeyState") + procCreateAcceleratorTable = moduser32.NewProc("CreateAcceleratorTableW") + procTranslateAccelerator = moduser32.NewProc("TranslateAcceleratorW") + + modshell32 = syscall.NewLazyDLL("shell32.dll") + procShellNotifyIcon = modshell32.NewProc("Shell_NotifyIconW") + + moddwmapi = syscall.NewLazyDLL("dwmapi.dll") + procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute") + + moduxtheme = syscall.NewLazyDLL("uxtheme.dll") + procSetWindowTheme = moduxtheme.NewProc("SetWindowTheme") + + AllowDarkModeForWindow func(HWND, bool) uintptr + SetPreferredAppMode func(int32) uintptr +) + +type PreferredAppMode = int32 + +const ( + PreferredAppModeDefault PreferredAppMode = iota + PreferredAppModeAllowDark + PreferredAppModeForceDark + PreferredAppModeForceLight + PreferredAppModeMax +) + +/* +RtlGetNtVersionNumbers = void (LPDWORD major, LPDWORD minor, LPDWORD build) // 1809 17763 +ShouldAppsUseDarkMode = bool () // ordinal 132 +AllowDarkModeForWindow = bool (HWND hWnd, bool allow) // ordinal 133 +AllowDarkModeForApp = bool (bool allow) // ordinal 135, removed since 18334 +FlushMenuThemes = void () // ordinal 136 +RefreshImmersiveColorPolicyState = void () // ordinal 104 +IsDarkModeAllowedForWindow = bool (HWND hWnd) // ordinal 137 +GetIsImmersiveColorUsingHighContrast = bool (IMMERSIVE_HC_CACHE_MODE mode) // ordinal 106 +OpenNcThemeData = HTHEME (HWND hWnd, LPCWSTR pszClassList) // ordinal 49 +// Insider 18290 +ShouldSystemUseDarkMode = bool () // ordinal 138 +// Insider 18334 +SetPreferredAppMode = PreferredAppMode (PreferredAppMode appMode) // ordinal 135, since 18334 +IsDarkModeAllowedForApp = bool () // ordinal 139 +*/ +func init() { + if IsWindowsVersionAtLeast(10, 0, 18334) { + + // AllowDarkModeForWindow is only available on Windows 10+ + uxtheme, err := windows.LoadLibrary("uxtheme.dll") + if err == nil { + procAllowDarkModeForWindow, err := windows.GetProcAddressByOrdinal(uxtheme, uintptr(133)) + if err == nil { + AllowDarkModeForWindow = func(hwnd HWND, allow bool) uintptr { + var allowInt int32 + if allow { + allowInt = 1 + } + ret, _, _ := syscall.SyscallN(procAllowDarkModeForWindow, uintptr(hwnd), uintptr(allowInt)) + return ret + } + } + } + + // SetPreferredAppMode is only available on Windows 10+ + procSetPreferredAppMode, err := windows.GetProcAddressByOrdinal(uxtheme, uintptr(135)) + if err == nil { + SetPreferredAppMode = func(mode int32) uintptr { + ret, _, _ := syscall.SyscallN(procSetPreferredAppMode, uintptr(mode)) + return ret + } + SetPreferredAppMode(PreferredAppModeAllowDark) + } + } + +} + +type HANDLE uintptr +type HINSTANCE = HANDLE +type HICON = HANDLE +type HCURSOR = HANDLE +type HBRUSH = HANDLE +type HWND = HANDLE +type HMENU = HANDLE +type DWORD = uint32 +type ATOM uint16 +type MenuID uint16 + +const ( + WM_APP = 32768 + WM_ACTIVATE = 6 + WM_ACTIVATEAPP = 28 + WM_AFXFIRST = 864 + WM_AFXLAST = 895 + WM_ASKCBFORMATNAME = 780 + WM_CANCELJOURNAL = 75 + WM_CANCELMODE = 31 + WM_CAPTURECHANGED = 533 + WM_CHANGECBCHAIN = 781 + WM_CHAR = 258 + WM_CHARTOITEM = 47 + WM_CHILDACTIVATE = 34 + WM_CLEAR = 771 + WM_CLOSE = 16 + WM_COMMAND = 273 + WM_COMMNOTIFY = 68 /* OBSOLETE */ + WM_COMPACTING = 65 + WM_COMPAREITEM = 57 + WM_CONTEXTMENU = 123 + WM_COPY = 769 + WM_COPYDATA = 74 + WM_CREATE = 1 + WM_CTLCOLORBTN = 309 + WM_CTLCOLORDLG = 310 + WM_CTLCOLOREDIT = 307 + WM_CTLCOLORLISTBOX = 308 + WM_CTLCOLORMSGBOX = 306 + WM_CTLCOLORSCROLLBAR = 311 + WM_CTLCOLORSTATIC = 312 + WM_CUT = 768 + WM_DEADCHAR = 259 + WM_DELETEITEM = 45 + WM_DESTROY = 2 + WM_DESTROYCLIPBOARD = 775 + WM_DEVICECHANGE = 537 + WM_DEVMODECHANGE = 27 + WM_DISPLAYCHANGE = 126 + WM_DRAWCLIPBOARD = 776 + WM_DRAWITEM = 43 + WM_DROPFILES = 563 + WM_ENABLE = 10 + WM_ENDSESSION = 22 + WM_ENTERIDLE = 289 + WM_ENTERMENULOOP = 529 + WM_ENTERSIZEMOVE = 561 + WM_ERASEBKGND = 20 + WM_EXITMENULOOP = 530 + WM_EXITSIZEMOVE = 562 + WM_FONTCHANGE = 29 + WM_GETDLGCODE = 135 + WM_GETFONT = 49 + WM_GETHOTKEY = 51 + WM_GETICON = 127 + WM_GETMINMAXINFO = 36 + WM_GETTEXT = 13 + WM_GETTEXTLENGTH = 14 + WM_HANDHELDFIRST = 856 + WM_HANDHELDLAST = 863 + WM_HELP = 83 + WM_HOTKEY = 786 + WM_HSCROLL = 276 + WM_HSCROLLCLIPBOARD = 782 + WM_ICONERASEBKGND = 39 + WM_INITDIALOG = 272 + WM_INITMENU = 278 + WM_INITMENUPOPUP = 279 + WM_INPUT = 0x00FF + WM_INPUTLANGCHANGE = 81 + WM_INPUTLANGCHANGEREQUEST = 80 + WM_KEYDOWN = 256 + WM_KEYUP = 257 + WM_KILLFOCUS = 8 + WM_MDIACTIVATE = 546 + WM_MDICASCADE = 551 + WM_MDICREATE = 544 + WM_MDIDESTROY = 545 + WM_MDIGETACTIVE = 553 + WM_MDIICONARRANGE = 552 + WM_MDIMAXIMIZE = 549 + WM_MDINEXT = 548 + WM_MDIREFRESHMENU = 564 + WM_MDIRESTORE = 547 + WM_MDISETMENU = 560 + WM_MDITILE = 550 + WM_MEASUREITEM = 44 + WM_GETOBJECT = 0x003D + WM_CHANGEUISTATE = 0x0127 + WM_UPDATEUISTATE = 0x0128 + WM_QUERYUISTATE = 0x0129 + WM_UNINITMENUPOPUP = 0x0125 + WM_MENURBUTTONUP = 290 + WM_MENUCOMMAND = 0x0126 + WM_MENUGETOBJECT = 0x0124 + WM_MENUDRAG = 0x0123 + WM_APPCOMMAND = 0x0319 + WM_MENUCHAR = 288 + WM_MENUSELECT = 287 + WM_MOVE = 3 + WM_MOVING = 534 + WM_NCACTIVATE = 134 + WM_NCCALCSIZE = 131 + WM_NCCREATE = 129 + WM_NCDESTROY = 130 + WM_NCHITTEST = 132 + WM_NCLBUTTONDBLCLK = 163 + WM_NCLBUTTONDOWN = 161 + WM_NCLBUTTONUP = 162 + WM_NCMBUTTONDBLCLK = 169 + WM_NCMBUTTONDOWN = 167 + WM_NCMBUTTONUP = 168 + WM_NCXBUTTONDOWN = 171 + WM_NCXBUTTONUP = 172 + WM_NCXBUTTONDBLCLK = 173 + WM_NCMOUSEHOVER = 0x02A0 + WM_NCMOUSELEAVE = 0x02A2 + WM_NCMOUSEMOVE = 160 + WM_NCPAINT = 133 + WM_NCRBUTTONDBLCLK = 166 + WM_NCRBUTTONDOWN = 164 + WM_NCRBUTTONUP = 165 + WM_NEXTDLGCTL = 40 + WM_NEXTMENU = 531 + WM_NOTIFY = 78 + WM_NOTIFYFORMAT = 85 + WM_NULL = 0 + WM_PAINT = 15 + WM_PAINTCLIPBOARD = 777 + WM_PAINTICON = 38 + WM_PALETTECHANGED = 785 + WM_PALETTEISCHANGING = 784 + WM_PARENTNOTIFY = 528 + WM_PASTE = 770 + WM_PENWINFIRST = 896 + WM_PENWINLAST = 911 + WM_POWER = 72 + WM_PRINT = 791 + WM_PRINTCLIENT = 792 + WM_QUERYDRAGICON = 55 + WM_QUERYENDSESSION = 17 + WM_QUERYNEWPALETTE = 783 + WM_QUERYOPEN = 19 + WM_QUEUESYNC = 35 + WM_QUIT = 18 + WM_RENDERALLFORMATS = 774 + WM_RENDERFORMAT = 773 + WM_SETCURSOR = 32 + WM_SETFOCUS = 7 + WM_SETFONT = 48 + WM_SETHOTKEY = 50 + WM_SETICON = 128 + WM_SETREDRAW = 11 + WM_SETTEXT = 12 + WM_SETTINGCHANGE = 26 + WM_SHOWWINDOW = 24 + WM_SIZE = 5 + WM_SIZECLIPBOARD = 779 + WM_SIZING = 532 + WM_SPOOLERSTATUS = 42 + WM_STYLECHANGED = 125 + WM_STYLECHANGING = 124 + WM_SYSCHAR = 262 + WM_SYSCOLORCHANGE = 21 + WM_SYSCOMMAND = 274 + WM_SYSDEADCHAR = 263 + WM_SYSKEYDOWN = 260 + WM_SYSKEYUP = 261 + WM_TCARD = 82 + WM_THEMECHANGED = 794 + WM_TIMECHANGE = 30 + WM_TIMER = 275 + WM_UNDO = 772 + WM_USER = 1024 + WM_USERCHANGED = 84 + WM_VKEYTOITEM = 46 + WM_VSCROLL = 277 + WM_VSCROLLCLIPBOARD = 778 + WM_WINDOWPOSCHANGED = 71 + WM_WINDOWPOSCHANGING = 70 + WM_WININICHANGE = 26 + WM_KEYFIRST = 256 + WM_KEYLAST = 264 + WM_SYNCPAINT = 136 + WM_MOUSEACTIVATE = 33 + WM_MOUSEMOVE = 512 + WM_LBUTTONDOWN = 513 + WM_LBUTTONUP = 514 + WM_LBUTTONDBLCLK = 515 + WM_RBUTTONDOWN = 516 + WM_RBUTTONUP = 517 + WM_RBUTTONDBLCLK = 518 + WM_MBUTTONDOWN = 519 + WM_MBUTTONUP = 520 + WM_MBUTTONDBLCLK = 521 + WM_MOUSEWHEEL = 522 + WM_MOUSEFIRST = 512 + WM_XBUTTONDOWN = 523 + WM_XBUTTONUP = 524 + WM_XBUTTONDBLCLK = 525 + WM_MOUSELAST = 525 + WM_MOUSEHOVER = 0x2A1 + WM_MOUSELEAVE = 0x2A3 + WM_CLIPBOARDUPDATE = 0x031D + + WS_EX_APPWINDOW = 0x00040000 + WS_OVERLAPPEDWINDOW = 0x00000000 | 0x00C00000 | 0x00080000 | 0x00040000 | 0x00020000 | 0x00010000 + WS_EX_NOREDIRECTIONBITMAP = 0x00200000 + CW_USEDEFAULT = 0x80000000 + + NIM_ADD = 0x00000000 + NIM_MODIFY = 0x00000001 + NIM_DELETE = 0x00000002 + NIM_SETVERSION = 0x00000004 + + NIF_MESSAGE = 0x00000001 + NIF_ICON = 0x00000002 + NIF_TIP = 0x00000004 + NIF_STATE = 0x00000008 + NIF_INFO = 0x00000010 + + NIS_HIDDEN = 0x00000001 + + NIIF_NONE = 0x00000000 + NIIF_INFO = 0x00000001 + NIIF_WARNING = 0x00000002 + NIIF_ERROR = 0x00000003 + NIIF_USER = 0x00000004 + NIIF_NOSOUND = 0x00000010 + NIIF_LARGE_ICON = 0x00000020 + NIIF_RESPECT_QUIET_TIME = 0x00000080 + NIIF_ICON_MASK = 0x0000000F + + IMAGE_BITMAP = 0 + IMAGE_ICON = 1 + LR_LOADFROMFILE = 0x00000010 + LR_DEFAULTSIZE = 0x00000040 + + IDC_ARROW = 32512 + COLOR_WINDOW = 5 + COLOR_BTNFACE = 15 + + GWLP_USERDATA = -21 + WS_CLIPSIBLINGS = 0x04000000 + WS_EX_CONTROLPARENT = 0x00010000 + + HWND_MESSAGE = ^HWND(2) + NOTIFYICON_VERSION = 4 + + IDI_APPLICATION = 32512 + + MenuItemMsgID = WM_APP + 1024 + NotifyIconMessageId = WM_APP + iota + + MF_STRING = 0x00000000 + MF_ENABLED = 0x00000000 + MF_GRAYED = 0x00000001 + MF_DISABLED = 0x00000002 + MF_SEPARATOR = 0x00000800 + MF_UNCHECKED = 0x00000000 + MF_CHECKED = 0x00000008 + MF_POPUP = 0x00000010 + MF_MENUBARBREAK = 0x00000020 + MF_BYCOMMAND = 0x00000000 + + TPM_LEFTALIGN = 0x0000 + + CS_VREDRAW = 0x0001 + CS_HREDRAW = 0x0002 +) + +func WMMessageToString(msg uintptr) string { + // Convert windows message to string + switch msg { + case WM_APP: + return "WM_APP" + case WM_ACTIVATE: + return "WM_ACTIVATE" + case WM_ACTIVATEAPP: + return "WM_ACTIVATEAPP" + case WM_AFXFIRST: + return "WM_AFXFIRST" + case WM_AFXLAST: + return "WM_AFXLAST" + case WM_ASKCBFORMATNAME: + return "WM_ASKCBFORMATNAME" + case WM_CANCELJOURNAL: + return "WM_CANCELJOURNAL" + case WM_CANCELMODE: + return "WM_CANCELMODE" + case WM_CAPTURECHANGED: + return "WM_CAPTURECHANGED" + case WM_CHANGECBCHAIN: + return "WM_CHANGECBCHAIN" + case WM_CHAR: + return "WM_CHAR" + case WM_CHARTOITEM: + return "WM_CHARTOITEM" + case WM_CHILDACTIVATE: + return "WM_CHILDACTIVATE" + case WM_CLEAR: + return "WM_CLEAR" + case WM_CLOSE: + return "WM_CLOSE" + case WM_COMMAND: + return "WM_COMMAND" + case WM_COMMNOTIFY /* OBSOLETE */ : + return "WM_COMMNOTIFY" + case WM_COMPACTING: + return "WM_COMPACTING" + case WM_COMPAREITEM: + return "WM_COMPAREITEM" + case WM_CONTEXTMENU: + return "WM_CONTEXTMENU" + case WM_COPY: + return "WM_COPY" + case WM_COPYDATA: + return "WM_COPYDATA" + case WM_CREATE: + return "WM_CREATE" + case WM_CTLCOLORBTN: + return "WM_CTLCOLORBTN" + case WM_CTLCOLORDLG: + return "WM_CTLCOLORDLG" + case WM_CTLCOLOREDIT: + return "WM_CTLCOLOREDIT" + case WM_CTLCOLORLISTBOX: + return "WM_CTLCOLORLISTBOX" + case WM_CTLCOLORMSGBOX: + return "WM_CTLCOLORMSGBOX" + case WM_CTLCOLORSCROLLBAR: + return "WM_CTLCOLORSCROLLBAR" + case WM_CTLCOLORSTATIC: + return "WM_CTLCOLORSTATIC" + case WM_CUT: + return "WM_CUT" + case WM_DEADCHAR: + return "WM_DEADCHAR" + case WM_DELETEITEM: + return "WM_DELETEITEM" + case WM_DESTROY: + return "WM_DESTROY" + case WM_DESTROYCLIPBOARD: + return "WM_DESTROYCLIPBOARD" + case WM_DEVICECHANGE: + return "WM_DEVICECHANGE" + case WM_DEVMODECHANGE: + return "WM_DEVMODECHANGE" + case WM_DISPLAYCHANGE: + return "WM_DISPLAYCHANGE" + case WM_DRAWCLIPBOARD: + return "WM_DRAWCLIPBOARD" + case WM_DRAWITEM: + return "WM_DRAWITEM" + case WM_DROPFILES: + return "WM_DROPFILES" + case WM_ENABLE: + return "WM_ENABLE" + case WM_ENDSESSION: + return "WM_ENDSESSION" + case WM_ENTERIDLE: + return "WM_ENTERIDLE" + case WM_ENTERMENULOOP: + return "WM_ENTERMENULOOP" + case WM_ENTERSIZEMOVE: + return "WM_ENTERSIZEMOVE" + case WM_ERASEBKGND: + return "WM_ERASEBKGND" + case WM_EXITMENULOOP: + return "WM_EXITMENULOOP" + case WM_EXITSIZEMOVE: + return "WM_EXITSIZEMOVE" + case WM_FONTCHANGE: + return "WM_FONTCHANGE" + case WM_GETDLGCODE: + return "WM_GETDLGCODE" + case WM_GETFONT: + return "WM_GETFONT" + case WM_GETHOTKEY: + return "WM_GETHOTKEY" + case WM_GETICON: + return "WM_GETICON" + case WM_GETMINMAXINFO: + return "WM_GETMINMAXINFO" + case WM_GETTEXT: + return "WM_GETTEXT" + case WM_GETTEXTLENGTH: + return "WM_GETTEXTLENGTH" + case WM_HANDHELDFIRST: + return "WM_HANDHELDFIRST" + case WM_HANDHELDLAST: + return "WM_HANDHELDLAST" + case WM_HELP: + return "WM_HELP" + case WM_HOTKEY: + return "WM_HOTKEY" + case WM_HSCROLL: + return "WM_HSCROLL" + case WM_HSCROLLCLIPBOARD: + return "WM_HSCROLLCLIPBOARD" + case WM_ICONERASEBKGND: + return "WM_ICONERASEBKGND" + case WM_INITDIALOG: + return "WM_INITDIALOG" + case WM_INITMENU: + return "WM_INITMENU" + case WM_INITMENUPOPUP: + return "WM_INITMENUPOPUP" + case WM_INPUT: + return "WM_INPUT" + case WM_INPUTLANGCHANGE: + return "WM_INPUTLANGCHANGE" + case WM_INPUTLANGCHANGEREQUEST: + return "WM_INPUTLANGCHANGEREQUEST" + case WM_KEYDOWN: + return "WM_KEYDOWN" + case WM_KEYUP: + return "WM_KEYUP" + case WM_KILLFOCUS: + return "WM_KILLFOCUS" + case WM_MDIACTIVATE: + return "WM_MDIACTIVATE" + case WM_MDICASCADE: + return "WM_MDICASCADE" + case WM_MDICREATE: + return "WM_MDICREATE" + case WM_MDIDESTROY: + return "WM_MDIDESTROY" + case WM_MDIGETACTIVE: + return "WM_MDIGETACTIVE" + case WM_MDIICONARRANGE: + return "WM_MDIICONARRANGE" + case WM_MDIMAXIMIZE: + return "WM_MDIMAXIMIZE" + case WM_MDINEXT: + return "WM_MDINEXT" + case WM_MDIREFRESHMENU: + return "WM_MDIREFRESHMENU" + case WM_MDIRESTORE: + return "WM_MDIRESTORE" + case WM_MDISETMENU: + return "WM_MDISETMENU" + case WM_MDITILE: + return "WM_MDITILE" + case WM_MEASUREITEM: + return "WM_MEASUREITEM" + case WM_GETOBJECT: + return "WM_GETOBJECT" + case WM_CHANGEUISTATE: + return "WM_CHANGEUISTATE" + case WM_UPDATEUISTATE: + return "WM_UPDATEUISTATE" + case WM_QUERYUISTATE: + return "WM_QUERYUISTATE" + case WM_UNINITMENUPOPUP: + return "WM_UNINITMENUPOPUP" + case WM_MENURBUTTONUP: + return "WM_MENURBUTTONUP" + case WM_MENUCOMMAND: + return "WM_MENUCOMMAND" + case WM_MENUGETOBJECT: + return "WM_MENUGETOBJECT" + case WM_MENUDRAG: + return "WM_MENUDRAG" + case WM_APPCOMMAND: + return "WM_APPCOMMAND" + case WM_MENUCHAR: + return "WM_MENUCHAR" + case WM_MENUSELECT: + return "WM_MENUSELECT" + case WM_MOVE: + return "WM_MOVE" + case WM_MOVING: + return "WM_MOVING" + case WM_NCACTIVATE: + return "WM_NCACTIVATE" + case WM_NCCALCSIZE: + return "WM_NCCALCSIZE" + case WM_NCCREATE: + return "WM_NCCREATE" + case WM_NCDESTROY: + return "WM_NCDESTROY" + case WM_NCHITTEST: + return "WM_NCHITTEST" + case WM_NCLBUTTONDBLCLK: + return "WM_NCLBUTTONDBLCLK" + case WM_NCLBUTTONDOWN: + return "WM_NCLBUTTONDOWN" + case WM_NCLBUTTONUP: + return "WM_NCLBUTTONUP" + case WM_NCMBUTTONDBLCLK: + return "WM_NCMBUTTONDBLCLK" + case WM_NCMBUTTONDOWN: + return "WM_NCMBUTTONDOWN" + case WM_NCMBUTTONUP: + return "WM_NCMBUTTONUP" + case WM_NCXBUTTONDOWN: + return "WM_NCXBUTTONDOWN" + case WM_NCXBUTTONUP: + return "WM_NCXBUTTONUP" + case WM_NCXBUTTONDBLCLK: + return "WM_NCXBUTTONDBLCLK" + case WM_NCMOUSEHOVER: + return "WM_NCMOUSEHOVER" + case WM_NCMOUSELEAVE: + return "WM_NCMOUSELEAVE" + case WM_NCMOUSEMOVE: + return "WM_NCMOUSEMOVE" + case WM_NCPAINT: + return "WM_NCPAINT" + case WM_NCRBUTTONDBLCLK: + return "WM_NCRBUTTONDBLCLK" + case WM_NCRBUTTONDOWN: + return "WM_NCRBUTTONDOWN" + case WM_NCRBUTTONUP: + return "WM_NCRBUTTONUP" + case WM_NEXTDLGCTL: + return "WM_NEXTDLGCTL" + case WM_NEXTMENU: + return "WM_NEXTMENU" + case WM_NOTIFY: + return "WM_NOTIFY" + case WM_NOTIFYFORMAT: + return "WM_NOTIFYFORMAT" + case WM_NULL: + return "WM_NULL" + case WM_PAINT: + return "WM_PAINT" + case WM_PAINTCLIPBOARD: + return "WM_PAINTCLIPBOARD" + case WM_PAINTICON: + return "WM_PAINTICON" + case WM_PALETTECHANGED: + return "WM_PALETTECHANGED" + case WM_PALETTEISCHANGING: + return "WM_PALETTEISCHANGING" + case WM_PARENTNOTIFY: + return "WM_PARENTNOTIFY" + case WM_PASTE: + return "WM_PASTE" + case WM_PENWINFIRST: + return "WM_PENWINFIRST" + case WM_PENWINLAST: + return "WM_PENWINLAST" + case WM_POWER: + return "WM_POWER" + case WM_PRINT: + return "WM_PRINT" + case WM_PRINTCLIENT: + return "WM_PRINTCLIENT" + case WM_QUERYDRAGICON: + return "WM_QUERYDRAGICON" + case WM_QUERYENDSESSION: + return "WM_QUERYENDSESSION" + case WM_QUERYNEWPALETTE: + return "WM_QUERYNEWPALETTE" + case WM_QUERYOPEN: + return "WM_QUERYOPEN" + case WM_QUEUESYNC: + return "WM_QUEUESYNC" + case WM_QUIT: + return "WM_QUIT" + case WM_RENDERALLFORMATS: + return "WM_RENDERALLFORMATS" + case WM_RENDERFORMAT: + return "WM_RENDERFORMAT" + case WM_SETCURSOR: + return "WM_SETCURSOR" + case WM_SETFOCUS: + return "WM_SETFOCUS" + case WM_SETFONT: + return "WM_SETFONT" + case WM_SETHOTKEY: + return "WM_SETHOTKEY" + case WM_SETICON: + return "WM_SETICON" + case WM_SETREDRAW: + return "WM_SETREDRAW" + case WM_SETTEXT: + return "WM_SETTEXT" + case WM_SETTINGCHANGE: + return "WM_SETTINGCHANGE" + case WM_SHOWWINDOW: + return "WM_SHOWWINDOW" + case WM_SIZE: + return "WM_SIZE" + case WM_SIZECLIPBOARD: + return "WM_SIZECLIPBOARD" + case WM_SIZING: + return "WM_SIZING" + case WM_SPOOLERSTATUS: + return "WM_SPOOLERSTATUS" + case WM_STYLECHANGED: + return "WM_STYLECHANGED" + case WM_STYLECHANGING: + return "WM_STYLECHANGING" + case WM_SYSCHAR: + return "WM_SYSCHAR" + case WM_SYSCOLORCHANGE: + return "WM_SYSCOLORCHANGE" + case WM_SYSCOMMAND: + return "WM_SYSCOMMAND" + case WM_SYSDEADCHAR: + return "WM_SYSDEADCHAR" + case WM_SYSKEYDOWN: + return "WM_SYSKEYDOWN" + case WM_SYSKEYUP: + return "WM_SYSKEYUP" + case WM_TCARD: + return "WM_TCARD" + case WM_THEMECHANGED: + return "WM_THEMECHANGED" + case WM_TIMECHANGE: + return "WM_TIMECHANGE" + case WM_TIMER: + return "WM_TIMER" + case WM_UNDO: + return "WM_UNDO" + case WM_USER: + return "WM_USER" + case WM_USERCHANGED: + return "WM_USERCHANGED" + case WM_VKEYTOITEM: + return "WM_VKEYTOITEM" + case WM_VSCROLL: + return "WM_VSCROLL" + case WM_VSCROLLCLIPBOARD: + return "WM_VSCROLLCLIPBOARD" + case WM_WINDOWPOSCHANGED: + return "WM_WINDOWPOSCHANGED" + case WM_WINDOWPOSCHANGING: + return "WM_WINDOWPOSCHANGING" + case WM_KEYLAST: + return "WM_KEYLAST" + case WM_SYNCPAINT: + return "WM_SYNCPAINT" + case WM_MOUSEACTIVATE: + return "WM_MOUSEACTIVATE" + case WM_MOUSEMOVE: + return "WM_MOUSEMOVE" + case WM_LBUTTONDOWN: + return "WM_LBUTTONDOWN" + case WM_LBUTTONUP: + return "WM_LBUTTONUP" + case WM_LBUTTONDBLCLK: + return "WM_LBUTTONDBLCLK" + case WM_RBUTTONDOWN: + return "WM_RBUTTONDOWN" + case WM_RBUTTONUP: + return "WM_RBUTTONUP" + case WM_RBUTTONDBLCLK: + return "WM_RBUTTONDBLCLK" + case WM_MBUTTONDOWN: + return "WM_MBUTTONDOWN" + case WM_MBUTTONUP: + return "WM_MBUTTONUP" + case WM_MBUTTONDBLCLK: + return "WM_MBUTTONDBLCLK" + case WM_MOUSEWHEEL: + return "WM_MOUSEWHEEL" + case WM_XBUTTONDOWN: + return "WM_XBUTTONDOWN" + case WM_XBUTTONUP: + return "WM_XBUTTONUP" + case WM_MOUSELAST: + return "WM_MOUSELAST" + case WM_MOUSEHOVER: + return "WM_MOUSEHOVER" + case WM_MOUSELEAVE: + return "WM_MOUSELEAVE" + case WM_CLIPBOARDUPDATE: + return "WM_CLIPBOARDUPDATE" + default: + return fmt.Sprintf("0x%08x", msg) + } +} + +var windowsVersion, _ = operatingsystem.GetWindowsVersionInfo() + +func IsWindowsVersionAtLeast(major, minor, buildNumber int) bool { + return windowsVersion.Major >= major && + windowsVersion.Minor >= minor && + windowsVersion.Build >= buildNumber +} + +type WindowProc func(hwnd HWND, msg uint32, wparam, lparam uintptr) uintptr + +func GetModuleHandle(value uintptr) uintptr { + result, _, _ := procGetModuleHandle.Call(value) + return result +} + +func GetMessage(msg *MSG) uintptr { + rt, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0) + return rt +} + +func PostMessage(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr { + ret, _, _ := procPostMessage.Call( + uintptr(hwnd), + uintptr(msg), + wParam, + lParam) + + return ret +} + +func ShellNotifyIcon(cmd uintptr, nid *NOTIFYICONDATA) bool { + ret, _, _ := procShellNotifyIcon.Call(cmd, uintptr(unsafe.Pointer(nid))) + return ret == 1 +} + +func IsDialogMessage(hwnd HWND, msg *MSG) uintptr { + ret, _, _ := procIsDialogMessage.Call(uintptr(hwnd), uintptr(unsafe.Pointer(msg))) + return ret +} + +func TranslateMessage(msg *MSG) uintptr { + ret, _, _ := procTranslateMessage.Call(uintptr(unsafe.Pointer(msg))) + return ret +} + +func DispatchMessage(msg *MSG) uintptr { + ret, _, _ := procDispatchMessage.Call(uintptr(unsafe.Pointer(msg))) + return ret +} + +func PostQuitMessage(exitCode int32) { + procPostQuitMessage.Call(uintptr(exitCode)) +} + +func LoHiWords(input uint32) (uint16, uint16) { + return uint16(input & 0xffff), uint16(input >> 16 & 0xffff) +} diff --git a/v2/internal/platform/win32/cursor.go b/v2/internal/platform/win32/cursor.go new file mode 100644 index 000000000..a9a32e733 --- /dev/null +++ b/v2/internal/platform/win32/cursor.go @@ -0,0 +1,9 @@ +package win32 + +import "unsafe" + +func GetCursorPos() (x, y int, ok bool) { + pt := POINT{} + ret, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt))) + return int(pt.X), int(pt.Y), ret != 0 +} diff --git a/v2/internal/platform/win32/icon.go b/v2/internal/platform/win32/icon.go new file mode 100644 index 000000000..43eb9a4a1 --- /dev/null +++ b/v2/internal/platform/win32/icon.go @@ -0,0 +1,39 @@ +package win32 + +import ( + "unsafe" +) + +func CreateIconFromResourceEx(presbits uintptr, dwResSize uint32, isIcon bool, version uint32, cxDesired int, cyDesired int, flags uint) (uintptr, error) { + icon := 0 + if isIcon { + icon = 1 + } + r, _, err := procCreateIconFromResourceEx.Call( + presbits, + uintptr(dwResSize), + uintptr(icon), + uintptr(version), + uintptr(cxDesired), + uintptr(cyDesired), + uintptr(flags), + ) + + if r == 0 { + return 0, err + } + return r, nil +} + +// CreateHIconFromPNG creates a HICON from a PNG file +func CreateHIconFromPNG(pngData []byte) (HICON, error) { + icon, err := CreateIconFromResourceEx( + uintptr(unsafe.Pointer(&pngData[0])), + uint32(len(pngData)), + true, + 0x00030000, + 0, + 0, + LR_DEFAULTSIZE) + return HICON(icon), err +} diff --git a/v2/internal/platform/win32/keyboard.go b/v2/internal/platform/win32/keyboard.go new file mode 100644 index 000000000..6146da5f3 --- /dev/null +++ b/v2/internal/platform/win32/keyboard.go @@ -0,0 +1,808 @@ +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package win32 + +import ( + "bytes" + "github.com/wailsapp/wails/v2/pkg/menu/keys" + "strings" + "unsafe" +) + +type Key uint16 + +func (k Key) String() string { + return key2string[k] +} + +// Virtual key codes +const ( + VK_LBUTTON = 1 + VK_RBUTTON = 2 + VK_CANCEL = 3 + VK_MBUTTON = 4 + VK_XBUTTON1 = 5 + VK_XBUTTON2 = 6 + VK_BACK = 8 + VK_TAB = 9 + VK_CLEAR = 12 + VK_RETURN = 13 + VK_SHIFT = 16 + VK_CONTROL = 17 + VK_MENU = 18 + VK_PAUSE = 19 + VK_CAPITAL = 20 + VK_KANA = 0x15 + VK_HANGEUL = 0x15 + VK_HANGUL = 0x15 + VK_JUNJA = 0x17 + VK_FINAL = 0x18 + VK_HANJA = 0x19 + VK_KANJI = 0x19 + VK_ESCAPE = 0x1B + VK_CONVERT = 0x1C + VK_NONCONVERT = 0x1D + VK_ACCEPT = 0x1E + VK_MODECHANGE = 0x1F + VK_SPACE = 32 + VK_PRIOR = 33 + VK_NEXT = 34 + VK_END = 35 + VK_HOME = 36 + VK_LEFT = 37 + VK_UP = 38 + VK_RIGHT = 39 + VK_DOWN = 40 + VK_SELECT = 41 + VK_PRINT = 42 + VK_EXECUTE = 43 + VK_SNAPSHOT = 44 + VK_INSERT = 45 + VK_DELETE = 46 + VK_HELP = 47 + VK_LWIN = 0x5B + VK_RWIN = 0x5C + VK_APPS = 0x5D + VK_SLEEP = 0x5F + VK_NUMPAD0 = 0x60 + VK_NUMPAD1 = 0x61 + VK_NUMPAD2 = 0x62 + VK_NUMPAD3 = 0x63 + VK_NUMPAD4 = 0x64 + VK_NUMPAD5 = 0x65 + VK_NUMPAD6 = 0x66 + VK_NUMPAD7 = 0x67 + VK_NUMPAD8 = 0x68 + VK_NUMPAD9 = 0x69 + VK_MULTIPLY = 0x6A + VK_ADD = 0x6B + VK_SEPARATOR = 0x6C + VK_SUBTRACT = 0x6D + VK_DECIMAL = 0x6E + VK_DIVIDE = 0x6F + VK_F1 = 0x70 + VK_F2 = 0x71 + VK_F3 = 0x72 + VK_F4 = 0x73 + VK_F5 = 0x74 + VK_F6 = 0x75 + VK_F7 = 0x76 + VK_F8 = 0x77 + VK_F9 = 0x78 + VK_F10 = 0x79 + VK_F11 = 0x7A + VK_F12 = 0x7B + VK_F13 = 0x7C + VK_F14 = 0x7D + VK_F15 = 0x7E + VK_F16 = 0x7F + VK_F17 = 0x80 + VK_F18 = 0x81 + VK_F19 = 0x82 + VK_F20 = 0x83 + VK_F21 = 0x84 + VK_F22 = 0x85 + VK_F23 = 0x86 + VK_F24 = 0x87 + VK_NUMLOCK = 0x90 + VK_SCROLL = 0x91 + VK_LSHIFT = 0xA0 + VK_RSHIFT = 0xA1 + VK_LCONTROL = 0xA2 + VK_RCONTROL = 0xA3 + VK_LMENU = 0xA4 + VK_RMENU = 0xA5 + VK_BROWSER_BACK = 0xA6 + VK_BROWSER_FORWARD = 0xA7 + VK_BROWSER_REFRESH = 0xA8 + VK_BROWSER_STOP = 0xA9 + VK_BROWSER_SEARCH = 0xAA + VK_BROWSER_FAVORITES = 0xAB + VK_BROWSER_HOME = 0xAC + VK_VOLUME_MUTE = 0xAD + VK_VOLUME_DOWN = 0xAE + VK_VOLUME_UP = 0xAF + VK_MEDIA_NEXT_TRACK = 0xB0 + VK_MEDIA_PREV_TRACK = 0xB1 + VK_MEDIA_STOP = 0xB2 + VK_MEDIA_PLAY_PAUSE = 0xB3 + VK_LAUNCH_MAIL = 0xB4 + VK_LAUNCH_MEDIA_SELECT = 0xB5 + VK_LAUNCH_APP1 = 0xB6 + VK_LAUNCH_APP2 = 0xB7 + VK_OEM_1 = 0xBA + VK_OEM_PLUS = 0xBB + VK_OEM_COMMA = 0xBC + VK_OEM_MINUS = 0xBD + VK_OEM_PERIOD = 0xBE + VK_OEM_2 = 0xBF + VK_OEM_3 = 0xC0 + VK_OEM_4 = 0xDB + VK_OEM_5 = 0xDC + VK_OEM_6 = 0xDD + VK_OEM_7 = 0xDE + VK_OEM_8 = 0xDF + VK_OEM_102 = 0xE2 + VK_PROCESSKEY = 0xE5 + VK_PACKET = 0xE7 + VK_ATTN = 0xF6 + VK_CRSEL = 0xF7 + VK_EXSEL = 0xF8 + VK_EREOF = 0xF9 + VK_PLAY = 0xFA + VK_ZOOM = 0xFB + VK_NONAME = 0xFC + VK_PA1 = 0xFD + VK_OEM_CLEAR = 0xFE +) + +const ( + KeyLButton Key = VK_LBUTTON + KeyRButton Key = VK_RBUTTON + KeyCancel Key = VK_CANCEL + KeyMButton Key = VK_MBUTTON + KeyXButton1 Key = VK_XBUTTON1 + KeyXButton2 Key = VK_XBUTTON2 + KeyBack Key = VK_BACK + KeyTab Key = VK_TAB + KeyClear Key = VK_CLEAR + KeyReturn Key = VK_RETURN + KeyShift Key = VK_SHIFT + KeyControl Key = VK_CONTROL + KeyAlt Key = VK_MENU + KeyMenu Key = VK_MENU + KeyPause Key = VK_PAUSE + KeyCapital Key = VK_CAPITAL + KeyKana Key = VK_KANA + KeyHangul Key = VK_HANGUL + KeyJunja Key = VK_JUNJA + KeyFinal Key = VK_FINAL + KeyHanja Key = VK_HANJA + KeyKanji Key = VK_KANJI + KeyEscape Key = VK_ESCAPE + KeyConvert Key = VK_CONVERT + KeyNonconvert Key = VK_NONCONVERT + KeyAccept Key = VK_ACCEPT + KeyModeChange Key = VK_MODECHANGE + KeySpace Key = VK_SPACE + KeyPrior Key = VK_PRIOR + KeyNext Key = VK_NEXT + KeyEnd Key = VK_END + KeyHome Key = VK_HOME + KeyLeft Key = VK_LEFT + KeyUp Key = VK_UP + KeyRight Key = VK_RIGHT + KeyDown Key = VK_DOWN + KeySelect Key = VK_SELECT + KeyPrint Key = VK_PRINT + KeyExecute Key = VK_EXECUTE + KeySnapshot Key = VK_SNAPSHOT + KeyInsert Key = VK_INSERT + KeyDelete Key = VK_DELETE + KeyHelp Key = VK_HELP + Key0 Key = 0x30 + Key1 Key = 0x31 + Key2 Key = 0x32 + Key3 Key = 0x33 + Key4 Key = 0x34 + Key5 Key = 0x35 + Key6 Key = 0x36 + Key7 Key = 0x37 + Key8 Key = 0x38 + Key9 Key = 0x39 + KeyA Key = 0x41 + KeyB Key = 0x42 + KeyC Key = 0x43 + KeyD Key = 0x44 + KeyE Key = 0x45 + KeyF Key = 0x46 + KeyG Key = 0x47 + KeyH Key = 0x48 + KeyI Key = 0x49 + KeyJ Key = 0x4A + KeyK Key = 0x4B + KeyL Key = 0x4C + KeyM Key = 0x4D + KeyN Key = 0x4E + KeyO Key = 0x4F + KeyP Key = 0x50 + KeyQ Key = 0x51 + KeyR Key = 0x52 + KeyS Key = 0x53 + KeyT Key = 0x54 + KeyU Key = 0x55 + KeyV Key = 0x56 + KeyW Key = 0x57 + KeyX Key = 0x58 + KeyY Key = 0x59 + KeyZ Key = 0x5A + KeyLWIN Key = VK_LWIN + KeyRWIN Key = VK_RWIN + KeyApps Key = VK_APPS + KeySleep Key = VK_SLEEP + KeyNumpad0 Key = VK_NUMPAD0 + KeyNumpad1 Key = VK_NUMPAD1 + KeyNumpad2 Key = VK_NUMPAD2 + KeyNumpad3 Key = VK_NUMPAD3 + KeyNumpad4 Key = VK_NUMPAD4 + KeyNumpad5 Key = VK_NUMPAD5 + KeyNumpad6 Key = VK_NUMPAD6 + KeyNumpad7 Key = VK_NUMPAD7 + KeyNumpad8 Key = VK_NUMPAD8 + KeyNumpad9 Key = VK_NUMPAD9 + KeyMultiply Key = VK_MULTIPLY + KeyAdd Key = VK_ADD + KeySeparator Key = VK_SEPARATOR + KeySubtract Key = VK_SUBTRACT + KeyDecimal Key = VK_DECIMAL + KeyDivide Key = VK_DIVIDE + KeyF1 Key = VK_F1 + KeyF2 Key = VK_F2 + KeyF3 Key = VK_F3 + KeyF4 Key = VK_F4 + KeyF5 Key = VK_F5 + KeyF6 Key = VK_F6 + KeyF7 Key = VK_F7 + KeyF8 Key = VK_F8 + KeyF9 Key = VK_F9 + KeyF10 Key = VK_F10 + KeyF11 Key = VK_F11 + KeyF12 Key = VK_F12 + KeyF13 Key = VK_F13 + KeyF14 Key = VK_F14 + KeyF15 Key = VK_F15 + KeyF16 Key = VK_F16 + KeyF17 Key = VK_F17 + KeyF18 Key = VK_F18 + KeyF19 Key = VK_F19 + KeyF20 Key = VK_F20 + KeyF21 Key = VK_F21 + KeyF22 Key = VK_F22 + KeyF23 Key = VK_F23 + KeyF24 Key = VK_F24 + KeyNumlock Key = VK_NUMLOCK + KeyScroll Key = VK_SCROLL + KeyLShift Key = VK_LSHIFT + KeyRShift Key = VK_RSHIFT + KeyLControl Key = VK_LCONTROL + KeyRControl Key = VK_RCONTROL + KeyLAlt Key = VK_LMENU + KeyLMenu Key = VK_LMENU + KeyRAlt Key = VK_RMENU + KeyRMenu Key = VK_RMENU + KeyBrowserBack Key = VK_BROWSER_BACK + KeyBrowserForward Key = VK_BROWSER_FORWARD + KeyBrowserRefresh Key = VK_BROWSER_REFRESH + KeyBrowserStop Key = VK_BROWSER_STOP + KeyBrowserSearch Key = VK_BROWSER_SEARCH + KeyBrowserFavorites Key = VK_BROWSER_FAVORITES + KeyBrowserHome Key = VK_BROWSER_HOME + KeyVolumeMute Key = VK_VOLUME_MUTE + KeyVolumeDown Key = VK_VOLUME_DOWN + KeyVolumeUp Key = VK_VOLUME_UP + KeyMediaNextTrack Key = VK_MEDIA_NEXT_TRACK + KeyMediaPrevTrack Key = VK_MEDIA_PREV_TRACK + KeyMediaStop Key = VK_MEDIA_STOP + KeyMediaPlayPause Key = VK_MEDIA_PLAY_PAUSE + KeyLaunchMail Key = VK_LAUNCH_MAIL + KeyLaunchMediaSelect Key = VK_LAUNCH_MEDIA_SELECT + KeyLaunchApp1 Key = VK_LAUNCH_APP1 + KeyLaunchApp2 Key = VK_LAUNCH_APP2 + KeyOEM1 Key = VK_OEM_1 + KeyOEMPlus Key = VK_OEM_PLUS + KeyOEMComma Key = VK_OEM_COMMA + KeyOEMMinus Key = VK_OEM_MINUS + KeyOEMPeriod Key = VK_OEM_PERIOD + KeyOEM2 Key = VK_OEM_2 + KeyOEM3 Key = VK_OEM_3 + KeyOEM4 Key = VK_OEM_4 + KeyOEM5 Key = VK_OEM_5 + KeyOEM6 Key = VK_OEM_6 + KeyOEM7 Key = VK_OEM_7 + KeyOEM8 Key = VK_OEM_8 + KeyOEM102 Key = VK_OEM_102 + KeyProcessKey Key = VK_PROCESSKEY + KeyPacket Key = VK_PACKET + KeyAttn Key = VK_ATTN + KeyCRSel Key = VK_CRSEL + KeyEXSel Key = VK_EXSEL + KeyErEOF Key = VK_EREOF + KeyPlay Key = VK_PLAY + KeyZoom Key = VK_ZOOM + KeyNoName Key = VK_NONAME + KeyPA1 Key = VK_PA1 + KeyOEMClear Key = VK_OEM_CLEAR +) + +var key2string = map[Key]string{ + KeyLButton: "LButton", + KeyRButton: "RButton", + KeyCancel: "Cancel", + KeyMButton: "MButton", + KeyXButton1: "XButton1", + KeyXButton2: "XButton2", + KeyBack: "Back", + KeyTab: "Tab", + KeyClear: "Clear", + KeyReturn: "Return", + KeyShift: "Shift", + KeyControl: "Control", + KeyAlt: "Alt / Menu", + KeyPause: "Pause", + KeyCapital: "Capital", + KeyKana: "Kana / Hangul", + KeyJunja: "Junja", + KeyFinal: "Final", + KeyHanja: "Hanja / Kanji", + KeyEscape: "Escape", + KeyConvert: "Convert", + KeyNonconvert: "Nonconvert", + KeyAccept: "Accept", + KeyModeChange: "ModeChange", + KeySpace: "Space", + KeyPrior: "Prior", + KeyNext: "Next", + KeyEnd: "End", + KeyHome: "Home", + KeyLeft: "Left", + KeyUp: "Up", + KeyRight: "Right", + KeyDown: "Down", + KeySelect: "Select", + KeyPrint: "Print", + KeyExecute: "Execute", + KeySnapshot: "Snapshot", + KeyInsert: "Insert", + KeyDelete: "Delete", + KeyHelp: "Help", + Key0: "0", + Key1: "1", + Key2: "2", + Key3: "3", + Key4: "4", + Key5: "5", + Key6: "6", + Key7: "7", + Key8: "8", + Key9: "9", + KeyA: "A", + KeyB: "B", + KeyC: "C", + KeyD: "D", + KeyE: "E", + KeyF: "F", + KeyG: "G", + KeyH: "H", + KeyI: "I", + KeyJ: "J", + KeyK: "K", + KeyL: "L", + KeyM: "M", + KeyN: "N", + KeyO: "O", + KeyP: "P", + KeyQ: "Q", + KeyR: "R", + KeyS: "S", + KeyT: "T", + KeyU: "U", + KeyV: "V", + KeyW: "W", + KeyX: "X", + KeyY: "Y", + KeyZ: "Z", + KeyLWIN: "LWIN", + KeyRWIN: "RWIN", + KeyApps: "Apps", + KeySleep: "Sleep", + KeyNumpad0: "Numpad0", + KeyNumpad1: "Numpad1", + KeyNumpad2: "Numpad2", + KeyNumpad3: "Numpad3", + KeyNumpad4: "Numpad4", + KeyNumpad5: "Numpad5", + KeyNumpad6: "Numpad6", + KeyNumpad7: "Numpad7", + KeyNumpad8: "Numpad8", + KeyNumpad9: "Numpad9", + KeyMultiply: "Multiply", + KeyAdd: "Add", + KeySeparator: "Separator", + KeySubtract: "Subtract", + KeyDecimal: "Decimal", + KeyDivide: "Divide", + KeyF1: "F1", + KeyF2: "F2", + KeyF3: "F3", + KeyF4: "F4", + KeyF5: "F5", + KeyF6: "F6", + KeyF7: "F7", + KeyF8: "F8", + KeyF9: "F9", + KeyF10: "F10", + KeyF11: "F11", + KeyF12: "F12", + KeyF13: "F13", + KeyF14: "F14", + KeyF15: "F15", + KeyF16: "F16", + KeyF17: "F17", + KeyF18: "F18", + KeyF19: "F19", + KeyF20: "F20", + KeyF21: "F21", + KeyF22: "F22", + KeyF23: "F23", + KeyF24: "F24", + KeyNumlock: "Numlock", + KeyScroll: "Scroll", + KeyLShift: "LShift", + KeyRShift: "RShift", + KeyLControl: "LControl", + KeyRControl: "RControl", + KeyLMenu: "LMenu", + KeyRMenu: "RMenu", + KeyBrowserBack: "BrowserBack", + KeyBrowserForward: "BrowserForward", + KeyBrowserRefresh: "BrowserRefresh", + KeyBrowserStop: "BrowserStop", + KeyBrowserSearch: "BrowserSearch", + KeyBrowserFavorites: "BrowserFavorites", + KeyBrowserHome: "BrowserHome", + KeyVolumeMute: "VolumeMute", + KeyVolumeDown: "VolumeDown", + KeyVolumeUp: "VolumeUp", + KeyMediaNextTrack: "MediaNextTrack", + KeyMediaPrevTrack: "MediaPrevTrack", + KeyMediaStop: "MediaStop", + KeyMediaPlayPause: "MediaPlayPause", + KeyLaunchMail: "LaunchMail", + KeyLaunchMediaSelect: "LaunchMediaSelect", + KeyLaunchApp1: "LaunchApp1", + KeyLaunchApp2: "LaunchApp2", + KeyOEM1: "OEM1", + KeyOEMPlus: "OEMPlus", + KeyOEMComma: "OEMComma", + KeyOEMMinus: "OEMMinus", + KeyOEMPeriod: "OEMPeriod", + KeyOEM2: "OEM2", + KeyOEM3: "OEM3", + KeyOEM4: "OEM4", + KeyOEM5: "OEM5", + KeyOEM6: "OEM6", + KeyOEM7: "OEM7", + KeyOEM8: "OEM8", + KeyOEM102: "OEM102", + KeyProcessKey: "ProcessKey", + KeyPacket: "Packet", + KeyAttn: "Attn", + KeyCRSel: "CRSel", + KeyEXSel: "EXSel", + KeyErEOF: "ErEOF", + KeyPlay: "Play", + KeyZoom: "Zoom", + KeyNoName: "NoName", + KeyPA1: "PA1", + KeyOEMClear: "OEMClear", +} + +type Modifiers byte + +func (m Modifiers) String() string { + return modifiers2string[m] +} + +var modifiers2string = map[Modifiers]string{ + ModShift: "Shift", + ModControl: "Ctrl", + ModControl | ModShift: "Ctrl+Shift", + ModAlt: "Alt", + ModAlt | ModShift: "Alt+Shift", + ModAlt | ModControl | ModShift: "Alt+Ctrl+Shift", +} + +const ( + ModShift Modifiers = 1 << iota + ModControl + ModAlt +) + +func ModifiersDown() Modifiers { + var m Modifiers + + if ShiftDown() { + m |= ModShift + } + if ControlDown() { + m |= ModControl + } + if AltDown() { + m |= ModAlt + } + + return m +} + +type Shortcut struct { + Modifiers Modifiers + Key Key +} + +func (s Shortcut) String() string { + m := s.Modifiers.String() + if m == "" { + return s.Key.String() + } + + b := new(bytes.Buffer) + + b.WriteString(m) + b.WriteRune('+') + b.WriteString(s.Key.String()) + + return b.String() +} + +func GetKeyState(nVirtKey int32) int16 { + ret, _, _ := procGetKeyState.Call( + uintptr(nVirtKey), + ) + + return int16(ret) +} + +func AltDown() bool { + return GetKeyState(int32(KeyAlt))>>15 != 0 +} + +func ControlDown() bool { + return GetKeyState(int32(KeyControl))>>15 != 0 +} + +func ShiftDown() bool { + return GetKeyState(int32(KeyShift))>>15 != 0 +} + +var ModifierMap = map[keys.Modifier]Modifiers{ + keys.ShiftKey: ModShift, + keys.ControlKey: ModControl, + keys.OptionOrAltKey: ModAlt, + keys.CmdOrCtrlKey: ModControl, +} + +var NoShortcut = Shortcut{} + +func AcceleratorToShortcut(accelerator *keys.Accelerator) Shortcut { + + if accelerator == nil { + return NoShortcut + } + inKey := strings.ToUpper(accelerator.Key) + key, exists := KeyMap[inKey] + if !exists { + return NoShortcut + } + var modifiers Modifiers + if _, exists := shiftMap[inKey]; exists { + modifiers = ModShift + } + for _, mod := range accelerator.Modifiers { + modifiers |= ModifierMap[mod] + } + return Shortcut{ + Modifiers: modifiers, + Key: key, + } +} + +var shiftMap = map[string]struct{}{ + "~": {}, + ")": {}, + "!": {}, + "@": {}, + "#": {}, + "$": {}, + "%": {}, + "^": {}, + "&": {}, + "*": {}, + "(": {}, + "_": {}, + "PLUS": {}, + "<": {}, + ">": {}, + "?": {}, + ":": {}, + `"`: {}, + "{": {}, + "}": {}, + "|": {}, +} + +var KeyMap = map[string]Key{ + "0": Key0, + "1": Key1, + "2": Key2, + "3": Key3, + "4": Key4, + "5": Key5, + "6": Key6, + "7": Key7, + "8": Key8, + "9": Key9, + "A": KeyA, + "B": KeyB, + "C": KeyC, + "D": KeyD, + "E": KeyE, + "F": KeyF, + "G": KeyG, + "H": KeyH, + "I": KeyI, + "J": KeyJ, + "K": KeyK, + "L": KeyL, + "M": KeyM, + "N": KeyN, + "O": KeyO, + "P": KeyP, + "Q": KeyQ, + "R": KeyR, + "S": KeyS, + "T": KeyT, + "U": KeyU, + "V": KeyV, + "W": KeyW, + "X": KeyX, + "Y": KeyY, + "Z": KeyZ, + "F1": KeyF1, + "F2": KeyF2, + "F3": KeyF3, + "F4": KeyF4, + "F5": KeyF5, + "F6": KeyF6, + "F7": KeyF7, + "F8": KeyF8, + "F9": KeyF9, + "F10": KeyF10, + "F11": KeyF11, + "F12": KeyF12, + "F13": KeyF13, + "F14": KeyF14, + "F15": KeyF15, + "F16": KeyF16, + "F17": KeyF17, + "F18": KeyF18, + "F19": KeyF19, + "F20": KeyF20, + "F21": KeyF21, + "F22": KeyF22, + "F23": KeyF23, + "F24": KeyF24, + + "`": KeyOEM3, + ",": KeyOEMComma, + ".": KeyOEMPeriod, + "/": KeyOEM2, + ";": KeyOEM1, + "'": KeyOEM7, + "[": KeyOEM4, + "]": KeyOEM6, + `\`: KeyOEM5, + "~": KeyOEM3, + ")": Key0, + "!": Key1, + "@": Key2, + "#": Key3, + "$": Key4, + "%": Key5, + "^": Key6, + "&": Key7, + "*": Key8, + "(": Key9, + "_": KeyOEMMinus, + "PLUS": KeyOEMPlus, + "<": KeyOEMComma, + ">": KeyOEMPeriod, + "?": KeyOEM2, + ":": KeyOEM1, + `"`: KeyOEM7, + "{": KeyOEM4, + "}": KeyOEM6, + "|": KeyOEM5, + + "SPACE": KeySpace, + "TAB": KeyTab, + "CAPSLOCK": KeyCapital, + "NUMLOCK": KeyNumlock, + "SCROLLLOCK": KeyScroll, + "BACKSPACE": KeyBack, + "DELETE": KeyDelete, + "INSERT": KeyInsert, + "RETURN": KeyReturn, + "ENTER": KeyReturn, + "UP": KeyUp, + "DOWN": KeyDown, + "LEFT": KeyLeft, + "RIGHT": KeyRight, + "HOME": KeyHome, + "END": KeyEnd, + "PAGEUP": KeyPrior, + "PAGEDOWN": KeyNext, + "ESCAPE": KeyEscape, + "ESC": KeyEscape, + "VOLUMEUP": KeyVolumeUp, + "VOLUMEDOWN": KeyVolumeDown, + "VOLUMEMUTE": KeyVolumeMute, + "MEDIANEXTTRACK": KeyMediaNextTrack, + "MEDIAPREVIOUSTRACK": KeyMediaPrevTrack, + "MEDIASTOP": KeyMediaStop, + "MEDIAPLAYPAUSE": KeyMediaPlayPause, + "PRINTSCREEN": KeyPrint, + "NUM0": KeyNumpad0, + "NUM1": KeyNumpad1, + "NUM2": KeyNumpad2, + "NUM3": KeyNumpad3, + "NUM4": KeyNumpad4, + "NUM5": KeyNumpad5, + "NUM6": KeyNumpad6, + "NUM7": KeyNumpad7, + "NUM8": KeyNumpad8, + "NUM9": KeyNumpad9, + "nummult": KeyMultiply, + "numadd": KeyAdd, + "numsub": KeySubtract, + "numdec": KeyDecimal, + "numdiv": KeyDivide, +} + +type Accelerator struct { + Virtual byte + Key uint16 + Cmd uint16 +} + +func CreateAcceleratorTable(acc []Accelerator) uintptr { + if len(acc) == 0 { + return 0 + } + ret, _, _ := procCreateAcceleratorTable.Call( + uintptr(unsafe.Pointer(&acc[0])), + uintptr(len(acc)), + ) + return ret +} + +func TranslateAccelerator(hwnd HWND, hAccTable uintptr, lpMsg *MSG) bool { + ret, _, _ := procTranslateAccelerator.Call( + uintptr(hwnd), + hAccTable, + uintptr(unsafe.Pointer(lpMsg)), + ) + return ret != 0 +} diff --git a/v2/internal/platform/win32/menu.go b/v2/internal/platform/win32/menu.go new file mode 100644 index 000000000..15c810be2 --- /dev/null +++ b/v2/internal/platform/win32/menu.go @@ -0,0 +1,80 @@ +package win32 + +type Menu HMENU +type PopupMenu Menu + +func CreatePopupMenu() PopupMenu { + ret, _, _ := procCreatePopupMenu.Call(0, 0, 0, 0) + return PopupMenu(ret) +} + +func (m Menu) Destroy() bool { + ret, _, _ := procDestroyMenu.Call(uintptr(m)) + return ret != 0 +} + +func (p PopupMenu) Destroy() bool { + return Menu(p).Destroy() +} + +func (p PopupMenu) Track(flags uint, x, y int, wnd HWND) bool { + ret, _, _ := procTrackPopupMenu.Call( + uintptr(p), + uintptr(flags), + uintptr(x), + uintptr(y), + 0, + uintptr(wnd), + 0, + ) + return ret != 0 +} + +func (p PopupMenu) Append(flags uintptr, id uintptr, text string) bool { + return Menu(p).Append(flags, id, text) +} + +func (m Menu) Append(flags uintptr, id uintptr, text string) bool { + ret, _, _ := procAppendMenuW.Call( + uintptr(m), + flags, + id, + MustStringToUTF16uintptr(text), + ) + return ret != 0 +} + +func (p PopupMenu) Check(id uintptr, checked bool) bool { + return Menu(p).Check(id, checked) +} + +func (m Menu) Check(id uintptr, check bool) bool { + var checkState uint = MF_UNCHECKED + if check { + checkState = MF_CHECKED + } + return CheckMenuItem(HMENU(m), id, checkState) != 0 +} + +func (m Menu) CheckRadio(startID int, endID int, selectedID int) bool { + ret, _, _ := procCheckMenuRadioItem.Call( + uintptr(m), + uintptr(startID), + uintptr(endID), + uintptr(selectedID), + MF_BYCOMMAND) + return ret != 0 +} + +func CheckMenuItem(menu HMENU, id uintptr, flags uint) uint { + ret, _, _ := procCheckMenuItem.Call( + uintptr(menu), + id, + uintptr(flags), + ) + return uint(ret) +} + +func (p PopupMenu) CheckRadio(startID, endID, selectedID int) bool { + return Menu(p).CheckRadio(startID, endID, selectedID) +} diff --git a/v2/internal/platform/win32/structs.go b/v2/internal/platform/win32/structs.go new file mode 100644 index 000000000..a0772d004 --- /dev/null +++ b/v2/internal/platform/win32/structs.go @@ -0,0 +1,54 @@ +package win32 + +type NOTIFYICONDATA struct { + CbSize uint32 + HWnd HWND + UID uint32 + UFlags uint32 + UCallbackMessage uint32 + HIcon HICON + SzTip [128]uint16 + DwState uint32 + DwStateMask uint32 + SzInfo [256]uint16 + UVersion uint32 + SzInfoTitle [64]uint16 + DwInfoFlags uint32 + GuidItem GUID + HBalloonIcon HICON +} + +type GUID struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4 [8]byte +} + +type WNDCLASSEX struct { + CbSize uint32 + Style uint32 + LpfnWndProc uintptr + CbClsExtra int32 + CbWndExtra int32 + HInstance HINSTANCE + HIcon HICON + HCursor HCURSOR + HbrBackground HBRUSH + LpszMenuName *uint16 + LpszClassName *uint16 + HIconSm HICON +} + +type MSG struct { + HWnd HWND + Message uint32 + WParam uintptr + LParam uintptr + Time uint32 + Pt POINT +} + +type POINT struct { + X, Y int32 +} diff --git a/v2/internal/platform/win32/theme.go b/v2/internal/platform/win32/theme.go new file mode 100644 index 000000000..6afb0ba67 --- /dev/null +++ b/v2/internal/platform/win32/theme.go @@ -0,0 +1,189 @@ +package win32 + +import ( + "golang.org/x/sys/windows/registry" + "unsafe" +) + +type DWMWINDOWATTRIBUTE int32 + +const DwmwaUseImmersiveDarkModeBefore20h1 DWMWINDOWATTRIBUTE = 19 +const DwmwaUseImmersiveDarkMode DWMWINDOWATTRIBUTE = 20 +const DwmwaBorderColor DWMWINDOWATTRIBUTE = 34 +const DwmwaCaptionColor DWMWINDOWATTRIBUTE = 35 +const DwmwaTextColor DWMWINDOWATTRIBUTE = 36 +const DwmwaSystemBackdropType DWMWINDOWATTRIBUTE = 38 + +const SPI_GETHIGHCONTRAST = 0x0042 +const HCF_HIGHCONTRASTON = 0x00000001 +const WCA_ACCENT_POLICY WINDOWCOMPOSITIONATTRIB = 19 + +type ACCENT_STATE DWORD + +const ( + ACCENT_DISABLED ACCENT_STATE = 0 + ACCENT_ENABLE_GRADIENT ACCENT_STATE = 1 + ACCENT_ENABLE_TRANSPARENTGRADIENT ACCENT_STATE = 2 + ACCENT_ENABLE_BLURBEHIND ACCENT_STATE = 3 + ACCENT_ENABLE_ACRYLICBLURBEHIND ACCENT_STATE = 4 // RS4 1803 + ACCENT_ENABLE_HOSTBACKDROP ACCENT_STATE = 5 // RS5 1809 + ACCENT_INVALID_STATE ACCENT_STATE = 6 +) + +type ACCENT_POLICY struct { + AccentState ACCENT_STATE + AccentFlags DWORD + GradientColor DWORD + AnimationId DWORD +} + +type WINDOWCOMPOSITIONATTRIBDATA struct { + Attrib WINDOWCOMPOSITIONATTRIB + PvData unsafe.Pointer + CbData uintptr +} + +type WINDOWCOMPOSITIONATTRIB DWORD + +// BackdropType defines the type of translucency we wish to use +type BackdropType int32 + +const ( + BackdropTypeAuto BackdropType = 0 + BackdropTypeNone BackdropType = 1 + BackdropTypeMica BackdropType = 2 + BackdropTypeAcrylic BackdropType = 3 + BackdropTypeTabbed BackdropType = 4 +) + +func dwmSetWindowAttribute(hwnd HWND, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute unsafe.Pointer, cbAttribute uintptr) { + ret, _, err := procDwmSetWindowAttribute.Call( + uintptr(hwnd), + uintptr(dwAttribute), + uintptr(pvAttribute), + cbAttribute) + if ret != 0 { + _ = err + // println(err.Error()) + } +} + +func SupportsThemes() bool { + // We can't support Windows versions before 17763 + return IsWindowsVersionAtLeast(10, 0, 17763) +} + +func SupportsCustomThemes() bool { + return IsWindowsVersionAtLeast(10, 0, 17763) +} + +func SupportsBackdropTypes() bool { + return IsWindowsVersionAtLeast(10, 0, 22621) +} + +func SupportsImmersiveDarkMode() bool { + return IsWindowsVersionAtLeast(10, 0, 18985) +} + +func SetTheme(hwnd HWND, useDarkMode bool) { + if SupportsThemes() { + attr := DwmwaUseImmersiveDarkModeBefore20h1 + if SupportsImmersiveDarkMode() { + attr = DwmwaUseImmersiveDarkMode + } + var winDark int32 + if useDarkMode { + winDark = 1 + } + dwmSetWindowAttribute(hwnd, attr, unsafe.Pointer(&winDark), unsafe.Sizeof(winDark)) + } +} + +func EnableBlurBehind(hwnd HWND) { + var accent = ACCENT_POLICY{ + AccentState: ACCENT_ENABLE_ACRYLICBLURBEHIND, + AccentFlags: 0x2, + } + var data WINDOWCOMPOSITIONATTRIBDATA + data.Attrib = WCA_ACCENT_POLICY + data.PvData = unsafe.Pointer(&accent) + data.CbData = unsafe.Sizeof(accent) + + SetWindowCompositionAttribute(hwnd, &data) +} + +func SetWindowCompositionAttribute(hwnd HWND, data *WINDOWCOMPOSITIONATTRIBDATA) bool { + if procSetWindowCompositionAttribute != nil { + ret, _, _ := procSetWindowCompositionAttribute.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(data)), + ) + return ret != 0 + } + return false +} + +func EnableTranslucency(hwnd HWND, backdrop BackdropType) { + if SupportsBackdropTypes() { + dwmSetWindowAttribute(hwnd, DwmwaSystemBackdropType, unsafe.Pointer(&backdrop), unsafe.Sizeof(backdrop)) + } else { + println("Warning: Translucency type unavailable on Windows < 22621") + } +} + +func SetTitleBarColour(hwnd HWND, titleBarColour int32) { + dwmSetWindowAttribute(hwnd, DwmwaCaptionColor, unsafe.Pointer(&titleBarColour), unsafe.Sizeof(titleBarColour)) +} + +func SetTitleTextColour(hwnd HWND, titleTextColour int32) { + dwmSetWindowAttribute(hwnd, DwmwaTextColor, unsafe.Pointer(&titleTextColour), unsafe.Sizeof(titleTextColour)) +} + +func SetBorderColour(hwnd HWND, titleBorderColour int32) { + dwmSetWindowAttribute(hwnd, DwmwaBorderColor, unsafe.Pointer(&titleBorderColour), unsafe.Sizeof(titleBorderColour)) +} + +func SetWindowTheme(hwnd HWND, appName string, subIdList string) uintptr { + var subID uintptr + if subIdList != "" { + subID = MustStringToUTF16uintptr(subIdList) + } + ret, _, _ := procSetWindowTheme.Call( + uintptr(hwnd), + MustStringToUTF16uintptr(appName), + subID, + ) + + return ret +} +func IsCurrentlyDarkMode() bool { + key, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE) + if err != nil { + return false + } + defer key.Close() + + AppsUseLightTheme, _, err := key.GetIntegerValue("AppsUseLightTheme") + if err != nil { + return false + } + return AppsUseLightTheme == 0 +} + +type highContrast struct { + CbSize uint32 + DwFlags uint32 + LpszDefaultScheme *int16 +} + +func IsCurrentlyHighContrastMode() bool { + var result highContrast + result.CbSize = uint32(unsafe.Sizeof(result)) + res, _, err := procSystemParametersInfo.Call(SPI_GETHIGHCONTRAST, uintptr(result.CbSize), uintptr(unsafe.Pointer(&result)), 0) + if res == 0 { + _ = err + return false + } + r := result.DwFlags&HCF_HIGHCONTRASTON == HCF_HIGHCONTRASTON + return r +} diff --git a/v2/internal/platform/win32/window.go b/v2/internal/platform/win32/window.go new file mode 100644 index 000000000..7cf15cadb --- /dev/null +++ b/v2/internal/platform/win32/window.go @@ -0,0 +1,137 @@ +package win32 + +import ( + "fmt" + "github.com/samber/lo" + "golang.org/x/sys/windows" + "syscall" + "unsafe" +) + +func LoadIconWithResourceID(instance HINSTANCE, res uintptr) HICON { + ret, _, _ := procLoadIcon.Call( + uintptr(instance), + res) + + return HICON(ret) +} + +func LoadCursorWithResourceID(instance HINSTANCE, res uintptr) HCURSOR { + ret, _, _ := procLoadCursor.Call( + uintptr(instance), + res) + + return HCURSOR(ret) +} + +func RegisterClassEx(wndClassEx *WNDCLASSEX) ATOM { + ret, _, _ := procRegisterClassEx.Call(uintptr(unsafe.Pointer(wndClassEx))) + return ATOM(ret) +} + +func RegisterClass(className string, wndproc uintptr, instance HINSTANCE) error { + classNamePtr, err := syscall.UTF16PtrFromString(className) + if err != nil { + return err + } + icon := LoadIconWithResourceID(instance, IDI_APPLICATION) + + var wc WNDCLASSEX + wc.CbSize = uint32(unsafe.Sizeof(wc)) + wc.Style = CS_HREDRAW | CS_VREDRAW + wc.LpfnWndProc = wndproc + wc.HInstance = instance + wc.HbrBackground = COLOR_WINDOW + 1 + wc.HIcon = icon + wc.HCursor = LoadCursorWithResourceID(0, IDC_ARROW) + wc.LpszClassName = classNamePtr + wc.LpszMenuName = nil + wc.HIconSm = icon + + if ret := RegisterClassEx(&wc); ret == 0 { + return syscall.GetLastError() + } + + return nil +} + +func CreateWindow(className string, instance HINSTANCE, parent HWND, exStyle, style uint) HWND { + + classNamePtr := lo.Must(syscall.UTF16PtrFromString(className)) + + result := CreateWindowEx( + exStyle, + classNamePtr, + nil, + style, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + parent, + 0, + instance, + nil) + + if result == 0 { + errStr := fmt.Sprintf("Error occurred in CreateWindow(%s, %v, %d, %d)", className, parent, exStyle, style) + panic(errStr) + } + + return result +} + +func CreateWindowEx(exStyle uint, className, windowName *uint16, + style uint, x, y, width, height int, parent HWND, menu HMENU, + instance HINSTANCE, param unsafe.Pointer) HWND { + ret, _, _ := procCreateWindowEx.Call( + uintptr(exStyle), + uintptr(unsafe.Pointer(className)), + uintptr(unsafe.Pointer(windowName)), + uintptr(style), + uintptr(x), + uintptr(y), + uintptr(width), + uintptr(height), + uintptr(parent), + uintptr(menu), + uintptr(instance), + uintptr(param)) + + return HWND(ret) +} + +func MustStringToUTF16Ptr(input string) *uint16 { + ret, err := syscall.UTF16PtrFromString(input) + if err != nil { + panic(err) + } + return ret +} + +func MustStringToUTF16uintptr(input string) uintptr { + ret, err := syscall.UTF16PtrFromString(input) + if err != nil { + panic(err) + } + return uintptr(unsafe.Pointer(ret)) +} + +func MustUTF16FromString(input string) []uint16 { + ret, err := syscall.UTF16FromString(input) + if err != nil { + panic(err) + } + return ret +} + +func UTF16PtrToString(input uintptr) string { + return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(input))) +} + +func SetForegroundWindow(wnd HWND) bool { + ret, _, _ := procSetForegroundWindow.Call( + uintptr(wnd), + ) + return ret != 0 +} diff --git a/v2/pkg/application/application.go b/v2/pkg/application/application.go index 205ef6bfb..03d98ebd7 100644 --- a/v2/pkg/application/application.go +++ b/v2/pkg/application/application.go @@ -1,10 +1,12 @@ package application import ( + "context" "github.com/wailsapp/wails/v2/internal/app" "github.com/wailsapp/wails/v2/internal/signal" "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/options" + "sync" ) // Application is the main Wails application @@ -12,8 +14,13 @@ type Application struct { application *app.App options *options.App + // System Trays + systemTrays []*SystemTray + // running flag running bool + + shutdown sync.Once } // NewWithOptions creates a new Application with the given options @@ -46,6 +53,10 @@ func (a *Application) SetApplicationMenu(appMenu *menu.Menu) { // Run starts the application func (a *Application) Run() error { + for _, systemtray := range a.systemTrays { + go systemtray.run() + } + err := applicationInit() if err != nil { return err @@ -66,10 +77,44 @@ func (a *Application) Run() error { a.running = true - return a.application.Run() + err = a.application.Run() + a.Quit() + return err } // Quit will shut down the application func (a *Application) Quit() { - a.application.Shutdown() + a.shutdown.Do(func() { + for _, systray := range a.systemTrays { + systray.Close() + } + a.application.Shutdown() + }) +} + +// Bind the given struct to the application +func (a *Application) Bind(boundStruct any) { + a.options.Bind = append(a.options.Bind, boundStruct) +} + +func (a *Application) On(eventType EventType, callback func()) { + + c := func(ctx context.Context) { + callback() + } + + switch eventType { + case StartUp: + a.options.OnStartup = c + case ShutDown: + a.options.OnShutdown = c + case DomReady: + a.options.OnDomReady = c + } +} + +func (a *Application) NewSystemTray(options *options.SystemTray) *SystemTray { + systemTray := newSystemTray(options) + a.systemTrays = append(a.systemTrays, systemTray) + return systemTray } diff --git a/v2/pkg/application/events.go b/v2/pkg/application/events.go new file mode 100644 index 000000000..3896e9e75 --- /dev/null +++ b/v2/pkg/application/events.go @@ -0,0 +1,9 @@ +package application + +type EventType int + +const ( + StartUp EventType = iota + ShutDown + DomReady +) diff --git a/v2/pkg/application/systray.go b/v2/pkg/application/systray.go new file mode 100644 index 000000000..9798ef5f3 --- /dev/null +++ b/v2/pkg/application/systray.go @@ -0,0 +1,151 @@ +package application + +import ( + "github.com/wailsapp/wails/v2/internal/platform" + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/options" +) + +// SystemTray defines a system tray! +type SystemTray struct { + title string + hidden bool + lightModeIcon *options.SystemTrayIcon + darkModeIcon *options.SystemTrayIcon + tooltip string + startHidden bool + menu *menu.Menu + onLeftClick func() + onRightClick func() + onLeftDoubleClick func() + onRightDoubleClick func() + onMenuClose func() + onMenuOpen func() + + // The platform specific implementation + impl platform.SysTray +} + +func newSystemTray(options *options.SystemTray) *SystemTray { + return &SystemTray{ + title: options.Title, + lightModeIcon: options.LightModeIcon, + darkModeIcon: options.DarkModeIcon, + tooltip: options.Tooltip, + startHidden: options.StartHidden, + menu: options.Menu, + onLeftClick: options.OnLeftClick, + onRightClick: options.OnRightClick, + onLeftDoubleClick: options.OnLeftDoubleClick, + onRightDoubleClick: options.OnRightDoubleClick, + onMenuOpen: options.OnMenuOpen, + onMenuClose: options.OnMenuClose, + } +} + +func (t *SystemTray) run() { + t.impl = platform.NewSysTray() + t.impl.SetTitle(t.title) + t.impl.SetIcons(t.lightModeIcon, t.darkModeIcon) + t.impl.SetTooltip(t.tooltip) + t.impl.OnLeftClick(t.onLeftClick) + t.impl.OnRightClick(t.onRightClick) + t.impl.OnLeftDoubleClick(t.onLeftDoubleClick) + t.impl.OnRightDoubleClick(t.onRightDoubleClick) + t.impl.OnMenuOpen(t.onMenuOpen) + t.impl.OnMenuClose(t.onMenuClose) + if !t.startHidden { + t.impl.Show() + } + t.impl.SetMenu(t.menu) + t.impl.Run() +} + +func (t *SystemTray) SetTitle(title string) { + if t.impl != nil { + t.impl.SetTitle(title) + } else { + t.title = title + } +} + +func (t *SystemTray) Run() error { + t.run() + return nil +} + +func (t *SystemTray) Close() { + if t.impl != nil { + t.impl.Close() + t.impl = nil + } +} + +func (t *SystemTray) SetMenu(items *menu.Menu) { + if t.impl != nil { + t.impl.SetMenu(t.menu) + } else { + t.menu = items + } +} + +func (t *SystemTray) Update() error { + if t.impl != nil { + return t.impl.Update() + } + return nil +} + +func (t *SystemTray) SetTooltip(s string) { + if t.impl != nil { + t.impl.SetTooltip(s) + } else { + t.tooltip = s + } +} + +func (t *SystemTray) SetIcons(lightModeIcon *options.SystemTrayIcon, darkModeIcon *options.SystemTrayIcon) { + if t.impl != nil { + t.impl.SetIcons(lightModeIcon, darkModeIcon) + } else { + t.lightModeIcon = lightModeIcon + t.darkModeIcon = darkModeIcon + } + +} + +func (t *SystemTray) OnLeftClick(fn func()) { + if t.impl != nil { + t.impl.OnLeftClick(fn) + } +} + +func (t *SystemTray) OnRightClick(fn func()) { + if t.impl != nil { + t.impl.OnRightClick(fn) + } +} + +func (t *SystemTray) OnLeftDoubleClick(fn func()) { + if t.impl != nil { + t.impl.OnLeftDoubleClick(fn) + } +} + +func (t *SystemTray) OnRightDoubleClick(fn func()) { + if t.impl != nil { + t.impl.OnRightDoubleClick(fn) + } +} + +func (t *SystemTray) OnMenuOpen(fn func()) { + if t.impl != nil { + t.impl.OnMenuOpen(fn) + } +} + +func (t *SystemTray) OnMenuClose(fn func()) { + if t.impl != nil { + t.impl.OnMenuClose(fn) + } +} diff --git a/v2/pkg/events/events.go b/v2/pkg/events/events.go new file mode 100644 index 000000000..40727ba1a --- /dev/null +++ b/v2/pkg/events/events.go @@ -0,0 +1,3 @@ +package events + +const ThemeChanged = ":wails:themechanged" diff --git a/v2/pkg/menu/menu.go b/v2/pkg/menu/menu.go index 0c3ddb618..c7ad7caf9 100644 --- a/v2/pkg/menu/menu.go +++ b/v2/pkg/menu/menu.go @@ -44,16 +44,23 @@ func (m *Menu) AddRadio(label string, checked bool, accelerator *keys.Accelerato } // AddSeparator adds a separator to the menu -func (m *Menu) AddSeparator() { +func (m *Menu) AddSeparator() *MenuItem { item := Separator() m.Append(item) + return item } -func (m *Menu) AddSubmenu(label string) *Menu { +func (m *Menu) AddSubmenu(label string) *MenuItem { submenu := NewMenu() item := SubMenu(label, submenu) m.Append(item) - return submenu + return item +} + +func (m *Menu) InsertSubmenu(label string, submenu *Menu) *MenuItem { + item := SubMenu(label, submenu) + m.Append(item) + return item } func (m *Menu) Prepend(item *MenuItem) { diff --git a/v2/pkg/menu/menuitem.go b/v2/pkg/menu/menuitem.go index ba9574eb3..f6ea681d7 100644 --- a/v2/pkg/menu/menuitem.go +++ b/v2/pkg/menu/menuitem.go @@ -216,6 +216,70 @@ func (m *MenuItem) insertItemAtIndex(index int, target *MenuItem) bool { return true } +func (m *MenuItem) SetLabel(name string) { + if m.Label == name { + return + } + m.Label = name +} + +func (m *MenuItem) IsSeparator() bool { + return m.Type == SeparatorType +} + +func (m *MenuItem) IsCheckbox() bool { + return m.Type == CheckboxType +} + +func (m *MenuItem) Disable() *MenuItem { + m.Disabled = true + return m +} + +func (m *MenuItem) Enable() *MenuItem { + m.Disabled = false + return m +} + +func (m *MenuItem) OnClick(click Callback) *MenuItem { + m.Click = click + return m +} + +func (m *MenuItem) SetAccelerator(acc *keys.Accelerator) *MenuItem { + m.Accelerator = acc + return m +} + +func (m *MenuItem) SetChecked(value bool) *MenuItem { + m.Checked = value + if m.Type != RadioType { + m.Type = CheckboxType + } + return m +} + +func (m *MenuItem) Hide() *MenuItem { + m.Hidden = true + return m +} + +func (m *MenuItem) Show() *MenuItem { + m.Hidden = false + return m +} + +func (m *MenuItem) IsRadio() bool { + return m.Type == RadioType +} + +func Label(label string) *MenuItem { + return &MenuItem{ + Type: TextType, + Label: label, + } +} + // Text is a helper to create basic Text menu items func Text(label string, accelerator *keys.Accelerator, click Callback) *MenuItem { return &MenuItem{ diff --git a/v2/pkg/menu/tray.go b/v2/pkg/menu/tray.go index 7554795ad..1c8ec07b4 100644 --- a/v2/pkg/menu/tray.go +++ b/v2/pkg/menu/tray.go @@ -1,20 +1,95 @@ package menu +import ( + "context" + "log" + goruntime "runtime" + + "github.com/wailsapp/wails/v2/pkg/events" +) + +type TrayMenuAdd interface { + TrayMenuAdd(menu *TrayMenu) TrayMenuImpl +} + +type TrayMenuImpl interface { + SetLabel(string) + SetImage(*TrayImage) + SetMenu(*Menu) +} + +type EventsImpl interface { + On(eventName string, callback func(...interface{})) +} + +type ImagePosition int + +const ( + ImageLeading ImagePosition = 0 + ImageOnly ImagePosition = 1 + ImageLeft ImagePosition = 2 + ImageRight ImagePosition = 3 + ImageBelow ImagePosition = 4 + ImageAbove ImagePosition = 5 + ImageOverlaps ImagePosition = 6 + NoImage ImagePosition = 7 + ImageTrailing ImagePosition = 8 +) + +type TraySizing int + +const ( + Variable TraySizing = 0 + Square TraySizing = 1 +) + +type TrayImage struct { + // Bitmaps hold images for different scaling factors + // First = 1x, Second = 2x, etc + Bitmaps [][]byte + BitmapsDark [][]byte + IsTemplate bool + Position ImagePosition +} + +func (t *TrayImage) getBestBitmap(scale int, isDarkMode bool) []byte { + bitmapsToCheck := t.Bitmaps + if isDarkMode { + bitmapsToCheck = t.BitmapsDark + } + if scale < 1 || scale >= len(bitmapsToCheck) { + return nil + } + for i := scale; i > 0; i-- { + if bitmapsToCheck[i] != nil { + return bitmapsToCheck[i] + } + } + return nil +} + +// GetBestBitmap will attempt to return the best bitmap for the theme +// If dark theme is used and no dark theme bitmap exists, then it will +// revert to light theme bitmaps +func (t *TrayImage) GetBestBitmap(scale int, isDarkMode bool) []byte { + var result []byte + if isDarkMode { + result = t.getBestBitmap(scale, true) + if result != nil { + return result + } + } + return t.getBestBitmap(scale, false) +} + // TrayMenu are the options type TrayMenu struct { + ctx context.Context // Label is the text we wish to display in the tray Label string - // Image is the name of the tray icon we wish to display. - // These are read up during build from /trayicons and - // the filenames are used as IDs, minus the extension - // EG: /trayicons/main.png can be referenced here with "main" - // If the image is not a filename, it will be treated as base64 image data - Image string - - // MacTemplateImage indicates that on a Mac, this image is a template image - MacTemplateImage bool + Image *TrayImage // Text Colour RGBA string @@ -27,7 +102,7 @@ type TrayMenu struct { Tooltip string // Callback function when menu clicked - //Click Callback `json:"-"` + Click Callback // Disabled makes the item unselectable Disabled bool @@ -40,4 +115,67 @@ type TrayMenu struct { // OnClose is called when the Menu is closed OnClose func() + + /* Mac Options */ + Sizing TraySizing + + // This is the reference to the OS specific implementation + impl TrayMenuImpl + + // Theme change callback + themeChangeCallback func(data ...interface{}) +} + +func NewTrayMenu() *TrayMenu { + return &TrayMenu{} +} + +func (t *TrayMenu) Show(ctx context.Context) { + if ctx == nil { + log.Fatal("TrayMenu.Show() called before Run()") + } + t.ctx = ctx + result := ctx.Value("frontend") + if result == nil { + pc, _, _, _ := goruntime.Caller(1) + funcName := goruntime.FuncForPC(pc).Name() + log.Fatalf("invalid context at '%s'", funcName) + } + t.impl = result.(TrayMenuAdd).TrayMenuAdd(t) + + if t.themeChangeCallback == nil { + t.themeChangeCallback = func(data ...interface{}) { + println("Update button image") + if t.Image != nil { + // Update the image + t.SetImage(t.Image) + } + } + result := ctx.Value("events") + if result != nil { + result.(EventsImpl).On(events.ThemeChanged, t.themeChangeCallback) + } + } + +} + +func (t *TrayMenu) SetLabel(label string) { + t.Label = label + if t.impl != nil { + t.impl.SetLabel(label) + } +} + +func (t *TrayMenu) SetImage(image *TrayImage) { + t.Image = image + if t.impl != nil { + t.impl.SetImage(image) + } +} + +func (t *TrayMenu) SetMenu(menu *Menu) { + t.Menu = menu + if t.impl != nil { + t.impl.SetMenu(menu) + } } diff --git a/v2/pkg/options/mac/mac.go b/v2/pkg/options/mac/mac.go index 033cfd1a2..2f574607f 100644 --- a/v2/pkg/options/mac/mac.go +++ b/v2/pkg/options/mac/mac.go @@ -1,12 +1,12 @@ package mac -//type ActivationPolicy int -// -//const ( -// NSApplicationActivationPolicyRegular ActivationPolicy = 0 -// NSApplicationActivationPolicyAccessory ActivationPolicy = 1 -// NSApplicationActivationPolicyProhibited ActivationPolicy = 2 -//) +type ActivationPolicy int + +const ( + NSApplicationActivationPolicyRegular ActivationPolicy = 0 + NSApplicationActivationPolicyAccessory ActivationPolicy = 1 + NSApplicationActivationPolicyProhibited ActivationPolicy = 2 +) type AboutInfo struct { Title string @@ -20,7 +20,7 @@ type Options struct { Appearance AppearanceType WebviewIsTransparent bool WindowIsTranslucent bool - //ActivationPolicy ActivationPolicy - About *AboutInfo + About *AboutInfo + ActivationPolicy ActivationPolicy //URLHandlers map[string]func(string) } diff --git a/v2/pkg/options/systemtray.go b/v2/pkg/options/systemtray.go new file mode 100644 index 000000000..117abb4d6 --- /dev/null +++ b/v2/pkg/options/systemtray.go @@ -0,0 +1,26 @@ +package options + +import ( + "github.com/wailsapp/wails/v2/pkg/menu" +) + +// SystemTray contains options for the system tray +type SystemTray struct { + LightModeIcon *SystemTrayIcon + DarkModeIcon *SystemTrayIcon + Title string + Tooltip string + StartHidden bool + Menu *menu.Menu + OnLeftClick func() + OnRightClick func() + OnLeftDoubleClick func() + OnRightDoubleClick func() + OnMenuClose func() + OnMenuOpen func() +} + +// SystemTrayIcon represents a system tray icon +type SystemTrayIcon struct { + Data []byte +} diff --git a/website/docs/reference/menus.mdx b/website/docs/reference/menus.mdx index 1c2d3f5b7..05038b960 100644 --- a/website/docs/reference/menus.mdx +++ b/website/docs/reference/menus.mdx @@ -80,17 +80,28 @@ type MenuItem struct { } ``` -| Field | Type | Notes | -| ----------- | ---------------------------------- | ------------------------------------------------------------- | -| Label | string | The menu text | -| Accelerator | [\*keys.Accelerator](#accelerator) | Key binding for this menu item | -| Type | [Type](#type) | Type of MenuItem | -| Disabled | bool | Disables the menu item | -| Hidden | bool | Hides this menu item | -| Checked | bool | Adds check to item (Checkbox & Radio types) | -| SubMenu | [\*Menu](#menu) | Sets the submenu | -| Click | [Callback](#callback) | Callback function when menu clicked | -| Role | string | Defines a [role](#role) for this menu item. Mac only for now. | +| Field | Type | Notes | +| ----------- | ---------------------------------- |------------------------------------------------------------------------------------------| +| Label | string | The menu text | +| Accelerator | [\*keys.Accelerator](#accelerator) | Key binding for this menu item | +| Type | [Type](#type) | Type of MenuItem | +| Disabled | bool | Disables the menu item | +| Hidden | bool | Hides this menu item | +| Checked | bool | Adds check to item (Checkbox & Radio types). Updated automatically when item is clicked. | +| SubMenu | [\*Menu](#menu) | Sets the submenu | +| Click | [Callback](#callback) | Callback function when menu clicked | +| Role | string | Defines a [role](#role) for this menu item. Mac only for now. | + +### Keeping menu items in sync + +A menuitem can be reused in multiple menus. This is useful for keeping menu items in sync. For example, if you have a +"Quit" menu item, you can add it to both the Application menu and a system tray menu. When the user clicks the menu item, +the callback will be called only once. + +For checkbox and radio menu items, the state of the menu item will be kept in sync. For example, if you have a "Dark Mode" +menu item in the Application menu and a system tray menu, when the user clicks the menu item in the Application menu, the +state of the menu item in the system tray menu will also be updated. For radio menu items that are used in multiple places, +clicking one will uncheck all others in all groups that the menu item is used in. ### Accelerator @@ -104,7 +115,7 @@ Example: myShortcut := keys.CmdOrCtrl("o") ``` -Keys are any single character on a keyboard with the exception of `+`, which is defined as `plus`. +Keys are any single character on a keyboard except for `+`, which is defined as `plus`. Some keys cannot be represented as characters so there are a set of named characters that may be used: | | | | | @@ -219,7 +230,7 @@ type CallbackData struct { ``` The function is given a `CallbackData` struct which indicates which menu item triggered the callback. This is useful when -using radio groups that may share a callback. +using radio groups that may share a callback. If the menuitem is a checkbox, the `Checked` property will be set to the new value of the checkbox. ### Role diff --git a/website/static/img/varly1.png b/website/static/img/varly1.png new file mode 100644 index 000000000..0dac80579 Binary files /dev/null and b/website/static/img/varly1.png differ