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..e7819a7e8 --- /dev/null +++ b/v2/examples/systray/build/darwin/Info.plist @@ -0,0 +1,27 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.Name}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + 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/dist/gitkeep b/v2/examples/systray/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb 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..f624d426f --- /dev/null +++ b/v2/examples/systray/go.mod @@ -0,0 +1,34 @@ +module github.com/wailsapp/examples/systray + +go 1.18 + +require github.com/wailsapp/wails/v2 v2.2.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/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.2.0 => ../.. diff --git a/v2/examples/systray/go.sum b/v2/examples/systray/go.sum new file mode 100644 index 000000000..9f577485c --- /dev/null +++ b/v2/examples/systray/go.sum @@ -0,0 +1,79 @@ +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.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +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/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.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/internal/platform/menu/manager.go b/v2/internal/platform/menu/manager.go new file mode 100644 index 000000000..0ddbc9dde --- /dev/null +++ b/v2/internal/platform/menu/manager.go @@ -0,0 +1,147 @@ +//go:build windows + +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..9e014b3ee --- /dev/null +++ b/v2/internal/platform/menu/manager_test.go @@ -0,0 +1,297 @@ +//go:build windows + +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..a74c191bb --- /dev/null +++ b/v2/internal/platform/systray.go @@ -0,0 +1,33 @@ +//go:build windows + +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..cad2a7293 --- /dev/null +++ b/v2/internal/platform/win32/consts.go @@ -0,0 +1,858 @@ +//go:build windows + +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..04449a91b --- /dev/null +++ b/v2/internal/platform/win32/cursor.go @@ -0,0 +1,11 @@ +//go:build windows + +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..916b92d44 --- /dev/null +++ b/v2/internal/platform/win32/icon.go @@ -0,0 +1,41 @@ +//go:build windows + +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..7a86d6643 --- /dev/null +++ b/v2/internal/platform/win32/keyboard.go @@ -0,0 +1,810 @@ +//go:build windows + +/* + * 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..f05886414 --- /dev/null +++ b/v2/internal/platform/win32/menu.go @@ -0,0 +1,82 @@ +//go:build windows + +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..3f79d8585 --- /dev/null +++ b/v2/internal/platform/win32/structs.go @@ -0,0 +1,51 @@ +//go:build windows + +package win32 + +import "golang.org/x/sys/windows" + +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 windows.GUID + HBalloonIcon HICON +} + +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..ad29b1201 --- /dev/null +++ b/v2/internal/platform/win32/theme.go @@ -0,0 +1,191 @@ +//go:build windows + +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..0ca31ecee --- /dev/null +++ b/v2/internal/platform/win32/window.go @@ -0,0 +1,139 @@ +//go:build windows + +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/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/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 +}