Compare commits

...

60 commits

Author SHA1 Message Date
Lea Anthony
4b11bed165
Address some issues in Code Review 2022-11-25 14:40:30 +11:00
Lea Anthony
f469d3e1cf
Merge branch 'master' into feature/1521_Support_Tray_Menus 2022-11-25 07:17:04 +11:00
Lea Anthony
78e3132d66
Merge branch 'master' into feature/1521_Support_Tray_Menus 2022-10-26 19:50:30 +11:00
Lea Anthony
ca07910365
Add support for double click mouse button events and menu open/close events. Updated example. 2022-10-23 17:27:00 +11:00
Lea Anthony
6dac02923c
Merge branch 'master' into feature/1521_Support_Tray_Menus 2022-10-23 10:11:27 +11:00
Lea Anthony
58be270cd3
Merge branch 'master' into feature/1521_Support_Tray_Menus 2022-10-22 21:08:33 +11:00
Lea Anthony
7969419052
Merge branch 'master' into feature/1521_Support_Tray_Menus 2022-10-22 20:35:23 +11:00
Lea Anthony
d7db4c1522
Merge branch 'master' into feature/1521_Support_Tray_Menus 2022-10-15 20:30:16 +11:00
Lea Anthony
97ca8387af
Merge branch 'master' into feature/1521_Support_Tray_Menus 2022-10-13 20:11:11 +11:00
Lea Anthony
312f1a61a5
Merge branch 'master' into feature/1521_Support_Tray_Menus 2022-10-12 21:15:55 +11:00
Lea Anthony
62c19f128e
Merge branch 'master' into feature/1521_Support_Tray_Menus 2022-10-12 21:04:52 +11:00
Lea Anthony
804816dd6f
Update example 2022-10-12 20:59:02 +11:00
Lea Anthony
4fe1aff55e
Fix menuID bug 2022-10-12 20:54:05 +11:00
Lea Anthony
3ffb92d6e3
Fix radio group bug when group is last in menu 2022-10-12 20:46:34 +11:00
Lea Anthony
cb879d7273
Update example 2022-10-12 07:21:33 +11:00
Lea Anthony
c767c75109
Update example 2022-10-11 21:36:22 +11:00
Lea Anthony
72a0db3938
Add translucency effects 2022-10-11 21:34:54 +11:00
Lea Anthony
7ef6f7d2b9
Fix bug in runtime.Show(). Tidy up debug. 2022-10-11 21:17:55 +11:00
Lea Anthony
2f99ef7c94
Add example 2022-10-11 21:09:59 +11:00
Lea Anthony
828ebd8fb6
Support left/right click override 2022-10-11 21:07:01 +11:00
Lea Anthony
bca58afc04
Remove accelerator code, but leave text in menu 2022-10-11 19:45:43 +11:00
Lea Anthony
bd0e3b407a
Add accelerator code 2022-10-11 19:43:22 +11:00
Lea Anthony
5adf8004ac
Support Radio Groups 2022-10-09 20:09:31 +11:00
Lea Anthony
c38e0ea84b
Use MenuManager for processing and handling menu state changes 2022-10-09 12:26:05 +11:00
Lea Anthony
9d6d90c3e4
Add transparency methods 2022-10-08 13:32:02 +11:00
Lea Anthony
c5ad7de9cf
Menu to follow theme 2022-10-08 10:10:39 +11:00
Lea Anthony
12a309c8a5
Update menu docs 2022-10-08 08:02:02 +11:00
Lea Anthony
5fab671af3
Support Hidden attribute. Add SetTooltip & SetIcons to SystemTray. Add Show & Hide to menuItem 2022-10-08 07:45:29 +11:00
Lea Anthony
d6f20d5f0b
menu refactor 2022-10-07 20:15:19 +11:00
Lea Anthony
b5478e52db
Support checkbox logic. Support duplicate checkboxes 2022-10-07 20:02:58 +11:00
Lea Anthony
181ba423a3
Support submenus 2022-10-06 23:13:42 +11:00
Lea Anthony
249dc81872
Fix GetMessage bug.
Refactor ShellNotifyIcon. Application shutdown once.
2022-10-06 20:30:34 +11:00
Lea Anthony
0664c51832
Fix GetMessage bug 2022-10-06 19:49:48 +11:00
Lea Anthony
abe83f2271
Support traymenu update 2022-10-06 08:21:30 +11:00
Lea Anthony
68bdc7c5eb
Support menu.Menu 2022-10-05 19:22:03 +11:00
Lea Anthony
c63f8160a6
Move systray startup into application 2022-10-05 07:15:36 +11:00
Lea Anthony
a967ce0a29
Refactor light/dark icons 2022-10-04 20:17:46 +11:00
Lea Anthony
bf088ea19c
Initial Windows systray support 2022-10-03 21:36:19 +11:00
Lea Anthony
03dd514768
Refactor app for linux 2022-10-02 17:49:41 +11:00
Lea Anthony
d319d51fcd
Refactor darwin app 2022-10-02 17:49:41 +11:00
Lea Anthony
311dde474c
[windows] refactor out main loop. Create new application struct. Refactor assethandler/assetserver signatures. 2022-10-02 17:49:41 +11:00
stffabi
cfb55f7c5e
[assetHandler] Remove redundant log prefix (#1896) 2022-10-02 17:49:40 +11:00
Arif Ali
2940a9e7ef
obfuscated instead of obfuscate in the docs (#1895)
the option in the docs is the wrong flag you have to add a d at the end
2022-10-02 17:49:40 +11:00
Misite Bao
022fd9afff
chore: update the label name in the issue template (#1893) 2022-10-02 17:49:40 +11:00
Lea Anthony
26c6881ed3
Add issue translator (#1891) 2022-10-02 17:49:34 +11:00
Lea Anthony
e5a1ddb3f3
Merge branch 'exp' into feature/1521_Support_Tray_Menus
# Conflicts:
#	.gitignore
#	v2/internal/frontend/desktop/windows/win32/consts.go
#	v2/internal/frontend/devserver/devserver.go
#	website/docs/introduction.mdx
2022-09-23 19:53:07 +10:00
Lea Anthony
823c365236
Application refactor 2022-09-23 17:29:54 +10:00
Lea Anthony
a11655c452
Merge master 2022-07-09 22:55:03 +10:00
Lea Anthony
11cbfb68f1
Merge branch 'master' into exp
# Conflicts:
#	v2/internal/frontend/desktop/windows/win32/consts.go
2022-07-09 22:50:01 +10:00
Lea Anthony
319d59296e Support loading images into Windows tray 2022-05-14 06:44:22 +10:00
Lea Anthony
368733b7cc WIP 2022-05-13 20:49:54 +10:00
Lea Anthony
ee5ca1a84a
Add SetLabel, minor api updates 2022-05-07 20:39:08 +10:00
Lea Anthony
1947a9a17a
Hugely improved + darkmode bitmap support. Dynamic scaling factor support 2022-05-05 21:15:50 +10:00
Lea Anthony
13fb5352d8
Support sizing tray menu 2022-05-03 08:31:42 +10:00
Lea Anthony
58b6e4c335
Support retina images 2022-05-02 20:16:04 +10:00
Lea Anthony
ae0933c82a
Move TrayMenu from config to runtime
Support better tray menu API
Move object creation to main thread
2022-05-02 18:57:35 +10:00
Lea Anthony
c1d4aeb3cd
Merge branch 'master' into exp 2022-05-01 20:55:41 +10:00
Lea Anthony
1830c64c79
Better Checked implementation 2022-04-30 21:39:19 +10:00
Lea Anthony
ee50170374
Support activation policy 2022-04-30 17:51:11 +10:00
Lea Anthony
3f82ceabba
Initial Tray support for Mac 2022-04-30 12:00:00 +10:00
70 changed files with 6564 additions and 308 deletions

3
.gitignore vendored
View file

@ -31,3 +31,6 @@ v2/test/kitchensink/frontend/package.json.md5
v2/cmd/wails/internal/commands/initialise/templates/testtemplates/
.env
/website/static/img/.cache.json
v2/internal/frontend/desktop/darwin/test.xcodeproj
TODO.md

15
v2/examples/README.md Normal file
View file

@ -0,0 +1,15 @@
<p align="center">
<img alt="Wails logo" src="https://github.com/wailsapp/wails/raw/master/assets/images/logo-universal.png" style="width:55%; max-width:400px"><br/>
</p>
<p align="center">
Various example applications for Wails.
</p>
## Table of Contents
- [Systray](./systray/README.md)
## Contributing
Please create a directory with your example, add a link to this README and raise a PR.

View file

@ -0,0 +1,93 @@
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

3
v2/examples/systray/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
build/bin
node_modules
frontend/wailsjs

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View file

@ -0,0 +1,14 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleName</key><string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key><string>{{.Name}}</string>
<key>CFBundleIdentifier</key><string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key><string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key><string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key><string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key><string>iconfile</string>
<key>LSMinimumSystemVersion</key><string>10.13.0</string>
<key>NSHighResolutionCapable</key><string>true</string>
<key>NSHumanReadableCopyright</key><string>{{.Info.Copyright}}</string>
</dict></plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -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}}"
}
}
}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

View file

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Systray Example</title>
<style>
html {
background-color: rgba(27, 38, 54, 0.75);
text-align: center;
color: white;
}
@font-face {
font-family: "Nunito";
font-style: normal;
font-weight: 400;
src: local(""),
url("../../common/fonts/nunito-v16-latin-regular.woff2") format("woff2");
}
body {
margin: 0;
color: white;
font-family: "Nunito", sans-serif;
}
.logo {
display: block;
width: 50%;
height: 50%;
background-position: center;
background-repeat: no-repeat;
background-image: url("../../common/images/logo-universal.png");
background-size: 100% 100%;
background-origin: content-box;
}
#app {
height: 100vh;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
align-items: center;
}
</style>
</head>
<body>
<div id="app">
<div class="logo"></div>
<p>Systray Example</p>
</div>
</body>
</html>

View file

@ -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"
}
}
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1,35 @@
module github.com/wailsapp/examples/systray
go 1.18
require github.com/wailsapp/wails/v2 v2.0.0
require (
github.com/bep/debounce v1.2.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/labstack/echo/v4 v4.9.0 // indirect
github.com/labstack/gommon v0.3.1 // indirect
github.com/leaanthony/go-ansi-parser v1.0.1 // indirect
github.com/leaanthony/gosod v1.0.3 // indirect
github.com/leaanthony/slicer v1.5.0 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/samber/lo v1.27.1 // indirect
github.com/tkrajina/go-reflector v0.5.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/text v0.3.7 // indirect
)
replace github.com/wailsapp/wails/v2 v2.0.0 => ../..

View file

@ -0,0 +1,82 @@
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4=
github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM=
github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc=
github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.27.1 h1:sTXwkRiIFIQG+G0HeAvOEnGjqWeWtI9cg5/n51KrxPg=
github.com/samber/lo v1.27.1/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ=
github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

216
v2/examples/systray/main.go Normal file
View file

@ -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())
}
}

View file

@ -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"
}
}

View file

@ -44,6 +44,7 @@ require (
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
github.com/samber/lo v1.27.1
github.com/stretchr/testify v1.7.1
golang.org/x/image v0.0.0-20201208152932-35266b937fa6
golang.org/x/tools v0.1.12
)
@ -83,7 +84,6 @@ require (
github.com/yuin/goldmark-emoji v1.0.1 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

View file

@ -17,6 +17,7 @@
@property bool startHidden;
@property bool startFullscreen;
@property (retain) WailsWindow* mainWindow;
@property int activationPolicy;
@end

View file

@ -9,6 +9,7 @@
#import <Cocoa/Cocoa.h>
#import "AppDelegate.h"
#import "message.h"
@implementation AppDelegate
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
@ -25,6 +26,8 @@
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
processNotification(0); // Notify Go
[NSApp setActivationPolicy:self.activationPolicy];
[NSApp activateIgnoringOtherApps:YES];
if ( self.startFullscreen ) {
NSWindowCollectionBehavior behaviour = [self.mainWindow collectionBehavior];

View file

@ -18,7 +18,7 @@
#define WindowStartsFullscreen 3
WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int debug, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight);
void Run(void*, const char* url);
void Run(void *inctx, const char* url, int activationPolicy);
void SetTitle(void* ctx, const char *title);
void Center(void* ctx);
@ -62,6 +62,16 @@ void AppendSubmenu(void* parent, void* child);
void AppendRole(void *inctx, void *inMenu, int role);
void SetAsApplicationMenu(void *inctx, void *inMenu);
void UpdateApplicationMenu(void *inctx);
void SetMenuItemChecked(void* nsMenuItem, int checked);
/* Tray Menu */
void NewNSStatusItem(int id, int length);
void SetTrayMenu(void *nsStatusItem, void* nsMenu);
void SetTrayMenuLabel(void *nsStatusItem, const char *label);
void SetTrayImage(void *nsStatusItem, void *imageData, int imageDataLength, int template, int position);
/* MenuItems */
void SetMenuItemLabel(void *nsStatusItem, const char *label);
void SetAbout(void *inctx, const char* title, const char* description, void* imagedata, int datalen);
void* AppendMenuItem(void* inctx, void* nsmenu, const char* label, const char* shortcutKey, int modifiers, int disabled, int checked, int menuItemID);
@ -72,4 +82,7 @@ void ReleaseContext(void *inctx);
NSString* safeInit(const char* input);
int ScalingFactor(void *ctx);
#endif /* Application_h */

View file

@ -12,6 +12,7 @@
#import "AppDelegate.h"
#import "WailsMenu.h"
#import "WailsMenuItem.h"
#import "message.h"
WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int debug, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight) {
@ -20,7 +21,7 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in
WailsContext *result = [WailsContext new];
result.debug = debug;
if ( windowStartState == WindowStartsFullscreen ) {
fullscreen = 1;
}
@ -28,7 +29,7 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in
[result CreateWindow:width :height :frameless :resizable :fullscreen :fullSizeContent :hideTitleBar :titlebarAppearsTransparent :hideTitle :useToolbar :hideToolbarSeparator :webviewIsTransparent :hideWindowOnClose :safeInit(appearance) :windowIsTranslucent :minWidth :minHeight :maxWidth :maxHeight];
[result SetTitle:safeInit(title)];
[result Center];
switch( windowStartState ) {
case WindowStartsMaximised:
[result.mainWindow zoom:nil];
@ -170,6 +171,10 @@ void ToggleMaximise(void* inctx) {
);
}
void SetMenuItemChecked(void* nsMenuItem, int checked) {
[(NSMenuItem*)nsMenuItem setState:(checked == 0 ? NSOffState : NSOnState)];
}
const char* GetSize(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSRect frame = [ctx.mainWindow frame];
@ -304,6 +309,55 @@ void AppendRole(void *inctx, void *inMenu, int role) {
[menu appendRole :ctx :role];
}
void NewNSStatusItem(int id, int length) {
dispatch_async(dispatch_get_main_queue(), ^{
NSStatusBar *statusBar = [NSStatusBar systemStatusBar];
// Map Go to Cocoa length. 0 = NSVariableStatusItemLength.
CGFloat length = NSVariableStatusItemLength;
if( length == 1 ) {
length = NSSquareStatusItemLength;
}
NSStatusItem *result = [[statusBar statusItemWithLength:length] retain];
objectCreated(id,result);
});
}
void DeleteStatusItem(void *_nsStatusItem) {
NSStatusItem *nsStatusItem = (NSStatusItem*) _nsStatusItem;
[nsStatusItem release];
}
void on_main_thread(void (^l)(void)) {
dispatch_async(dispatch_get_main_queue(), l);
}
void SetTrayMenuLabel(void *_nsStatusItem, const char *label) {
on_main_thread(^{
NSStatusItem *nsStatusItem = (NSStatusItem*) _nsStatusItem;
nsStatusItem.button.title = safeInit(label);
free((void*)label);
});
}
void SetTrayMenu(void *nsStatusItem, void* nsMenu) {
ON_MAIN_THREAD(
[(NSStatusItem*)nsStatusItem setMenu:(NSMenu *)nsMenu];
)
}
/**** Menu Item ****/
void SetMenuItemLabel(void *_nsMenuItem, const char *label) {
on_main_thread(^{
NSMenuItem *nsMenuItem = (NSMenuItem*) _nsMenuItem;
[ nsMenuItem setTitle:safeInit(label) ];
free((void*)label);
});
}
void* NewMenu(const char *name) {
NSString *title = @"";
if (name != nil) {
@ -328,8 +382,8 @@ void SetAsApplicationMenu(void *inctx, void *inMenu) {
void UpdateApplicationMenu(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
NSApplication *app = [NSApplication sharedApplication];
[app setMainMenu:ctx.applicationMenu];
NSApplication *app = [NSApplication sharedApplication];
[app setMainMenu:ctx.applicationMenu];
)
}
@ -354,7 +408,7 @@ void UpdateMenuItem(void* nsmenuitem, int checked) {
ON_MAIN_THREAD(
WailsMenuItem *menuItem = (__bridge WailsMenuItem*) nsmenuitem;
[menuItem setState:(checked == 1?NSControlStateValueOn:NSControlStateValueOff)];
)
)
}
@ -363,12 +417,39 @@ void AppendSeparator(void* inMenu) {
[menu AppendSeparator];
}
void SetTrayImage(void *nsStatusItem, void *imageData, int imageDataLength, int template, int position) {
ON_MAIN_THREAD(
NSStatusItem *statusItem = (NSStatusItem*) nsStatusItem;
NSData *nsdata = [NSData dataWithBytes:imageData length:imageDataLength];
NSImage *image = [[[NSImage alloc] initWithData:nsdata] autorelease];
if(template) {
image.template = true;
}
image.size = NSMakeSize(22.0, 22.0);
statusItem.button.image = image;
// Swap NSNoImage and NSImageLeading because we wanted NSImageLeading to be default in Go
int actualPosition = position;
if( position == 7) {
actualPosition = 0;
} else if (position == 0) {
actualPosition = 7;
}
[statusItem.button setImagePosition:actualPosition];
)
}
int ScalingFactor(void *ctx) {
CGFloat scale = [((WailsContext*)ctx).mainWindow backingScaleFactor];
return (int)scale;
}
void Run(void *inctx, const char* url) {
void Run(void *inctx, const char* url, int activationPolicy) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSApplication *app = [NSApplication sharedApplication];
AppDelegate* delegate = [AppDelegate new];
delegate.activationPolicy = activationPolicy;
[app setDelegate:(id)delegate];
ctx.appdelegate = delegate;
delegate.mainWindow = ctx.mainWindow;

View file

@ -23,6 +23,7 @@
@property NSSize userMinSize;
@property NSSize userMaxSize;
@property int activationPolicy;
- (BOOL) canBecomeKeyWindow;
- (void) applyWindowConstraints;

View file

@ -3,15 +3,6 @@
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "Application.h"
#import "WailsContext.h"
#include <stdlib.h>
*/
import "C"
import (
"bytes"
@ -31,14 +22,33 @@ import (
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/assetserver"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
)
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "Application.h"
#import "WailsContext.h"
#include <stdlib.h>
*/
import "C"
const startURL = "wails://wails/"
type NotificationType uint8
const (
ApplicationDidFinishLaunching NotificationType = 0
)
var messageBuffer = make(chan string, 100)
var requestBuffer = make(chan *request, 100)
var callbackBuffer = make(chan uint, 10)
var notificationBuffer = make(chan NotificationType, 10)
type Frontend struct {
@ -57,6 +67,19 @@ type Frontend struct {
mainWindow *Window
bindings *binding.Bindings
dispatcher frontend.Dispatcher
trayMenus map[*menu.TrayMenu]*NSTrayMenu
applicationDidFinishLaunching bool
notificationCallbacks map[NotificationType][]func()
trayMenusBuffer []*menu.TrayMenu
}
func (f *Frontend) RunMainLoop() {
C.RunMainLoop()
}
func (f *Frontend) WindowClose() {
C.ReleaseContext(f.mainWindow.context)
}
func (f *Frontend) RunMainLoop() {
@ -69,11 +92,13 @@ func (f *Frontend) WindowClose() {
func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend {
result := &Frontend{
frontendOptions: appoptions,
logger: myLogger,
bindings: appBindings,
dispatcher: dispatcher,
ctx: ctx,
frontendOptions: appoptions,
logger: myLogger,
bindings: appBindings,
dispatcher: dispatcher,
ctx: ctx,
trayMenus: make(map[*menu.TrayMenu]*NSTrayMenu),
notificationCallbacks: make(map[NotificationType][]func()),
}
result.startURL, _ = url.Parse(startURL)
@ -101,10 +126,17 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
go result.startMessageProcessor()
go result.startCallbackProcessor()
go result.startNotificationsProcessor()
result.registerNotificationCallback(ApplicationDidFinishLaunching, result.processTrayMenus)
return result
}
func (f *Frontend) registerNotificationCallback(notificationType NotificationType, callback func()) {
f.notificationCallbacks[notificationType] = append(f.notificationCallbacks[notificationType], callback)
}
func (f *Frontend) startMessageProcessor() {
for message := range messageBuffer {
f.processMessage(message)
@ -123,6 +155,11 @@ func (f *Frontend) startCallbackProcessor() {
}
}
}
func (f *Frontend) startNotificationsProcessor() {
for notification := range notificationBuffer {
f.handleNotification(notification)
}
}
func (f *Frontend) WindowReload() {
f.ExecJS("runtime.WindowReload();")
@ -160,7 +197,11 @@ func (f *Frontend) Run(ctx context.Context) error {
f.frontendOptions.OnStartup(f.ctx)
}
}()
mainWindow.Run(f.startURL.String())
var activationPolicy C.int
if f.frontendOptions != nil && f.frontendOptions.Mac != nil {
activationPolicy = C.int(f.frontendOptions.Mac.ActivationPolicy)
}
mainWindow.Run(f.startURL.String(), activationPolicy)
return nil
}
@ -444,6 +485,11 @@ func processMessage(message *C.char) {
messageBuffer <- goMessage
}
//export processNotification
func processNotification(notification NotificationType) {
notificationBuffer <- notification
}
//export processURLRequest
func processURLRequest(ctx unsafe.Pointer, requestId C.ulonglong, url *C.char, method *C.char, headers *C.char, body unsafe.Pointer, bodyLen C.int) {
var goBody []byte
@ -465,3 +511,19 @@ func processURLRequest(ctx unsafe.Pointer, requestId C.ulonglong, url *C.char, m
func processCallback(callbackID uint) {
callbackBuffer <- callbackID
}
func (f *Frontend) handleNotification(notification NotificationType) {
switch notification {
case ApplicationDidFinishLaunching:
f.applicationDidFinishLaunching = true
for _, callback := range f.notificationCallbacks[notification] {
go callback()
}
}
}
func (f *Frontend) processTrayMenus() {
for _, trayMenu := range f.trayMenusBuffer {
f.mainWindow.TrayMenuAdd(trayMenu)
}
}

View file

@ -14,12 +14,86 @@ package darwin
*/
import "C"
import (
"fmt"
"sync"
"unsafe"
"github.com/google/uuid"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
)
var createNSObjectMap = make(map[uint32]chan unsafe.Pointer)
var createNSObjectMapLock sync.RWMutex
func waitNSObjectCreate(id uint32, fn func()) unsafe.Pointer {
waitchan := make(chan unsafe.Pointer)
createNSObjectMapLock.Lock()
createNSObjectMap[id] = waitchan
createNSObjectMapLock.Unlock()
fn()
result := <-waitchan
createNSObjectMapLock.Lock()
createNSObjectMap[id] = nil
createNSObjectMapLock.Unlock()
return result
}
//export objectCreated
func objectCreated(id uint32, pointer unsafe.Pointer) {
createNSObjectMapLock.Lock()
createNSObjectMap[id] <- pointer
createNSObjectMapLock.Unlock()
}
func NewNSTrayMenu(context unsafe.Pointer, trayMenu *menu.TrayMenu, scalingFactor int) *NSTrayMenu {
c := NewCalloc()
defer c.Free()
id := uuid.New().ID()
nsStatusItem := waitNSObjectCreate(id, func() {
C.NewNSStatusItem(C.int(id), C.int(trayMenu.Sizing))
})
result := &NSTrayMenu{
context: context,
nsStatusItem: nsStatusItem,
scalingFactor: scalingFactor,
}
result.SetLabel(trayMenu.Label)
result.SetMenu(trayMenu.Menu)
result.SetImage(trayMenu.Image)
return result
}
func (n *NSTrayMenu) SetImage(image *menu.TrayImage) {
if image == nil {
return
}
bitmap := image.GetBestBitmap(n.scalingFactor, false)
if bitmap == nil {
fmt.Printf("[Warning] No TrayMenu Image available for scaling factor %dx\n", n.scalingFactor)
return
}
C.SetTrayImage(n.nsStatusItem,
unsafe.Pointer(&bitmap[0]),
C.int(len(bitmap)),
bool2Cint(image.IsTemplate),
C.int(image.Position),
)
}
func (n *NSTrayMenu) SetMenu(menu *menu.Menu) {
if menu == nil {
return
}
theMenu := NewNSMenu(n.context, "")
processMenu(theMenu, menu)
C.SetTrayMenu(n.nsStatusItem, theMenu.nsmenu)
}
type NSMenu struct {
context unsafe.Pointer
nsmenu unsafe.Pointer
@ -53,6 +127,15 @@ type MenuItem struct {
radioGroupMembers []*MenuItem
}
func (m *MenuItem) SetChecked(value bool) {
C.SetMenuItemChecked(m.nsmenuitem, bool2Cint(value))
}
func (m *MenuItem) SetLabel(label string) {
cLabel := C.CString(label)
C.SetMenuItemLabel(m.nsmenuitem, cLabel)
}
func (m *NSMenu) AddMenuItem(menuItem *menu.MenuItem) *MenuItem {
c := NewCalloc()
defer c.Free()
@ -69,6 +152,7 @@ func (m *NSMenu) AddMenuItem(menuItem *menu.MenuItem) *MenuItem {
result.id = createMenuItemID(result)
result.nsmenuitem = C.AppendMenuItem(m.context, m.nsmenu, c.String(menuItem.Label), key, modifier, bool2Cint(menuItem.Disabled), bool2Cint(menuItem.Checked), C.int(result.id))
menuItem.Impl = result
return result
}

View file

@ -20,6 +20,8 @@ void processMessageDialogResponse(int);
void processOpenFileDialogResponse(const char*);
void processSaveFileDialogResponse(const char*);
void processCallback(int);
void processNotification(int);
void objectCreated(int, void*);
#ifdef __cplusplus
}

View file

@ -0,0 +1,44 @@
//go:build darwin
// +build darwin
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "Application.h"
#import "WailsContext.h"
#include <stdlib.h>
*/
import "C"
import (
"unsafe"
"github.com/wailsapp/wails/v2/pkg/menu"
)
func (f *Frontend) TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl {
nsTrayMenu := f.mainWindow.TrayMenuAdd(trayMenu)
f.trayMenus[trayMenu] = nsTrayMenu
return nsTrayMenu
}
type NSTrayMenu struct {
context unsafe.Pointer
nsStatusItem unsafe.Pointer // NSStatusItem
scalingFactor int
}
func (n *NSTrayMenu) SetLabel(label string) {
if label == "" {
return
}
cLabel := C.CString(label)
C.SetTrayMenuLabel(n.nsStatusItem, cLabel)
}
func (w *Window) TrayMenuAdd(trayMenu *menu.TrayMenu) *NSTrayMenu {
return NewNSTrayMenu(w.context, trayMenu, ScalingFactor(w))
}

View file

@ -40,6 +40,10 @@ func bool2Cint(value bool) C.int {
return C.int(0)
}
func ScalingFactor(window *Window) int {
return int(C.ScalingFactor(window.context))
}
func NewWindow(frontendOptions *options.App, debugMode bool) *Window {
c := NewCalloc()
@ -122,9 +126,9 @@ func (w *Window) Center() {
C.Center(w.context)
}
func (w *Window) Run(url string) {
func (w *Window) Run(url string, activationPolicy C.int) {
_url := C.CString(url)
C.Run(w.context, _url)
C.Run(w.context, _url, activationPolicy)
C.free(unsafe.Pointer(_url))
}

View file

@ -373,11 +373,11 @@ func (f *Frontend) ScreenGetAll() ([]Screen, error) {
}
func (f *Frontend) Show() {
f.mainWindow.Show()
f.WindowShow()
}
func (f *Frontend) Hide() {
f.mainWindow.Hide()
f.WindowHide()
}
func (f *Frontend) WindowIsMaximised() bool {

View file

@ -0,0 +1,36 @@
package windows
import (
"bufio"
"bytes"
"golang.org/x/image/draw"
"image"
"image/png"
)
func ResizePNG(in []byte, size int) ([]byte, error) {
imagedata, _, err := image.Decode(bytes.NewReader(in))
if err != nil {
return nil, err
}
// Scale image
rect := image.Rect(0, 0, size, size)
rawdata := image.NewRGBA(rect)
scale := draw.CatmullRom
scale.Scale(rawdata, rect, imagedata, imagedata.Bounds(), draw.Over, nil)
// Convert back to PNG
icondata := new(bytes.Buffer)
writer := bufio.NewWriter(icondata)
err = png.Encode(writer, rawdata)
if err != nil {
return nil, err
}
err = writer.Flush()
if err != nil {
return nil, err
}
// Save image data
return icondata.Bytes(), nil
}

View file

@ -0,0 +1,79 @@
//go:build windows
// +build windows
package windows
import (
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32"
"github.com/wailsapp/wails/v2/pkg/menu"
"log"
"sync"
"unsafe"
)
var uids uint32
var lock sync.RWMutex
func newUID() uint32 {
lock.Lock()
result := uids
uids++
lock.Unlock()
return result
}
type Win32TrayMenu struct {
hwnd uintptr
uid uint32
icon uintptr
}
func (w *Win32TrayMenu) SetLabel(label string) {}
func (w *Win32TrayMenu) SetMenu(menu *menu.Menu) {}
func (w *Win32TrayMenu) SetImage(image *menu.TrayImage) {
data := w.newNotifyIconData()
bitmap := image.GetBestBitmap(1, false)
icon, err := win32.CreateIconFromResourceEx(uintptr(unsafe.Pointer(&bitmap[0])), uint32(len(bitmap)), true, 0x30000, 0, 0, 0)
if err != nil {
log.Fatal(err.Error())
}
data.UFlags |= win32.NIF_ICON
data.HIcon = icon
if _, err := win32.NotifyIcon(win32.NIM_MODIFY, data); err != nil {
log.Fatal(err.Error())
}
}
func (f *Frontend) NewWin32TrayMenu(trayMenu *menu.TrayMenu) *Win32TrayMenu {
result := &Win32TrayMenu{
hwnd: f.mainWindow.Handle(),
uid: newUID(),
}
data := result.newNotifyIconData()
data.UFlags |= win32.NIF_MESSAGE | win32.NIF_ICON
data.UCallbackMessage = win32.WM_APP + result.uid
if _, err := win32.NotifyIcon(win32.NIM_ADD, data); err != nil {
log.Fatal(err.Error())
}
return result
}
func (w *Win32TrayMenu) newNotifyIconData() *win32.NOTIFYICONDATA {
var data win32.NOTIFYICONDATA
data.CbSize = uint32(unsafe.Sizeof(data))
data.UFlags = win32.NIF_GUID
data.HWnd = w.hwnd
data.UID = w.uid
return &data
}
func (f *Frontend) TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl {
win32TrayMenu := f.NewWin32TrayMenu(trayMenu)
return win32TrayMenu
}

View file

@ -13,16 +13,26 @@ type HANDLE uintptr
type HMONITOR HANDLE
var (
moduser32 = syscall.NewLazyDLL("user32.dll")
procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW")
procGetWindowLong = moduser32.NewProc("GetWindowLongW")
procSetClassLong = moduser32.NewProc("SetClassLongW")
procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW")
procShowWindow = moduser32.NewProc("ShowWindow")
procIsWindowVisible = moduser32.NewProc("IsWindowVisible")
procGetWindowRect = moduser32.NewProc("GetWindowRect")
procGetMonitorInfo = moduser32.NewProc("GetMonitorInfoW")
procMonitorFromWindow = moduser32.NewProc("MonitorFromWindow")
moduser32 = syscall.NewLazyDLL("user32.dll")
procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW")
procGetWindowLong = moduser32.NewProc("GetWindowLongW")
procSetClassLong = moduser32.NewProc("SetClassLongW")
procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW")
procShowWindow = moduser32.NewProc("ShowWindow")
procIsWindowVisible = moduser32.NewProc("IsWindowVisible")
procGetWindowRect = moduser32.NewProc("GetWindowRect")
procGetMonitorInfo = moduser32.NewProc("GetMonitorInfoW")
procMonitorFromWindow = moduser32.NewProc("MonitorFromWindow")
procLookupIconIdFromDirectoryEx = moduser32.NewProc("LookupIconIdFromDirectoryEx")
procCreateIconFromResourceEx = moduser32.NewProc("CreateIconFromResourceEx")
procCreateIconIndirect = moduser32.NewProc("CreateIconIndirect")
procLoadImageW = moduser32.NewProc("LoadImageW")
procBringWindowToTop = moduser32.NewProc("BringWindowToTop")
)
var (
modshell32 = syscall.NewLazyDLL("shell32.dll")
procShellNotifyIcon = modshell32.NewProc("Shell_NotifyIconW")
)
var (
moddwmapi = syscall.NewLazyDLL("dwmapi.dll")
@ -41,3 +51,209 @@ func IsWindowsVersionAtLeast(major, minor, buildNumber int) bool {
windowsVersion.Minor >= minor &&
windowsVersion.Build >= buildNumber
}
const (
WM_APP = 32768
WM_ACTIVATE = 6
WM_ACTIVATEAPP = 28
WM_AFXFIRST = 864
WM_AFXLAST = 895
WM_ASKCBFORMATNAME = 780
WM_CANCELJOURNAL = 75
WM_CANCELMODE = 31
WM_CAPTURECHANGED = 533
WM_CHANGECBCHAIN = 781
WM_CHAR = 258
WM_CHARTOITEM = 47
WM_CHILDACTIVATE = 34
WM_CLEAR = 771
WM_CLOSE = 16
WM_COMMAND = 273
WM_COMMNOTIFY = 68 /* OBSOLETE */
WM_COMPACTING = 65
WM_COMPAREITEM = 57
WM_CONTEXTMENU = 123
WM_COPY = 769
WM_COPYDATA = 74
WM_CREATE = 1
WM_CTLCOLORBTN = 309
WM_CTLCOLORDLG = 310
WM_CTLCOLOREDIT = 307
WM_CTLCOLORLISTBOX = 308
WM_CTLCOLORMSGBOX = 306
WM_CTLCOLORSCROLLBAR = 311
WM_CTLCOLORSTATIC = 312
WM_CUT = 768
WM_DEADCHAR = 259
WM_DELETEITEM = 45
WM_DESTROY = 2
WM_DESTROYCLIPBOARD = 775
WM_DEVICECHANGE = 537
WM_DEVMODECHANGE = 27
WM_DISPLAYCHANGE = 126
WM_DRAWCLIPBOARD = 776
WM_DRAWITEM = 43
WM_DROPFILES = 563
WM_ENABLE = 10
WM_ENDSESSION = 22
WM_ENTERIDLE = 289
WM_ENTERMENULOOP = 529
WM_ENTERSIZEMOVE = 561
WM_ERASEBKGND = 20
WM_EXITMENULOOP = 530
WM_EXITSIZEMOVE = 562
WM_FONTCHANGE = 29
WM_GETDLGCODE = 135
WM_GETFONT = 49
WM_GETHOTKEY = 51
WM_GETICON = 127
WM_GETMINMAXINFO = 36
WM_GETTEXT = 13
WM_GETTEXTLENGTH = 14
WM_HANDHELDFIRST = 856
WM_HANDHELDLAST = 863
WM_HELP = 83
WM_HOTKEY = 786
WM_HSCROLL = 276
WM_HSCROLLCLIPBOARD = 782
WM_ICONERASEBKGND = 39
WM_INITDIALOG = 272
WM_INITMENU = 278
WM_INITMENUPOPUP = 279
WM_INPUT = 0x00FF
WM_INPUTLANGCHANGE = 81
WM_INPUTLANGCHANGEREQUEST = 80
WM_KEYDOWN = 256
WM_KEYUP = 257
WM_KILLFOCUS = 8
WM_MDIACTIVATE = 546
WM_MDICASCADE = 551
WM_MDICREATE = 544
WM_MDIDESTROY = 545
WM_MDIGETACTIVE = 553
WM_MDIICONARRANGE = 552
WM_MDIMAXIMIZE = 549
WM_MDINEXT = 548
WM_MDIREFRESHMENU = 564
WM_MDIRESTORE = 547
WM_MDISETMENU = 560
WM_MDITILE = 550
WM_MEASUREITEM = 44
WM_GETOBJECT = 0x003D
WM_CHANGEUISTATE = 0x0127
WM_UPDATEUISTATE = 0x0128
WM_QUERYUISTATE = 0x0129
WM_UNINITMENUPOPUP = 0x0125
WM_MENURBUTTONUP = 290
WM_MENUCOMMAND = 0x0126
WM_MENUGETOBJECT = 0x0124
WM_MENUDRAG = 0x0123
WM_APPCOMMAND = 0x0319
WM_MENUCHAR = 288
WM_MENUSELECT = 287
WM_MOVE = 3
WM_MOVING = 534
WM_NCACTIVATE = 134
WM_NCCALCSIZE = 131
WM_NCCREATE = 129
WM_NCDESTROY = 130
WM_NCHITTEST = 132
WM_NCLBUTTONDBLCLK = 163
WM_NCLBUTTONDOWN = 161
WM_NCLBUTTONUP = 162
WM_NCMBUTTONDBLCLK = 169
WM_NCMBUTTONDOWN = 167
WM_NCMBUTTONUP = 168
WM_NCXBUTTONDOWN = 171
WM_NCXBUTTONUP = 172
WM_NCXBUTTONDBLCLK = 173
WM_NCMOUSEHOVER = 0x02A0
WM_NCMOUSELEAVE = 0x02A2
WM_NCMOUSEMOVE = 160
WM_NCPAINT = 133
WM_NCRBUTTONDBLCLK = 166
WM_NCRBUTTONDOWN = 164
WM_NCRBUTTONUP = 165
WM_NEXTDLGCTL = 40
WM_NEXTMENU = 531
WM_NOTIFY = 78
WM_NOTIFYFORMAT = 85
WM_NULL = 0
WM_PAINT = 15
WM_PAINTCLIPBOARD = 777
WM_PAINTICON = 38
WM_PALETTECHANGED = 785
WM_PALETTEISCHANGING = 784
WM_PARENTNOTIFY = 528
WM_PASTE = 770
WM_PENWINFIRST = 896
WM_PENWINLAST = 911
WM_POWER = 72
WM_PRINT = 791
WM_PRINTCLIENT = 792
WM_QUERYDRAGICON = 55
WM_QUERYENDSESSION = 17
WM_QUERYNEWPALETTE = 783
WM_QUERYOPEN = 19
WM_QUEUESYNC = 35
WM_QUIT = 18
WM_RENDERALLFORMATS = 774
WM_RENDERFORMAT = 773
WM_SETCURSOR = 32
WM_SETFOCUS = 7
WM_SETFONT = 48
WM_SETHOTKEY = 50
WM_SETICON = 128
WM_SETREDRAW = 11
WM_SETTEXT = 12
WM_SETTINGCHANGE = 26
WM_SHOWWINDOW = 24
WM_SIZE = 5
WM_SIZECLIPBOARD = 779
WM_SIZING = 532
WM_SPOOLERSTATUS = 42
WM_STYLECHANGED = 125
WM_STYLECHANGING = 124
WM_SYSCHAR = 262
WM_SYSCOLORCHANGE = 21
WM_SYSCOMMAND = 274
WM_SYSDEADCHAR = 263
WM_SYSKEYDOWN = 260
WM_SYSKEYUP = 261
WM_TCARD = 82
WM_THEMECHANGED = 794
WM_TIMECHANGE = 30
WM_TIMER = 275
WM_UNDO = 772
WM_USER = 1024
WM_USERCHANGED = 84
WM_VKEYTOITEM = 46
WM_VSCROLL = 277
WM_VSCROLLCLIPBOARD = 778
WM_WINDOWPOSCHANGED = 71
WM_WINDOWPOSCHANGING = 70
WM_WININICHANGE = 26
WM_KEYFIRST = 256
WM_KEYLAST = 264
WM_SYNCPAINT = 136
WM_MOUSEACTIVATE = 33
WM_MOUSEMOVE = 512
WM_LBUTTONDOWN = 513
WM_LBUTTONUP = 514
WM_LBUTTONDBLCLK = 515
WM_RBUTTONDOWN = 516
WM_RBUTTONUP = 517
WM_RBUTTONDBLCLK = 518
WM_MBUTTONDOWN = 519
WM_MBUTTONUP = 520
WM_MBUTTONDBLCLK = 521
WM_MOUSEWHEEL = 522
WM_MOUSEFIRST = 512
WM_XBUTTONDOWN = 523
WM_XBUTTONUP = 524
WM_XBUTTONDBLCLK = 525
WM_MOUSELAST = 525
WM_MOUSEHOVER = 0x2A1
WM_MOUSELEAVE = 0x2A3
WM_CLIPBOARDUPDATE = 0x031D
)

View file

@ -0,0 +1,133 @@
package win32
import (
"golang.org/x/sys/windows"
"unsafe"
)
const (
NIF_MESSAGE = 0x00000001
NIF_ICON = 0x00000002
NIF_TIP = 0x00000004
NIF_STATE = 0x00000008
NIF_INFO = 0x00000010
NIF_GUID = 0x00000020
NIF_REALTIME = 0x00000040
NIF_SHOWTIP = 0x00000080
NIM_ADD = 0x00000000
NIM_MODIFY = 0x00000001
NIM_DELETE = 0x00000002
NIM_SETFOCUS = 0x00000003
NIM_SETVERSION = 0x00000004
NIS_HIDDEN = 0x00000001
NIS_SHAREDICON = 0x00000002
NIN_BALLOONSHOW = 0x0402
NIN_BALLOONTIMEOUT = 0x0404
NIN_BALLOONUSERCLICK = 0x0405
NIIF_NONE = 0x00000000
NIIF_INFO = 0x00000001
NIIF_WARNING = 0x00000002
NIIF_ERROR = 0x00000003
NIIF_USER = 0x00000004
NIIF_NOSOUND = 0x00000010
NIIF_LARGE_ICON = 0x00000020
NIIF_RESPECT_QUIET_TIME = 0x00000080
NIIF_ICON_MASK = 0x0000000F
)
type NOTIFYICONDATA struct {
CbSize uint32
HWnd uintptr
UID uint32
UFlags uint32
UCallbackMessage uint32
HIcon uintptr
SzTip [128]uint16
DwState uint32
DwStateMask uint32
SzInfo [256]uint16
UVersion uint32
SzInfoTitle [64]uint16
DwInfoFlags uint32
GUIDItem windows.GUID
HBalloonIcon uintptr
}
func NotifyIcon(msg uint32, lpData *NOTIFYICONDATA) (int32, error) {
r, _, err := procShellNotifyIcon.Call(
uintptr(msg),
uintptr(unsafe.Pointer(lpData)))
if r == 0 {
return 0, err
}
return int32(r), nil
}
func LookupIconIdFromDirectoryEx(presbits uintptr, isIcon bool, cxDesired int, cyDesired int, flags uint) (int32, error) {
var icon uint32 = 0
if isIcon {
icon = 1
}
r, _, err := procLookupIconIdFromDirectoryEx.Call(
presbits,
uintptr(icon),
uintptr(cxDesired),
uintptr(cyDesired),
uintptr(flags),
)
if r == 0 {
return 0, err
}
return int32(r), nil
}
func CreateIconIndirect(data uintptr) (uintptr, error) {
r, _, err := procCreateIconIndirect.Call(
data,
)
if r == 0 {
return 0, err
}
return r, nil
}
func CreateIconFromResourceEx(presbits uintptr, dwResSize uint32, isIcon bool, version uint32, cxDesired int, cyDesired int, flags uint) (uintptr, error) {
icon := 0
if isIcon {
icon = 1
}
r, _, err := procCreateIconFromResourceEx.Call(
presbits,
uintptr(dwResSize),
uintptr(icon),
uintptr(version),
uintptr(cxDesired),
uintptr(cyDesired),
uintptr(flags),
)
if r == 0 {
return 0, err
}
return r, nil
}
func LoadImage(
hInst uintptr,
name *uint16,
type_ uint32,
cx, cy int32,
fuLoad uint32) (uintptr, error) {
r, _, err := procLoadImageW.Call(
hInst,
uintptr(unsafe.Pointer(name)),
uintptr(type_),
uintptr(cx),
uintptr(cy),
uintptr(fuLoad))
if r == 0 {
return 0, err
}
return r, nil
}

View file

@ -218,3 +218,10 @@ func GetMonitorInfo(hMonitor HMONITOR, lmpi *MONITORINFO) bool {
)
return ret != 0
}
func BringWindowToTop(hwnd uintptr) bool {
ret, _, _ := procBringWindowToTop.Call(
hwnd,
)
return ret != 0
}

View file

@ -121,4 +121,7 @@ type Frontend interface {
// Browser
BrowserOpenURL(url string)
// Tray Menu
TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl
}

View file

@ -0,0 +1,9 @@
package options
import "github.com/wailsapp/wails/v2/pkg/options"
// Frontend contains options for creating the Frontend
type Frontend struct {
options.App
HasMainWindow bool
}

View file

@ -1,12 +1,15 @@
package menumanager
import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
)
import "github.com/wailsapp/wails/v2/pkg/menu"
//
//import (
// "encoding/json"
// "fmt"
//
// "github.com/wailsapp/wails/v2/pkg/menu"
//)
//
type ContextMenu struct {
ID string
ProcessedMenu *WailsMenu
@ -14,48 +17,49 @@ type ContextMenu struct {
menu *menu.Menu
}
func (t *ContextMenu) AsJSON() (string, error) {
data, err := json.Marshal(t)
if err != nil {
return "", err
}
return string(data), nil
}
func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu {
result := &ContextMenu{
ID: contextMenu.ID,
menu: contextMenu.Menu,
menuItemMap: NewMenuItemMap(),
}
result.menuItemMap.AddMenu(contextMenu.Menu)
result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
return result
}
func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) {
newContextMenu := NewContextMenu(contextMenu)
// Save the references
m.contextMenus[contextMenu.ID] = newContextMenu
m.contextMenuPointers[contextMenu] = contextMenu.ID
}
func (m *Manager) UpdateContextMenu(contextMenu *menu.ContextMenu) (string, error) {
contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu]
if !contextMenuKnown {
return "", fmt.Errorf("unknown Context Menu '%s'. Please add the context menu using AddContextMenu()", contextMenu.ID)
}
// Create the updated context menu
updatedContextMenu := NewContextMenu(contextMenu)
// Save the reference
m.contextMenus[contextMenuID] = updatedContextMenu
return updatedContextMenu.AsJSON()
}
//
//func (t *ContextMenu) AsJSON() (string, error) {
// data, err := json.Marshal(t)
// if err != nil {
// return "", err
// }
// return string(data), nil
//}
//
//func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu {
//
// result := &ContextMenu{
// ID: contextMenu.ID,
// menu: contextMenu.Menu,
// menuItemMap: NewMenuItemMap(),
// }
//
// result.menuItemMap.AddMenu(contextMenu.Menu)
// result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
//
// return result
//}
//
//func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) {
//
// newContextMenu := NewContextMenu(contextMenu)
//
// // Save the references
// m.contextMenus[contextMenu.ID] = newContextMenu
// m.contextMenuPointers[contextMenu] = contextMenu.ID
//}
//
//func (m *Manager) UpdateContextMenu(contextMenu *menu.ContextMenu) (string, error) {
// contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu]
// if !contextMenuKnown {
// return "", fmt.Errorf("unknown Context Menu '%s'. Please add the context menu using AddContextMenu()", contextMenu.ID)
// }
//
// // Create the updated context menu
// updatedContextMenu := NewContextMenu(contextMenu)
//
// // Save the reference
// m.contextMenus[contextMenuID] = updatedContextMenu
//
// return updatedContextMenu.AsJSON()
//}

View file

@ -2,6 +2,7 @@ package menumanager
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
)

View file

@ -1,15 +1,11 @@
package menumanager
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"sync"
"github.com/leaanthony/go-ansi-parser"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/pkg/menu"
)
@ -32,7 +28,7 @@ type TrayMenu struct {
FontName string
Disabled bool
Tooltip string `json:",omitempty"`
Image string
Image []byte
MacTemplateImage bool
RGBA string
menuItemMap *MenuItemMap
@ -42,181 +38,182 @@ type TrayMenu struct {
StyledLabel []*ansi.StyledText `json:",omitempty"`
}
func (t *TrayMenu) AsJSON() (string, error) {
data, err := json.Marshal(t)
if err != nil {
return "", err
}
return string(data), nil
}
func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
// Parse ANSI text
var styledLabel []*ansi.StyledText
tempLabel := trayMenu.Label
if strings.Contains(tempLabel, "\033[") {
parsedLabel, err := ansi.Parse(tempLabel)
if err == nil {
styledLabel = parsedLabel
}
}
result := &TrayMenu{
Label: trayMenu.Label,
FontName: trayMenu.FontName,
FontSize: trayMenu.FontSize,
Disabled: trayMenu.Disabled,
Tooltip: trayMenu.Tooltip,
Image: trayMenu.Image,
MacTemplateImage: trayMenu.MacTemplateImage,
menu: trayMenu.Menu,
RGBA: trayMenu.RGBA,
menuItemMap: NewMenuItemMap(),
trayMenu: trayMenu,
StyledLabel: styledLabel,
}
result.menuItemMap.AddMenu(trayMenu.Menu)
result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
return result
}
func (m *Manager) OnTrayMenuOpen(id string) {
trayMenu, ok := m.trayMenus[id]
if !ok {
return
}
if trayMenu.trayMenu.OnOpen == nil {
return
}
go trayMenu.trayMenu.OnOpen()
}
func (m *Manager) OnTrayMenuClose(id string) {
trayMenu, ok := m.trayMenus[id]
if !ok {
return
}
if trayMenu.trayMenu.OnClose == nil {
return
}
go trayMenu.trayMenu.OnClose()
}
func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
newTrayMenu := NewTrayMenu(trayMenu)
// Hook up a new ID
trayID := generateTrayID()
newTrayMenu.ID = trayID
// Save the references
m.trayMenus[trayID] = newTrayMenu
m.trayMenuPointers[trayMenu] = trayID
return newTrayMenu.AsJSON()
}
func (m *Manager) GetTrayID(trayMenu *menu.TrayMenu) (string, error) {
trayID, exists := m.trayMenuPointers[trayMenu]
if !exists {
return "", fmt.Errorf("Unable to find menu ID for tray menu!")
}
return trayID, nil
}
// SetTrayMenu updates or creates a menu
func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
if !trayMenuKnown {
return m.AddTrayMenu(trayMenu)
}
// Create the updated tray menu
updatedTrayMenu := NewTrayMenu(trayMenu)
updatedTrayMenu.ID = trayID
// Save the reference
m.trayMenus[trayID] = updatedTrayMenu
return updatedTrayMenu.AsJSON()
}
func (m *Manager) GetTrayMenus() ([]string, error) {
result := []string{}
for _, trayMenu := range m.trayMenus {
JSON, err := trayMenu.AsJSON()
if err != nil {
return nil, err
}
result = append(result, JSON)
}
return result, nil
}
func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) {
trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
if !trayMenuKnown {
return "", fmt.Errorf("[UpdateTrayMenuLabel] unknown tray id for tray %s", trayMenu.Label)
}
type LabelUpdate struct {
ID string
Label string `json:",omitempty"`
FontName string `json:",omitempty"`
FontSize int
RGBA string `json:",omitempty"`
Disabled bool
Tooltip string `json:",omitempty"`
Image string `json:",omitempty"`
MacTemplateImage bool
StyledLabel []*ansi.StyledText `json:",omitempty"`
}
// Parse ANSI text
var styledLabel []*ansi.StyledText
tempLabel := trayMenu.Label
if strings.Contains(tempLabel, "\033[") {
parsedLabel, err := ansi.Parse(tempLabel)
if err == nil {
styledLabel = parsedLabel
}
}
update := &LabelUpdate{
ID: trayID,
Label: trayMenu.Label,
FontName: trayMenu.FontName,
FontSize: trayMenu.FontSize,
Disabled: trayMenu.Disabled,
Tooltip: trayMenu.Tooltip,
Image: trayMenu.Image,
MacTemplateImage: trayMenu.MacTemplateImage,
RGBA: trayMenu.RGBA,
StyledLabel: styledLabel,
}
data, err := json.Marshal(update)
if err != nil {
return "", errors.Wrap(err, "[UpdateTrayMenuLabel] ")
}
return string(data), nil
}
func (m *Manager) GetContextMenus() ([]string, error) {
result := []string{}
for _, contextMenu := range m.contextMenus {
JSON, err := contextMenu.AsJSON()
if err != nil {
return nil, err
}
result = append(result, JSON)
}
return result, nil
}
//
//func (t *TrayMenu) AsJSON() (string, error) {
// data, err := json.Marshal(t)
// if err != nil {
// return "", err
// }
// return string(data), nil
//}
//
//func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
//
// // Parse ANSI text
// var styledLabel []*ansi.StyledText
// tempLabel := trayMenu.Label
// if strings.Contains(tempLabel, "\033[") {
// parsedLabel, err := ansi.Parse(tempLabel)
// if err == nil {
// styledLabel = parsedLabel
// }
// }
//
// result := &TrayMenu{
// Label: trayMenu.Label,
// FontName: trayMenu.FontName,
// FontSize: trayMenu.FontSize,
// Disabled: trayMenu.Disabled,
// Tooltip: trayMenu.Tooltip,
// Image: trayMenu.Image,
// MacTemplateImage: trayMenu.MacTemplateImage,
// menu: trayMenu.Menu,
// RGBA: trayMenu.RGBA,
// menuItemMap: NewMenuItemMap(),
// trayMenu: trayMenu,
// StyledLabel: styledLabel,
// }
//
// result.menuItemMap.AddMenu(trayMenu.Menu)
// result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
//
// return result
//}
//
//func (m *Manager) OnTrayMenuOpen(id string) {
// trayMenu, ok := m.trayMenus[id]
// if !ok {
// return
// }
// if trayMenu.trayMenu.OnOpen == nil {
// return
// }
// go trayMenu.trayMenu.OnOpen()
//}
//
//func (m *Manager) OnTrayMenuClose(id string) {
// trayMenu, ok := m.trayMenus[id]
// if !ok {
// return
// }
// if trayMenu.trayMenu.OnClose == nil {
// return
// }
// go trayMenu.trayMenu.OnClose()
//}
//
//func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
// newTrayMenu := NewTrayMenu(trayMenu)
//
// // Hook up a new ID
// trayID := generateTrayID()
// newTrayMenu.ID = trayID
//
// // Save the references
// m.trayMenus[trayID] = newTrayMenu
// m.trayMenuPointers[trayMenu] = trayID
//
// return newTrayMenu.AsJSON()
//}
//
//func (m *Manager) GetTrayID(trayMenu *menu.TrayMenu) (string, error) {
// trayID, exists := m.trayMenuPointers[trayMenu]
// if !exists {
// return "", fmt.Errorf("Unable to find menu ID for tray menu!")
// }
// return trayID, nil
//}
//
//// SetTrayMenu updates or creates a menu
//func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
// trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
// if !trayMenuKnown {
// return m.AddTrayMenu(trayMenu)
// }
//
// // Create the updated tray menu
// updatedTrayMenu := NewTrayMenu(trayMenu)
// updatedTrayMenu.ID = trayID
//
// // Save the reference
// m.trayMenus[trayID] = updatedTrayMenu
//
// return updatedTrayMenu.AsJSON()
//}
//
//func (m *Manager) GetTrayMenus() ([]string, error) {
// result := []string{}
// for _, trayMenu := range m.trayMenus {
// JSON, err := trayMenu.AsJSON()
// if err != nil {
// return nil, err
// }
// result = append(result, JSON)
// }
//
// return result, nil
//}
//
//func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) {
// trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
// if !trayMenuKnown {
// return "", fmt.Errorf("[UpdateTrayMenuLabel] unknown tray id for tray %s", trayMenu.Label)
// }
//
// type LabelUpdate struct {
// ID string
// Label string `json:",omitempty"`
// FontName string `json:",omitempty"`
// FontSize int
// RGBA string `json:",omitempty"`
// Disabled bool
// Tooltip string `json:",omitempty"`
// Image []byte `json:",omitempty"`
// MacTemplateImage bool
// StyledLabel []*ansi.StyledText `json:",omitempty"`
// }
//
// // Parse ANSI text
// var styledLabel []*ansi.StyledText
// tempLabel := trayMenu.Label
// if strings.Contains(tempLabel, "\033[") {
// parsedLabel, err := ansi.Parse(tempLabel)
// if err == nil {
// styledLabel = parsedLabel
// }
// }
//
// update := &LabelUpdate{
// ID: trayID,
// Label: trayMenu.Label,
// FontName: trayMenu.FontName,
// FontSize: trayMenu.FontSize,
// Disabled: trayMenu.Disabled,
// Tooltip: trayMenu.Tooltip,
// Image: trayMenu.Image,
// MacTemplateImage: trayMenu.MacTemplateImage,
// RGBA: trayMenu.RGBA,
// StyledLabel: styledLabel,
// }
//
// data, err := json.Marshal(update)
// if err != nil {
// return "", errors.Wrap(err, "[UpdateTrayMenuLabel] ")
// }
//
// return string(data), nil
//
//}
//
//func (m *Manager) GetContextMenus() ([]string, error) {
// result := []string{}
// for _, contextMenu := range m.contextMenus {
// JSON, err := contextMenu.AsJSON()
// if err != nil {
// return nil, err
// }
// result = append(result, JSON)
// }
//
// return result, nil
//}

View file

@ -0,0 +1,145 @@
package menu
import (
"github.com/wailsapp/wails/v2/pkg/menu"
)
// MenuManager manages the menus for the application
var MenuManager = NewManager()
type radioGroup []*menu.MenuItem
// Click updates the radio group state based on the item clicked
func (g *radioGroup) Click(item *menu.MenuItem) {
for _, radioGroupItem := range *g {
if radioGroupItem != item {
radioGroupItem.Checked = false
}
}
}
type processedMenu struct {
// the menu we processed
menu *menu.Menu
// updateMenuItemCallback is called when the menu item needs to be updated in the UI
updateMenuItemCallback func(*menu.MenuItem)
// items is a map of all menu items in this menu
items map[*menu.MenuItem]struct{}
// radioGroups tracks which radiogroup a menu item belongs to
radioGroups map[*menu.MenuItem][]*radioGroup
}
func newProcessedMenu(topLevelMenu *menu.Menu, updateMenuItemCallback func(*menu.MenuItem)) *processedMenu {
result := &processedMenu{
updateMenuItemCallback: updateMenuItemCallback,
menu: topLevelMenu,
items: make(map[*menu.MenuItem]struct{}),
radioGroups: make(map[*menu.MenuItem][]*radioGroup),
}
result.process(topLevelMenu.Items)
return result
}
func (p *processedMenu) process(items []*menu.MenuItem) {
var currentRadioGroup radioGroup
for index, item := range items {
// Save the reference to the top level menu for this item
p.items[item] = struct{}{}
// If this is a radio item, add it to the radio group
if item.Type == menu.RadioType {
currentRadioGroup = append(currentRadioGroup, item)
}
// If this is not a radio item, or we are processing the last item in the menu,
// then we need to add the current radio group to the map if it has items
if item.Type != menu.RadioType || index == len(items)-1 {
if len(currentRadioGroup) > 0 {
p.addRadioGroup(currentRadioGroup)
currentRadioGroup = nil
}
}
// Process the submenu
if item.SubMenu != nil {
p.process(item.SubMenu.Items)
}
}
}
func (p *processedMenu) processClick(item *menu.MenuItem) {
// If this item is not in our menu, then we can't process it
if _, ok := p.items[item]; !ok {
return
}
// If this is a radio item, then we need to update the radio group
if item.Type == menu.RadioType {
// Get the radio groups for this item
radioGroups := p.radioGroups[item]
// Iterate each radio group this item belongs to and set the checked state
// of all items apart from the one that was clicked to false
for _, thisRadioGroup := range radioGroups {
thisRadioGroup.Click(item)
for _, thisRadioGroupItem := range *thisRadioGroup {
p.updateMenuItemCallback(thisRadioGroupItem)
}
}
}
if item.Type == menu.CheckboxType {
p.updateMenuItemCallback(item)
}
}
func (p *processedMenu) addRadioGroup(r radioGroup) {
for _, item := range r {
p.radioGroups[item] = append(p.radioGroups[item], &r)
}
}
type Manager struct {
menus map[*menu.Menu]*processedMenu
}
func NewManager() *Manager {
return &Manager{
menus: make(map[*menu.Menu]*processedMenu),
}
}
func (m *Manager) AddMenu(menu *menu.Menu, updateMenuItemCallback func(*menu.MenuItem)) {
m.menus[menu] = newProcessedMenu(menu, updateMenuItemCallback)
}
func (m *Manager) ProcessClick(item *menu.MenuItem) {
// if menuitem is a checkbox, then we need to toggle the state
if item.Type == menu.CheckboxType {
item.Checked = !item.Checked
}
// Set the radio item to checked
if item.Type == menu.RadioType {
item.Checked = true
}
for _, thisMenu := range m.menus {
thisMenu.processClick(item)
}
if item.Click != nil {
item.Click(&menu.CallbackData{
MenuItem: item,
})
}
}
func (m *Manager) RemoveMenu(data *menu.Menu) {
delete(m.menus, data)
}

View file

@ -0,0 +1,295 @@
package menu_test
import (
"github.com/stretchr/testify/require"
platformMenu "github.com/wailsapp/wails/v2/internal/platform/menu"
"github.com/wailsapp/wails/v2/pkg/menu"
"testing"
)
func TestManager_ProcessClick_Checkbox(t *testing.T) {
checkbox := menu.Label("Checkbox").SetChecked(false)
menu1 := &menu.Menu{
Items: []*menu.MenuItem{
checkbox,
},
}
menu2 := &menu.Menu{
Items: []*menu.MenuItem{
checkbox,
},
}
menuWithNoCheckbox := &menu.Menu{
Items: []*menu.MenuItem{
menu.Label("No Checkbox"),
},
}
clicked := false
tests := []struct {
name string
inputs []*menu.Menu
startState bool
expectedState bool
expectedMenuUpdates map[*menu.Menu][]*menu.MenuItem
click func(*menu.CallbackData)
}{
{
name: "should callback menu checkbox state when clicked (false -> true)",
inputs: []*menu.Menu{menu1},
expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
menu1: {checkbox},
},
startState: false,
expectedState: true,
},
{
name: "should callback multiple menus when checkbox state when clicked (false -> true)",
inputs: []*menu.Menu{menu1, menu2},
startState: false,
expectedState: true,
expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
menu1: {checkbox},
menu2: {checkbox},
},
},
{
name: "should callback only for the menus that the checkbox is in (false -> true)",
inputs: []*menu.Menu{menu1, menuWithNoCheckbox},
startState: false,
expectedState: true,
expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
menu1: {checkbox},
},
},
{
name: "should callback menu checkbox state when clicked (true->false)",
inputs: []*menu.Menu{menu1},
expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
menu1: {checkbox},
},
startState: true,
expectedState: false,
},
{
name: "should callback multiple menus when checkbox state when clicked (true->false)",
inputs: []*menu.Menu{menu1, menu2},
startState: true,
expectedState: false,
expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
menu1: {checkbox},
menu2: {checkbox},
},
},
{
name: "should callback only for the menus that the checkbox is in (true->false)",
inputs: []*menu.Menu{menu1, menuWithNoCheckbox},
startState: true,
expectedState: false,
expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
menu1: {checkbox},
},
},
{
name: "should callback no menus if checkbox not in them",
inputs: []*menu.Menu{menuWithNoCheckbox},
startState: false,
expectedState: false,
expectedMenuUpdates: nil,
},
{
name: "should call Click on the checkbox",
inputs: []*menu.Menu{menu1, menu2},
startState: false,
expectedState: true,
expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
menu1: {checkbox},
menu2: {checkbox},
},
click: func(data *menu.CallbackData) {
clicked = true
},
},
}
for _, tt := range tests {
menusUpdated := map[*menu.Menu][]*menu.MenuItem{}
clicked = false
var checkMenuItemStateInMenu func(menu *menu.Menu)
checkMenuItemStateInMenu = func(menu *menu.Menu) {
for _, item := range menusUpdated[menu] {
if item == checkbox {
require.Equal(t, tt.expectedState, item.Checked)
}
if item.SubMenu != nil {
checkMenuItemStateInMenu(item.SubMenu)
}
}
}
t.Run(tt.name, func(t *testing.T) {
m := platformMenu.NewManager()
checkbox.SetChecked(tt.startState)
checkbox.Click = tt.click
for _, thisMenu := range tt.inputs {
thisMenu := thisMenu
m.AddMenu(thisMenu, func(menuItem *menu.MenuItem) {
menusUpdated[thisMenu] = append(menusUpdated[thisMenu], menuItem)
})
}
m.ProcessClick(checkbox)
// Check the item has the correct state in all the menus
for thisMenu := range menusUpdated {
require.EqualValues(t, tt.expectedMenuUpdates[thisMenu], menusUpdated[thisMenu])
}
if tt.click != nil {
require.Equal(t, true, clicked)
}
})
}
}
func TestManager_ProcessClick_RadioGroups(t *testing.T) {
radio1 := menu.Radio("Radio1", false, nil, nil)
radio2 := menu.Radio("Radio2", false, nil, nil)
radio3 := menu.Radio("Radio3", false, nil, nil)
radio4 := menu.Radio("Radio4", false, nil, nil)
radio5 := menu.Radio("Radio5", false, nil, nil)
radio6 := menu.Radio("Radio6", false, nil, nil)
radioGroupOne := &menu.Menu{
Items: []*menu.MenuItem{
radio1,
radio2,
radio3,
},
}
radioGroupTwo := &menu.Menu{
Items: []*menu.MenuItem{
radio4,
radio5,
radio6,
},
}
radioGroupThree := &menu.Menu{
Items: []*menu.MenuItem{
radio1,
radio2,
radio3,
},
}
clicked := false
tests := []struct {
name string
inputs []*menu.Menu
startState map[*menu.MenuItem]bool
selected *menu.MenuItem
expectedMenuUpdates map[*menu.Menu][]*menu.MenuItem
click func(*menu.CallbackData)
expectedState map[*menu.MenuItem]bool
}{
{
name: "should only set the clicked radio item",
inputs: []*menu.Menu{radioGroupOne},
expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
radioGroupOne: {radio1, radio2, radio3},
},
startState: map[*menu.MenuItem]bool{
radio1: true,
radio2: false,
radio3: false,
},
selected: radio2,
expectedState: map[*menu.MenuItem]bool{
radio1: false,
radio2: true,
radio3: false,
},
},
{
name: "should not affect other radio groups or menus",
inputs: []*menu.Menu{radioGroupOne, radioGroupTwo},
expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
radioGroupOne: {radio1, radio2, radio3},
},
startState: map[*menu.MenuItem]bool{
radio1: true,
radio2: false,
radio3: false,
radio4: true,
radio5: false,
radio6: false,
},
selected: radio2,
expectedState: map[*menu.MenuItem]bool{
radio1: false,
radio2: true,
radio3: false,
radio4: true,
radio5: false,
radio6: false,
},
},
{
name: "menus with the same radio group should be updated",
inputs: []*menu.Menu{radioGroupOne, radioGroupThree},
expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
radioGroupOne: {radio1, radio2, radio3},
radioGroupThree: {radio1, radio2, radio3},
},
startState: map[*menu.MenuItem]bool{
radio1: true,
radio2: false,
radio3: false,
},
selected: radio2,
expectedState: map[*menu.MenuItem]bool{
radio1: false,
radio2: true,
radio3: false,
},
},
}
for _, tt := range tests {
menusUpdated := map[*menu.Menu][]*menu.MenuItem{}
clicked = false
t.Run(tt.name, func(t *testing.T) {
m := platformMenu.NewManager()
for item, value := range tt.startState {
item.SetChecked(value)
}
tt.selected.Click = tt.click
for _, thisMenu := range tt.inputs {
thisMenu := thisMenu
m.AddMenu(thisMenu, func(menuItem *menu.MenuItem) {
menusUpdated[thisMenu] = append(menusUpdated[thisMenu], menuItem)
})
}
m.ProcessClick(tt.selected)
require.Equal(t, tt.expectedMenuUpdates, menusUpdated)
// Check the items have the correct state in all the menus
for item, expectedValue := range tt.expectedState {
require.Equal(t, expectedValue, item.Checked)
}
if tt.click != nil {
require.Equal(t, true, clicked)
}
})
}
}

View file

@ -0,0 +1,9 @@
//go:build windows
package menu
import "github.com/wailsapp/wails/v2/internal/platform/win32"
type Menu struct {
menu win32.HMENU
}

View file

@ -0,0 +1,31 @@
package platform
import (
"github.com/wailsapp/wails/v2/internal/platform/systray"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
)
import "github.com/samber/lo"
type SysTray interface {
// SetTitle sets the title of the tray menu
SetTitle(title string)
SetTooltip(tooltip string) error
Show() error
Hide() error
Run() error
Close()
SetMenu(menu *menu.Menu) error
SetIcons(lightModeIcon, darkModeIcon *options.SystemTrayIcon) error
Update() error
OnLeftClick(func())
OnRightClick(func())
OnLeftDoubleClick(func())
OnRightDoubleClick(func())
OnMenuClose(func())
OnMenuOpen(func())
}
func NewSysTray() SysTray {
return lo.Must(systray.New())
}

View file

@ -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], &currentRadioGroup)
}
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], &currentRadioGroup)
}
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
}

View file

@ -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
}

View file

@ -0,0 +1,856 @@
package win32
import (
"fmt"
"github.com/wailsapp/wails/v2/internal/system/operatingsystem"
"golang.org/x/sys/windows"
"syscall"
"unsafe"
)
var (
modKernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetModuleHandle = modKernel32.NewProc("GetModuleHandleW")
moduser32 = syscall.NewLazyDLL("user32.dll")
procRegisterClassEx = moduser32.NewProc("RegisterClassExW")
procLoadIcon = moduser32.NewProc("LoadIconW")
procLoadCursor = moduser32.NewProc("LoadCursorW")
procCreateWindowEx = moduser32.NewProc("CreateWindowExW")
procPostMessage = moduser32.NewProc("PostMessageW")
procGetCursorPos = moduser32.NewProc("GetCursorPos")
procSetForegroundWindow = moduser32.NewProc("SetForegroundWindow")
procCreatePopupMenu = moduser32.NewProc("CreatePopupMenu")
procTrackPopupMenu = moduser32.NewProc("TrackPopupMenu")
procDestroyMenu = moduser32.NewProc("DestroyMenu")
procAppendMenuW = moduser32.NewProc("AppendMenuW")
procCheckMenuItem = moduser32.NewProc("CheckMenuItem")
procCheckMenuRadioItem = moduser32.NewProc("CheckMenuRadioItem")
procCreateIconFromResourceEx = moduser32.NewProc("CreateIconFromResourceEx")
procGetMessageW = moduser32.NewProc("GetMessageW")
procIsDialogMessage = moduser32.NewProc("IsDialogMessageW")
procTranslateMessage = moduser32.NewProc("TranslateMessage")
procDispatchMessage = moduser32.NewProc("DispatchMessageW")
procPostQuitMessage = moduser32.NewProc("PostQuitMessage")
procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW")
procSetWindowCompositionAttribute = moduser32.NewProc("SetWindowCompositionAttribute")
procGetKeyState = moduser32.NewProc("GetKeyState")
procCreateAcceleratorTable = moduser32.NewProc("CreateAcceleratorTableW")
procTranslateAccelerator = moduser32.NewProc("TranslateAcceleratorW")
modshell32 = syscall.NewLazyDLL("shell32.dll")
procShellNotifyIcon = modshell32.NewProc("Shell_NotifyIconW")
moddwmapi = syscall.NewLazyDLL("dwmapi.dll")
procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute")
moduxtheme = syscall.NewLazyDLL("uxtheme.dll")
procSetWindowTheme = moduxtheme.NewProc("SetWindowTheme")
AllowDarkModeForWindow func(HWND, bool) uintptr
SetPreferredAppMode func(int32) uintptr
)
type PreferredAppMode = int32
const (
PreferredAppModeDefault PreferredAppMode = iota
PreferredAppModeAllowDark
PreferredAppModeForceDark
PreferredAppModeForceLight
PreferredAppModeMax
)
/*
RtlGetNtVersionNumbers = void (LPDWORD major, LPDWORD minor, LPDWORD build) // 1809 17763
ShouldAppsUseDarkMode = bool () // ordinal 132
AllowDarkModeForWindow = bool (HWND hWnd, bool allow) // ordinal 133
AllowDarkModeForApp = bool (bool allow) // ordinal 135, removed since 18334
FlushMenuThemes = void () // ordinal 136
RefreshImmersiveColorPolicyState = void () // ordinal 104
IsDarkModeAllowedForWindow = bool (HWND hWnd) // ordinal 137
GetIsImmersiveColorUsingHighContrast = bool (IMMERSIVE_HC_CACHE_MODE mode) // ordinal 106
OpenNcThemeData = HTHEME (HWND hWnd, LPCWSTR pszClassList) // ordinal 49
// Insider 18290
ShouldSystemUseDarkMode = bool () // ordinal 138
// Insider 18334
SetPreferredAppMode = PreferredAppMode (PreferredAppMode appMode) // ordinal 135, since 18334
IsDarkModeAllowedForApp = bool () // ordinal 139
*/
func init() {
if IsWindowsVersionAtLeast(10, 0, 18334) {
// AllowDarkModeForWindow is only available on Windows 10+
uxtheme, err := windows.LoadLibrary("uxtheme.dll")
if err == nil {
procAllowDarkModeForWindow, err := windows.GetProcAddressByOrdinal(uxtheme, uintptr(133))
if err == nil {
AllowDarkModeForWindow = func(hwnd HWND, allow bool) uintptr {
var allowInt int32
if allow {
allowInt = 1
}
ret, _, _ := syscall.SyscallN(procAllowDarkModeForWindow, uintptr(hwnd), uintptr(allowInt))
return ret
}
}
}
// SetPreferredAppMode is only available on Windows 10+
procSetPreferredAppMode, err := windows.GetProcAddressByOrdinal(uxtheme, uintptr(135))
if err == nil {
SetPreferredAppMode = func(mode int32) uintptr {
ret, _, _ := syscall.SyscallN(procSetPreferredAppMode, uintptr(mode))
return ret
}
SetPreferredAppMode(PreferredAppModeAllowDark)
}
}
}
type HANDLE uintptr
type HINSTANCE = HANDLE
type HICON = HANDLE
type HCURSOR = HANDLE
type HBRUSH = HANDLE
type HWND = HANDLE
type HMENU = HANDLE
type DWORD = uint32
type ATOM uint16
type MenuID uint16
const (
WM_APP = 32768
WM_ACTIVATE = 6
WM_ACTIVATEAPP = 28
WM_AFXFIRST = 864
WM_AFXLAST = 895
WM_ASKCBFORMATNAME = 780
WM_CANCELJOURNAL = 75
WM_CANCELMODE = 31
WM_CAPTURECHANGED = 533
WM_CHANGECBCHAIN = 781
WM_CHAR = 258
WM_CHARTOITEM = 47
WM_CHILDACTIVATE = 34
WM_CLEAR = 771
WM_CLOSE = 16
WM_COMMAND = 273
WM_COMMNOTIFY = 68 /* OBSOLETE */
WM_COMPACTING = 65
WM_COMPAREITEM = 57
WM_CONTEXTMENU = 123
WM_COPY = 769
WM_COPYDATA = 74
WM_CREATE = 1
WM_CTLCOLORBTN = 309
WM_CTLCOLORDLG = 310
WM_CTLCOLOREDIT = 307
WM_CTLCOLORLISTBOX = 308
WM_CTLCOLORMSGBOX = 306
WM_CTLCOLORSCROLLBAR = 311
WM_CTLCOLORSTATIC = 312
WM_CUT = 768
WM_DEADCHAR = 259
WM_DELETEITEM = 45
WM_DESTROY = 2
WM_DESTROYCLIPBOARD = 775
WM_DEVICECHANGE = 537
WM_DEVMODECHANGE = 27
WM_DISPLAYCHANGE = 126
WM_DRAWCLIPBOARD = 776
WM_DRAWITEM = 43
WM_DROPFILES = 563
WM_ENABLE = 10
WM_ENDSESSION = 22
WM_ENTERIDLE = 289
WM_ENTERMENULOOP = 529
WM_ENTERSIZEMOVE = 561
WM_ERASEBKGND = 20
WM_EXITMENULOOP = 530
WM_EXITSIZEMOVE = 562
WM_FONTCHANGE = 29
WM_GETDLGCODE = 135
WM_GETFONT = 49
WM_GETHOTKEY = 51
WM_GETICON = 127
WM_GETMINMAXINFO = 36
WM_GETTEXT = 13
WM_GETTEXTLENGTH = 14
WM_HANDHELDFIRST = 856
WM_HANDHELDLAST = 863
WM_HELP = 83
WM_HOTKEY = 786
WM_HSCROLL = 276
WM_HSCROLLCLIPBOARD = 782
WM_ICONERASEBKGND = 39
WM_INITDIALOG = 272
WM_INITMENU = 278
WM_INITMENUPOPUP = 279
WM_INPUT = 0x00FF
WM_INPUTLANGCHANGE = 81
WM_INPUTLANGCHANGEREQUEST = 80
WM_KEYDOWN = 256
WM_KEYUP = 257
WM_KILLFOCUS = 8
WM_MDIACTIVATE = 546
WM_MDICASCADE = 551
WM_MDICREATE = 544
WM_MDIDESTROY = 545
WM_MDIGETACTIVE = 553
WM_MDIICONARRANGE = 552
WM_MDIMAXIMIZE = 549
WM_MDINEXT = 548
WM_MDIREFRESHMENU = 564
WM_MDIRESTORE = 547
WM_MDISETMENU = 560
WM_MDITILE = 550
WM_MEASUREITEM = 44
WM_GETOBJECT = 0x003D
WM_CHANGEUISTATE = 0x0127
WM_UPDATEUISTATE = 0x0128
WM_QUERYUISTATE = 0x0129
WM_UNINITMENUPOPUP = 0x0125
WM_MENURBUTTONUP = 290
WM_MENUCOMMAND = 0x0126
WM_MENUGETOBJECT = 0x0124
WM_MENUDRAG = 0x0123
WM_APPCOMMAND = 0x0319
WM_MENUCHAR = 288
WM_MENUSELECT = 287
WM_MOVE = 3
WM_MOVING = 534
WM_NCACTIVATE = 134
WM_NCCALCSIZE = 131
WM_NCCREATE = 129
WM_NCDESTROY = 130
WM_NCHITTEST = 132
WM_NCLBUTTONDBLCLK = 163
WM_NCLBUTTONDOWN = 161
WM_NCLBUTTONUP = 162
WM_NCMBUTTONDBLCLK = 169
WM_NCMBUTTONDOWN = 167
WM_NCMBUTTONUP = 168
WM_NCXBUTTONDOWN = 171
WM_NCXBUTTONUP = 172
WM_NCXBUTTONDBLCLK = 173
WM_NCMOUSEHOVER = 0x02A0
WM_NCMOUSELEAVE = 0x02A2
WM_NCMOUSEMOVE = 160
WM_NCPAINT = 133
WM_NCRBUTTONDBLCLK = 166
WM_NCRBUTTONDOWN = 164
WM_NCRBUTTONUP = 165
WM_NEXTDLGCTL = 40
WM_NEXTMENU = 531
WM_NOTIFY = 78
WM_NOTIFYFORMAT = 85
WM_NULL = 0
WM_PAINT = 15
WM_PAINTCLIPBOARD = 777
WM_PAINTICON = 38
WM_PALETTECHANGED = 785
WM_PALETTEISCHANGING = 784
WM_PARENTNOTIFY = 528
WM_PASTE = 770
WM_PENWINFIRST = 896
WM_PENWINLAST = 911
WM_POWER = 72
WM_PRINT = 791
WM_PRINTCLIENT = 792
WM_QUERYDRAGICON = 55
WM_QUERYENDSESSION = 17
WM_QUERYNEWPALETTE = 783
WM_QUERYOPEN = 19
WM_QUEUESYNC = 35
WM_QUIT = 18
WM_RENDERALLFORMATS = 774
WM_RENDERFORMAT = 773
WM_SETCURSOR = 32
WM_SETFOCUS = 7
WM_SETFONT = 48
WM_SETHOTKEY = 50
WM_SETICON = 128
WM_SETREDRAW = 11
WM_SETTEXT = 12
WM_SETTINGCHANGE = 26
WM_SHOWWINDOW = 24
WM_SIZE = 5
WM_SIZECLIPBOARD = 779
WM_SIZING = 532
WM_SPOOLERSTATUS = 42
WM_STYLECHANGED = 125
WM_STYLECHANGING = 124
WM_SYSCHAR = 262
WM_SYSCOLORCHANGE = 21
WM_SYSCOMMAND = 274
WM_SYSDEADCHAR = 263
WM_SYSKEYDOWN = 260
WM_SYSKEYUP = 261
WM_TCARD = 82
WM_THEMECHANGED = 794
WM_TIMECHANGE = 30
WM_TIMER = 275
WM_UNDO = 772
WM_USER = 1024
WM_USERCHANGED = 84
WM_VKEYTOITEM = 46
WM_VSCROLL = 277
WM_VSCROLLCLIPBOARD = 778
WM_WINDOWPOSCHANGED = 71
WM_WINDOWPOSCHANGING = 70
WM_WININICHANGE = 26
WM_KEYFIRST = 256
WM_KEYLAST = 264
WM_SYNCPAINT = 136
WM_MOUSEACTIVATE = 33
WM_MOUSEMOVE = 512
WM_LBUTTONDOWN = 513
WM_LBUTTONUP = 514
WM_LBUTTONDBLCLK = 515
WM_RBUTTONDOWN = 516
WM_RBUTTONUP = 517
WM_RBUTTONDBLCLK = 518
WM_MBUTTONDOWN = 519
WM_MBUTTONUP = 520
WM_MBUTTONDBLCLK = 521
WM_MOUSEWHEEL = 522
WM_MOUSEFIRST = 512
WM_XBUTTONDOWN = 523
WM_XBUTTONUP = 524
WM_XBUTTONDBLCLK = 525
WM_MOUSELAST = 525
WM_MOUSEHOVER = 0x2A1
WM_MOUSELEAVE = 0x2A3
WM_CLIPBOARDUPDATE = 0x031D
WS_EX_APPWINDOW = 0x00040000
WS_OVERLAPPEDWINDOW = 0x00000000 | 0x00C00000 | 0x00080000 | 0x00040000 | 0x00020000 | 0x00010000
WS_EX_NOREDIRECTIONBITMAP = 0x00200000
CW_USEDEFAULT = 0x80000000
NIM_ADD = 0x00000000
NIM_MODIFY = 0x00000001
NIM_DELETE = 0x00000002
NIM_SETVERSION = 0x00000004
NIF_MESSAGE = 0x00000001
NIF_ICON = 0x00000002
NIF_TIP = 0x00000004
NIF_STATE = 0x00000008
NIF_INFO = 0x00000010
NIS_HIDDEN = 0x00000001
NIIF_NONE = 0x00000000
NIIF_INFO = 0x00000001
NIIF_WARNING = 0x00000002
NIIF_ERROR = 0x00000003
NIIF_USER = 0x00000004
NIIF_NOSOUND = 0x00000010
NIIF_LARGE_ICON = 0x00000020
NIIF_RESPECT_QUIET_TIME = 0x00000080
NIIF_ICON_MASK = 0x0000000F
IMAGE_BITMAP = 0
IMAGE_ICON = 1
LR_LOADFROMFILE = 0x00000010
LR_DEFAULTSIZE = 0x00000040
IDC_ARROW = 32512
COLOR_WINDOW = 5
COLOR_BTNFACE = 15
GWLP_USERDATA = -21
WS_CLIPSIBLINGS = 0x04000000
WS_EX_CONTROLPARENT = 0x00010000
HWND_MESSAGE = ^HWND(2)
NOTIFYICON_VERSION = 4
IDI_APPLICATION = 32512
MenuItemMsgID = WM_APP + 1024
NotifyIconMessageId = WM_APP + iota
MF_STRING = 0x00000000
MF_ENABLED = 0x00000000
MF_GRAYED = 0x00000001
MF_DISABLED = 0x00000002
MF_SEPARATOR = 0x00000800
MF_UNCHECKED = 0x00000000
MF_CHECKED = 0x00000008
MF_POPUP = 0x00000010
MF_MENUBARBREAK = 0x00000020
MF_BYCOMMAND = 0x00000000
TPM_LEFTALIGN = 0x0000
CS_VREDRAW = 0x0001
CS_HREDRAW = 0x0002
)
func WMMessageToString(msg uintptr) string {
// Convert windows message to string
switch msg {
case WM_APP:
return "WM_APP"
case WM_ACTIVATE:
return "WM_ACTIVATE"
case WM_ACTIVATEAPP:
return "WM_ACTIVATEAPP"
case WM_AFXFIRST:
return "WM_AFXFIRST"
case WM_AFXLAST:
return "WM_AFXLAST"
case WM_ASKCBFORMATNAME:
return "WM_ASKCBFORMATNAME"
case WM_CANCELJOURNAL:
return "WM_CANCELJOURNAL"
case WM_CANCELMODE:
return "WM_CANCELMODE"
case WM_CAPTURECHANGED:
return "WM_CAPTURECHANGED"
case WM_CHANGECBCHAIN:
return "WM_CHANGECBCHAIN"
case WM_CHAR:
return "WM_CHAR"
case WM_CHARTOITEM:
return "WM_CHARTOITEM"
case WM_CHILDACTIVATE:
return "WM_CHILDACTIVATE"
case WM_CLEAR:
return "WM_CLEAR"
case WM_CLOSE:
return "WM_CLOSE"
case WM_COMMAND:
return "WM_COMMAND"
case WM_COMMNOTIFY /* OBSOLETE */ :
return "WM_COMMNOTIFY"
case WM_COMPACTING:
return "WM_COMPACTING"
case WM_COMPAREITEM:
return "WM_COMPAREITEM"
case WM_CONTEXTMENU:
return "WM_CONTEXTMENU"
case WM_COPY:
return "WM_COPY"
case WM_COPYDATA:
return "WM_COPYDATA"
case WM_CREATE:
return "WM_CREATE"
case WM_CTLCOLORBTN:
return "WM_CTLCOLORBTN"
case WM_CTLCOLORDLG:
return "WM_CTLCOLORDLG"
case WM_CTLCOLOREDIT:
return "WM_CTLCOLOREDIT"
case WM_CTLCOLORLISTBOX:
return "WM_CTLCOLORLISTBOX"
case WM_CTLCOLORMSGBOX:
return "WM_CTLCOLORMSGBOX"
case WM_CTLCOLORSCROLLBAR:
return "WM_CTLCOLORSCROLLBAR"
case WM_CTLCOLORSTATIC:
return "WM_CTLCOLORSTATIC"
case WM_CUT:
return "WM_CUT"
case WM_DEADCHAR:
return "WM_DEADCHAR"
case WM_DELETEITEM:
return "WM_DELETEITEM"
case WM_DESTROY:
return "WM_DESTROY"
case WM_DESTROYCLIPBOARD:
return "WM_DESTROYCLIPBOARD"
case WM_DEVICECHANGE:
return "WM_DEVICECHANGE"
case WM_DEVMODECHANGE:
return "WM_DEVMODECHANGE"
case WM_DISPLAYCHANGE:
return "WM_DISPLAYCHANGE"
case WM_DRAWCLIPBOARD:
return "WM_DRAWCLIPBOARD"
case WM_DRAWITEM:
return "WM_DRAWITEM"
case WM_DROPFILES:
return "WM_DROPFILES"
case WM_ENABLE:
return "WM_ENABLE"
case WM_ENDSESSION:
return "WM_ENDSESSION"
case WM_ENTERIDLE:
return "WM_ENTERIDLE"
case WM_ENTERMENULOOP:
return "WM_ENTERMENULOOP"
case WM_ENTERSIZEMOVE:
return "WM_ENTERSIZEMOVE"
case WM_ERASEBKGND:
return "WM_ERASEBKGND"
case WM_EXITMENULOOP:
return "WM_EXITMENULOOP"
case WM_EXITSIZEMOVE:
return "WM_EXITSIZEMOVE"
case WM_FONTCHANGE:
return "WM_FONTCHANGE"
case WM_GETDLGCODE:
return "WM_GETDLGCODE"
case WM_GETFONT:
return "WM_GETFONT"
case WM_GETHOTKEY:
return "WM_GETHOTKEY"
case WM_GETICON:
return "WM_GETICON"
case WM_GETMINMAXINFO:
return "WM_GETMINMAXINFO"
case WM_GETTEXT:
return "WM_GETTEXT"
case WM_GETTEXTLENGTH:
return "WM_GETTEXTLENGTH"
case WM_HANDHELDFIRST:
return "WM_HANDHELDFIRST"
case WM_HANDHELDLAST:
return "WM_HANDHELDLAST"
case WM_HELP:
return "WM_HELP"
case WM_HOTKEY:
return "WM_HOTKEY"
case WM_HSCROLL:
return "WM_HSCROLL"
case WM_HSCROLLCLIPBOARD:
return "WM_HSCROLLCLIPBOARD"
case WM_ICONERASEBKGND:
return "WM_ICONERASEBKGND"
case WM_INITDIALOG:
return "WM_INITDIALOG"
case WM_INITMENU:
return "WM_INITMENU"
case WM_INITMENUPOPUP:
return "WM_INITMENUPOPUP"
case WM_INPUT:
return "WM_INPUT"
case WM_INPUTLANGCHANGE:
return "WM_INPUTLANGCHANGE"
case WM_INPUTLANGCHANGEREQUEST:
return "WM_INPUTLANGCHANGEREQUEST"
case WM_KEYDOWN:
return "WM_KEYDOWN"
case WM_KEYUP:
return "WM_KEYUP"
case WM_KILLFOCUS:
return "WM_KILLFOCUS"
case WM_MDIACTIVATE:
return "WM_MDIACTIVATE"
case WM_MDICASCADE:
return "WM_MDICASCADE"
case WM_MDICREATE:
return "WM_MDICREATE"
case WM_MDIDESTROY:
return "WM_MDIDESTROY"
case WM_MDIGETACTIVE:
return "WM_MDIGETACTIVE"
case WM_MDIICONARRANGE:
return "WM_MDIICONARRANGE"
case WM_MDIMAXIMIZE:
return "WM_MDIMAXIMIZE"
case WM_MDINEXT:
return "WM_MDINEXT"
case WM_MDIREFRESHMENU:
return "WM_MDIREFRESHMENU"
case WM_MDIRESTORE:
return "WM_MDIRESTORE"
case WM_MDISETMENU:
return "WM_MDISETMENU"
case WM_MDITILE:
return "WM_MDITILE"
case WM_MEASUREITEM:
return "WM_MEASUREITEM"
case WM_GETOBJECT:
return "WM_GETOBJECT"
case WM_CHANGEUISTATE:
return "WM_CHANGEUISTATE"
case WM_UPDATEUISTATE:
return "WM_UPDATEUISTATE"
case WM_QUERYUISTATE:
return "WM_QUERYUISTATE"
case WM_UNINITMENUPOPUP:
return "WM_UNINITMENUPOPUP"
case WM_MENURBUTTONUP:
return "WM_MENURBUTTONUP"
case WM_MENUCOMMAND:
return "WM_MENUCOMMAND"
case WM_MENUGETOBJECT:
return "WM_MENUGETOBJECT"
case WM_MENUDRAG:
return "WM_MENUDRAG"
case WM_APPCOMMAND:
return "WM_APPCOMMAND"
case WM_MENUCHAR:
return "WM_MENUCHAR"
case WM_MENUSELECT:
return "WM_MENUSELECT"
case WM_MOVE:
return "WM_MOVE"
case WM_MOVING:
return "WM_MOVING"
case WM_NCACTIVATE:
return "WM_NCACTIVATE"
case WM_NCCALCSIZE:
return "WM_NCCALCSIZE"
case WM_NCCREATE:
return "WM_NCCREATE"
case WM_NCDESTROY:
return "WM_NCDESTROY"
case WM_NCHITTEST:
return "WM_NCHITTEST"
case WM_NCLBUTTONDBLCLK:
return "WM_NCLBUTTONDBLCLK"
case WM_NCLBUTTONDOWN:
return "WM_NCLBUTTONDOWN"
case WM_NCLBUTTONUP:
return "WM_NCLBUTTONUP"
case WM_NCMBUTTONDBLCLK:
return "WM_NCMBUTTONDBLCLK"
case WM_NCMBUTTONDOWN:
return "WM_NCMBUTTONDOWN"
case WM_NCMBUTTONUP:
return "WM_NCMBUTTONUP"
case WM_NCXBUTTONDOWN:
return "WM_NCXBUTTONDOWN"
case WM_NCXBUTTONUP:
return "WM_NCXBUTTONUP"
case WM_NCXBUTTONDBLCLK:
return "WM_NCXBUTTONDBLCLK"
case WM_NCMOUSEHOVER:
return "WM_NCMOUSEHOVER"
case WM_NCMOUSELEAVE:
return "WM_NCMOUSELEAVE"
case WM_NCMOUSEMOVE:
return "WM_NCMOUSEMOVE"
case WM_NCPAINT:
return "WM_NCPAINT"
case WM_NCRBUTTONDBLCLK:
return "WM_NCRBUTTONDBLCLK"
case WM_NCRBUTTONDOWN:
return "WM_NCRBUTTONDOWN"
case WM_NCRBUTTONUP:
return "WM_NCRBUTTONUP"
case WM_NEXTDLGCTL:
return "WM_NEXTDLGCTL"
case WM_NEXTMENU:
return "WM_NEXTMENU"
case WM_NOTIFY:
return "WM_NOTIFY"
case WM_NOTIFYFORMAT:
return "WM_NOTIFYFORMAT"
case WM_NULL:
return "WM_NULL"
case WM_PAINT:
return "WM_PAINT"
case WM_PAINTCLIPBOARD:
return "WM_PAINTCLIPBOARD"
case WM_PAINTICON:
return "WM_PAINTICON"
case WM_PALETTECHANGED:
return "WM_PALETTECHANGED"
case WM_PALETTEISCHANGING:
return "WM_PALETTEISCHANGING"
case WM_PARENTNOTIFY:
return "WM_PARENTNOTIFY"
case WM_PASTE:
return "WM_PASTE"
case WM_PENWINFIRST:
return "WM_PENWINFIRST"
case WM_PENWINLAST:
return "WM_PENWINLAST"
case WM_POWER:
return "WM_POWER"
case WM_PRINT:
return "WM_PRINT"
case WM_PRINTCLIENT:
return "WM_PRINTCLIENT"
case WM_QUERYDRAGICON:
return "WM_QUERYDRAGICON"
case WM_QUERYENDSESSION:
return "WM_QUERYENDSESSION"
case WM_QUERYNEWPALETTE:
return "WM_QUERYNEWPALETTE"
case WM_QUERYOPEN:
return "WM_QUERYOPEN"
case WM_QUEUESYNC:
return "WM_QUEUESYNC"
case WM_QUIT:
return "WM_QUIT"
case WM_RENDERALLFORMATS:
return "WM_RENDERALLFORMATS"
case WM_RENDERFORMAT:
return "WM_RENDERFORMAT"
case WM_SETCURSOR:
return "WM_SETCURSOR"
case WM_SETFOCUS:
return "WM_SETFOCUS"
case WM_SETFONT:
return "WM_SETFONT"
case WM_SETHOTKEY:
return "WM_SETHOTKEY"
case WM_SETICON:
return "WM_SETICON"
case WM_SETREDRAW:
return "WM_SETREDRAW"
case WM_SETTEXT:
return "WM_SETTEXT"
case WM_SETTINGCHANGE:
return "WM_SETTINGCHANGE"
case WM_SHOWWINDOW:
return "WM_SHOWWINDOW"
case WM_SIZE:
return "WM_SIZE"
case WM_SIZECLIPBOARD:
return "WM_SIZECLIPBOARD"
case WM_SIZING:
return "WM_SIZING"
case WM_SPOOLERSTATUS:
return "WM_SPOOLERSTATUS"
case WM_STYLECHANGED:
return "WM_STYLECHANGED"
case WM_STYLECHANGING:
return "WM_STYLECHANGING"
case WM_SYSCHAR:
return "WM_SYSCHAR"
case WM_SYSCOLORCHANGE:
return "WM_SYSCOLORCHANGE"
case WM_SYSCOMMAND:
return "WM_SYSCOMMAND"
case WM_SYSDEADCHAR:
return "WM_SYSDEADCHAR"
case WM_SYSKEYDOWN:
return "WM_SYSKEYDOWN"
case WM_SYSKEYUP:
return "WM_SYSKEYUP"
case WM_TCARD:
return "WM_TCARD"
case WM_THEMECHANGED:
return "WM_THEMECHANGED"
case WM_TIMECHANGE:
return "WM_TIMECHANGE"
case WM_TIMER:
return "WM_TIMER"
case WM_UNDO:
return "WM_UNDO"
case WM_USER:
return "WM_USER"
case WM_USERCHANGED:
return "WM_USERCHANGED"
case WM_VKEYTOITEM:
return "WM_VKEYTOITEM"
case WM_VSCROLL:
return "WM_VSCROLL"
case WM_VSCROLLCLIPBOARD:
return "WM_VSCROLLCLIPBOARD"
case WM_WINDOWPOSCHANGED:
return "WM_WINDOWPOSCHANGED"
case WM_WINDOWPOSCHANGING:
return "WM_WINDOWPOSCHANGING"
case WM_KEYLAST:
return "WM_KEYLAST"
case WM_SYNCPAINT:
return "WM_SYNCPAINT"
case WM_MOUSEACTIVATE:
return "WM_MOUSEACTIVATE"
case WM_MOUSEMOVE:
return "WM_MOUSEMOVE"
case WM_LBUTTONDOWN:
return "WM_LBUTTONDOWN"
case WM_LBUTTONUP:
return "WM_LBUTTONUP"
case WM_LBUTTONDBLCLK:
return "WM_LBUTTONDBLCLK"
case WM_RBUTTONDOWN:
return "WM_RBUTTONDOWN"
case WM_RBUTTONUP:
return "WM_RBUTTONUP"
case WM_RBUTTONDBLCLK:
return "WM_RBUTTONDBLCLK"
case WM_MBUTTONDOWN:
return "WM_MBUTTONDOWN"
case WM_MBUTTONUP:
return "WM_MBUTTONUP"
case WM_MBUTTONDBLCLK:
return "WM_MBUTTONDBLCLK"
case WM_MOUSEWHEEL:
return "WM_MOUSEWHEEL"
case WM_XBUTTONDOWN:
return "WM_XBUTTONDOWN"
case WM_XBUTTONUP:
return "WM_XBUTTONUP"
case WM_MOUSELAST:
return "WM_MOUSELAST"
case WM_MOUSEHOVER:
return "WM_MOUSEHOVER"
case WM_MOUSELEAVE:
return "WM_MOUSELEAVE"
case WM_CLIPBOARDUPDATE:
return "WM_CLIPBOARDUPDATE"
default:
return fmt.Sprintf("0x%08x", msg)
}
}
var windowsVersion, _ = operatingsystem.GetWindowsVersionInfo()
func IsWindowsVersionAtLeast(major, minor, buildNumber int) bool {
return windowsVersion.Major >= major &&
windowsVersion.Minor >= minor &&
windowsVersion.Build >= buildNumber
}
type WindowProc func(hwnd HWND, msg uint32, wparam, lparam uintptr) uintptr
func GetModuleHandle(value uintptr) uintptr {
result, _, _ := procGetModuleHandle.Call(value)
return result
}
func GetMessage(msg *MSG) uintptr {
rt, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)
return rt
}
func PostMessage(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr {
ret, _, _ := procPostMessage.Call(
uintptr(hwnd),
uintptr(msg),
wParam,
lParam)
return ret
}
func ShellNotifyIcon(cmd uintptr, nid *NOTIFYICONDATA) bool {
ret, _, _ := procShellNotifyIcon.Call(cmd, uintptr(unsafe.Pointer(nid)))
return ret == 1
}
func IsDialogMessage(hwnd HWND, msg *MSG) uintptr {
ret, _, _ := procIsDialogMessage.Call(uintptr(hwnd), uintptr(unsafe.Pointer(msg)))
return ret
}
func TranslateMessage(msg *MSG) uintptr {
ret, _, _ := procTranslateMessage.Call(uintptr(unsafe.Pointer(msg)))
return ret
}
func DispatchMessage(msg *MSG) uintptr {
ret, _, _ := procDispatchMessage.Call(uintptr(unsafe.Pointer(msg)))
return ret
}
func PostQuitMessage(exitCode int32) {
procPostQuitMessage.Call(uintptr(exitCode))
}
func LoHiWords(input uint32) (uint16, uint16) {
return uint16(input & 0xffff), uint16(input >> 16 & 0xffff)
}

View file

@ -0,0 +1,9 @@
package win32
import "unsafe"
func GetCursorPos() (x, y int, ok bool) {
pt := POINT{}
ret, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
return int(pt.X), int(pt.Y), ret != 0
}

View file

@ -0,0 +1,39 @@
package win32
import (
"unsafe"
)
func CreateIconFromResourceEx(presbits uintptr, dwResSize uint32, isIcon bool, version uint32, cxDesired int, cyDesired int, flags uint) (uintptr, error) {
icon := 0
if isIcon {
icon = 1
}
r, _, err := procCreateIconFromResourceEx.Call(
presbits,
uintptr(dwResSize),
uintptr(icon),
uintptr(version),
uintptr(cxDesired),
uintptr(cyDesired),
uintptr(flags),
)
if r == 0 {
return 0, err
}
return r, nil
}
// CreateHIconFromPNG creates a HICON from a PNG file
func CreateHIconFromPNG(pngData []byte) (HICON, error) {
icon, err := CreateIconFromResourceEx(
uintptr(unsafe.Pointer(&pngData[0])),
uint32(len(pngData)),
true,
0x00030000,
0,
0,
LR_DEFAULTSIZE)
return HICON(icon), err
}

View file

@ -0,0 +1,808 @@
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package win32
import (
"bytes"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
"strings"
"unsafe"
)
type Key uint16
func (k Key) String() string {
return key2string[k]
}
// Virtual key codes
const (
VK_LBUTTON = 1
VK_RBUTTON = 2
VK_CANCEL = 3
VK_MBUTTON = 4
VK_XBUTTON1 = 5
VK_XBUTTON2 = 6
VK_BACK = 8
VK_TAB = 9
VK_CLEAR = 12
VK_RETURN = 13
VK_SHIFT = 16
VK_CONTROL = 17
VK_MENU = 18
VK_PAUSE = 19
VK_CAPITAL = 20
VK_KANA = 0x15
VK_HANGEUL = 0x15
VK_HANGUL = 0x15
VK_JUNJA = 0x17
VK_FINAL = 0x18
VK_HANJA = 0x19
VK_KANJI = 0x19
VK_ESCAPE = 0x1B
VK_CONVERT = 0x1C
VK_NONCONVERT = 0x1D
VK_ACCEPT = 0x1E
VK_MODECHANGE = 0x1F
VK_SPACE = 32
VK_PRIOR = 33
VK_NEXT = 34
VK_END = 35
VK_HOME = 36
VK_LEFT = 37
VK_UP = 38
VK_RIGHT = 39
VK_DOWN = 40
VK_SELECT = 41
VK_PRINT = 42
VK_EXECUTE = 43
VK_SNAPSHOT = 44
VK_INSERT = 45
VK_DELETE = 46
VK_HELP = 47
VK_LWIN = 0x5B
VK_RWIN = 0x5C
VK_APPS = 0x5D
VK_SLEEP = 0x5F
VK_NUMPAD0 = 0x60
VK_NUMPAD1 = 0x61
VK_NUMPAD2 = 0x62
VK_NUMPAD3 = 0x63
VK_NUMPAD4 = 0x64
VK_NUMPAD5 = 0x65
VK_NUMPAD6 = 0x66
VK_NUMPAD7 = 0x67
VK_NUMPAD8 = 0x68
VK_NUMPAD9 = 0x69
VK_MULTIPLY = 0x6A
VK_ADD = 0x6B
VK_SEPARATOR = 0x6C
VK_SUBTRACT = 0x6D
VK_DECIMAL = 0x6E
VK_DIVIDE = 0x6F
VK_F1 = 0x70
VK_F2 = 0x71
VK_F3 = 0x72
VK_F4 = 0x73
VK_F5 = 0x74
VK_F6 = 0x75
VK_F7 = 0x76
VK_F8 = 0x77
VK_F9 = 0x78
VK_F10 = 0x79
VK_F11 = 0x7A
VK_F12 = 0x7B
VK_F13 = 0x7C
VK_F14 = 0x7D
VK_F15 = 0x7E
VK_F16 = 0x7F
VK_F17 = 0x80
VK_F18 = 0x81
VK_F19 = 0x82
VK_F20 = 0x83
VK_F21 = 0x84
VK_F22 = 0x85
VK_F23 = 0x86
VK_F24 = 0x87
VK_NUMLOCK = 0x90
VK_SCROLL = 0x91
VK_LSHIFT = 0xA0
VK_RSHIFT = 0xA1
VK_LCONTROL = 0xA2
VK_RCONTROL = 0xA3
VK_LMENU = 0xA4
VK_RMENU = 0xA5
VK_BROWSER_BACK = 0xA6
VK_BROWSER_FORWARD = 0xA7
VK_BROWSER_REFRESH = 0xA8
VK_BROWSER_STOP = 0xA9
VK_BROWSER_SEARCH = 0xAA
VK_BROWSER_FAVORITES = 0xAB
VK_BROWSER_HOME = 0xAC
VK_VOLUME_MUTE = 0xAD
VK_VOLUME_DOWN = 0xAE
VK_VOLUME_UP = 0xAF
VK_MEDIA_NEXT_TRACK = 0xB0
VK_MEDIA_PREV_TRACK = 0xB1
VK_MEDIA_STOP = 0xB2
VK_MEDIA_PLAY_PAUSE = 0xB3
VK_LAUNCH_MAIL = 0xB4
VK_LAUNCH_MEDIA_SELECT = 0xB5
VK_LAUNCH_APP1 = 0xB6
VK_LAUNCH_APP2 = 0xB7
VK_OEM_1 = 0xBA
VK_OEM_PLUS = 0xBB
VK_OEM_COMMA = 0xBC
VK_OEM_MINUS = 0xBD
VK_OEM_PERIOD = 0xBE
VK_OEM_2 = 0xBF
VK_OEM_3 = 0xC0
VK_OEM_4 = 0xDB
VK_OEM_5 = 0xDC
VK_OEM_6 = 0xDD
VK_OEM_7 = 0xDE
VK_OEM_8 = 0xDF
VK_OEM_102 = 0xE2
VK_PROCESSKEY = 0xE5
VK_PACKET = 0xE7
VK_ATTN = 0xF6
VK_CRSEL = 0xF7
VK_EXSEL = 0xF8
VK_EREOF = 0xF9
VK_PLAY = 0xFA
VK_ZOOM = 0xFB
VK_NONAME = 0xFC
VK_PA1 = 0xFD
VK_OEM_CLEAR = 0xFE
)
const (
KeyLButton Key = VK_LBUTTON
KeyRButton Key = VK_RBUTTON
KeyCancel Key = VK_CANCEL
KeyMButton Key = VK_MBUTTON
KeyXButton1 Key = VK_XBUTTON1
KeyXButton2 Key = VK_XBUTTON2
KeyBack Key = VK_BACK
KeyTab Key = VK_TAB
KeyClear Key = VK_CLEAR
KeyReturn Key = VK_RETURN
KeyShift Key = VK_SHIFT
KeyControl Key = VK_CONTROL
KeyAlt Key = VK_MENU
KeyMenu Key = VK_MENU
KeyPause Key = VK_PAUSE
KeyCapital Key = VK_CAPITAL
KeyKana Key = VK_KANA
KeyHangul Key = VK_HANGUL
KeyJunja Key = VK_JUNJA
KeyFinal Key = VK_FINAL
KeyHanja Key = VK_HANJA
KeyKanji Key = VK_KANJI
KeyEscape Key = VK_ESCAPE
KeyConvert Key = VK_CONVERT
KeyNonconvert Key = VK_NONCONVERT
KeyAccept Key = VK_ACCEPT
KeyModeChange Key = VK_MODECHANGE
KeySpace Key = VK_SPACE
KeyPrior Key = VK_PRIOR
KeyNext Key = VK_NEXT
KeyEnd Key = VK_END
KeyHome Key = VK_HOME
KeyLeft Key = VK_LEFT
KeyUp Key = VK_UP
KeyRight Key = VK_RIGHT
KeyDown Key = VK_DOWN
KeySelect Key = VK_SELECT
KeyPrint Key = VK_PRINT
KeyExecute Key = VK_EXECUTE
KeySnapshot Key = VK_SNAPSHOT
KeyInsert Key = VK_INSERT
KeyDelete Key = VK_DELETE
KeyHelp Key = VK_HELP
Key0 Key = 0x30
Key1 Key = 0x31
Key2 Key = 0x32
Key3 Key = 0x33
Key4 Key = 0x34
Key5 Key = 0x35
Key6 Key = 0x36
Key7 Key = 0x37
Key8 Key = 0x38
Key9 Key = 0x39
KeyA Key = 0x41
KeyB Key = 0x42
KeyC Key = 0x43
KeyD Key = 0x44
KeyE Key = 0x45
KeyF Key = 0x46
KeyG Key = 0x47
KeyH Key = 0x48
KeyI Key = 0x49
KeyJ Key = 0x4A
KeyK Key = 0x4B
KeyL Key = 0x4C
KeyM Key = 0x4D
KeyN Key = 0x4E
KeyO Key = 0x4F
KeyP Key = 0x50
KeyQ Key = 0x51
KeyR Key = 0x52
KeyS Key = 0x53
KeyT Key = 0x54
KeyU Key = 0x55
KeyV Key = 0x56
KeyW Key = 0x57
KeyX Key = 0x58
KeyY Key = 0x59
KeyZ Key = 0x5A
KeyLWIN Key = VK_LWIN
KeyRWIN Key = VK_RWIN
KeyApps Key = VK_APPS
KeySleep Key = VK_SLEEP
KeyNumpad0 Key = VK_NUMPAD0
KeyNumpad1 Key = VK_NUMPAD1
KeyNumpad2 Key = VK_NUMPAD2
KeyNumpad3 Key = VK_NUMPAD3
KeyNumpad4 Key = VK_NUMPAD4
KeyNumpad5 Key = VK_NUMPAD5
KeyNumpad6 Key = VK_NUMPAD6
KeyNumpad7 Key = VK_NUMPAD7
KeyNumpad8 Key = VK_NUMPAD8
KeyNumpad9 Key = VK_NUMPAD9
KeyMultiply Key = VK_MULTIPLY
KeyAdd Key = VK_ADD
KeySeparator Key = VK_SEPARATOR
KeySubtract Key = VK_SUBTRACT
KeyDecimal Key = VK_DECIMAL
KeyDivide Key = VK_DIVIDE
KeyF1 Key = VK_F1
KeyF2 Key = VK_F2
KeyF3 Key = VK_F3
KeyF4 Key = VK_F4
KeyF5 Key = VK_F5
KeyF6 Key = VK_F6
KeyF7 Key = VK_F7
KeyF8 Key = VK_F8
KeyF9 Key = VK_F9
KeyF10 Key = VK_F10
KeyF11 Key = VK_F11
KeyF12 Key = VK_F12
KeyF13 Key = VK_F13
KeyF14 Key = VK_F14
KeyF15 Key = VK_F15
KeyF16 Key = VK_F16
KeyF17 Key = VK_F17
KeyF18 Key = VK_F18
KeyF19 Key = VK_F19
KeyF20 Key = VK_F20
KeyF21 Key = VK_F21
KeyF22 Key = VK_F22
KeyF23 Key = VK_F23
KeyF24 Key = VK_F24
KeyNumlock Key = VK_NUMLOCK
KeyScroll Key = VK_SCROLL
KeyLShift Key = VK_LSHIFT
KeyRShift Key = VK_RSHIFT
KeyLControl Key = VK_LCONTROL
KeyRControl Key = VK_RCONTROL
KeyLAlt Key = VK_LMENU
KeyLMenu Key = VK_LMENU
KeyRAlt Key = VK_RMENU
KeyRMenu Key = VK_RMENU
KeyBrowserBack Key = VK_BROWSER_BACK
KeyBrowserForward Key = VK_BROWSER_FORWARD
KeyBrowserRefresh Key = VK_BROWSER_REFRESH
KeyBrowserStop Key = VK_BROWSER_STOP
KeyBrowserSearch Key = VK_BROWSER_SEARCH
KeyBrowserFavorites Key = VK_BROWSER_FAVORITES
KeyBrowserHome Key = VK_BROWSER_HOME
KeyVolumeMute Key = VK_VOLUME_MUTE
KeyVolumeDown Key = VK_VOLUME_DOWN
KeyVolumeUp Key = VK_VOLUME_UP
KeyMediaNextTrack Key = VK_MEDIA_NEXT_TRACK
KeyMediaPrevTrack Key = VK_MEDIA_PREV_TRACK
KeyMediaStop Key = VK_MEDIA_STOP
KeyMediaPlayPause Key = VK_MEDIA_PLAY_PAUSE
KeyLaunchMail Key = VK_LAUNCH_MAIL
KeyLaunchMediaSelect Key = VK_LAUNCH_MEDIA_SELECT
KeyLaunchApp1 Key = VK_LAUNCH_APP1
KeyLaunchApp2 Key = VK_LAUNCH_APP2
KeyOEM1 Key = VK_OEM_1
KeyOEMPlus Key = VK_OEM_PLUS
KeyOEMComma Key = VK_OEM_COMMA
KeyOEMMinus Key = VK_OEM_MINUS
KeyOEMPeriod Key = VK_OEM_PERIOD
KeyOEM2 Key = VK_OEM_2
KeyOEM3 Key = VK_OEM_3
KeyOEM4 Key = VK_OEM_4
KeyOEM5 Key = VK_OEM_5
KeyOEM6 Key = VK_OEM_6
KeyOEM7 Key = VK_OEM_7
KeyOEM8 Key = VK_OEM_8
KeyOEM102 Key = VK_OEM_102
KeyProcessKey Key = VK_PROCESSKEY
KeyPacket Key = VK_PACKET
KeyAttn Key = VK_ATTN
KeyCRSel Key = VK_CRSEL
KeyEXSel Key = VK_EXSEL
KeyErEOF Key = VK_EREOF
KeyPlay Key = VK_PLAY
KeyZoom Key = VK_ZOOM
KeyNoName Key = VK_NONAME
KeyPA1 Key = VK_PA1
KeyOEMClear Key = VK_OEM_CLEAR
)
var key2string = map[Key]string{
KeyLButton: "LButton",
KeyRButton: "RButton",
KeyCancel: "Cancel",
KeyMButton: "MButton",
KeyXButton1: "XButton1",
KeyXButton2: "XButton2",
KeyBack: "Back",
KeyTab: "Tab",
KeyClear: "Clear",
KeyReturn: "Return",
KeyShift: "Shift",
KeyControl: "Control",
KeyAlt: "Alt / Menu",
KeyPause: "Pause",
KeyCapital: "Capital",
KeyKana: "Kana / Hangul",
KeyJunja: "Junja",
KeyFinal: "Final",
KeyHanja: "Hanja / Kanji",
KeyEscape: "Escape",
KeyConvert: "Convert",
KeyNonconvert: "Nonconvert",
KeyAccept: "Accept",
KeyModeChange: "ModeChange",
KeySpace: "Space",
KeyPrior: "Prior",
KeyNext: "Next",
KeyEnd: "End",
KeyHome: "Home",
KeyLeft: "Left",
KeyUp: "Up",
KeyRight: "Right",
KeyDown: "Down",
KeySelect: "Select",
KeyPrint: "Print",
KeyExecute: "Execute",
KeySnapshot: "Snapshot",
KeyInsert: "Insert",
KeyDelete: "Delete",
KeyHelp: "Help",
Key0: "0",
Key1: "1",
Key2: "2",
Key3: "3",
Key4: "4",
Key5: "5",
Key6: "6",
Key7: "7",
Key8: "8",
Key9: "9",
KeyA: "A",
KeyB: "B",
KeyC: "C",
KeyD: "D",
KeyE: "E",
KeyF: "F",
KeyG: "G",
KeyH: "H",
KeyI: "I",
KeyJ: "J",
KeyK: "K",
KeyL: "L",
KeyM: "M",
KeyN: "N",
KeyO: "O",
KeyP: "P",
KeyQ: "Q",
KeyR: "R",
KeyS: "S",
KeyT: "T",
KeyU: "U",
KeyV: "V",
KeyW: "W",
KeyX: "X",
KeyY: "Y",
KeyZ: "Z",
KeyLWIN: "LWIN",
KeyRWIN: "RWIN",
KeyApps: "Apps",
KeySleep: "Sleep",
KeyNumpad0: "Numpad0",
KeyNumpad1: "Numpad1",
KeyNumpad2: "Numpad2",
KeyNumpad3: "Numpad3",
KeyNumpad4: "Numpad4",
KeyNumpad5: "Numpad5",
KeyNumpad6: "Numpad6",
KeyNumpad7: "Numpad7",
KeyNumpad8: "Numpad8",
KeyNumpad9: "Numpad9",
KeyMultiply: "Multiply",
KeyAdd: "Add",
KeySeparator: "Separator",
KeySubtract: "Subtract",
KeyDecimal: "Decimal",
KeyDivide: "Divide",
KeyF1: "F1",
KeyF2: "F2",
KeyF3: "F3",
KeyF4: "F4",
KeyF5: "F5",
KeyF6: "F6",
KeyF7: "F7",
KeyF8: "F8",
KeyF9: "F9",
KeyF10: "F10",
KeyF11: "F11",
KeyF12: "F12",
KeyF13: "F13",
KeyF14: "F14",
KeyF15: "F15",
KeyF16: "F16",
KeyF17: "F17",
KeyF18: "F18",
KeyF19: "F19",
KeyF20: "F20",
KeyF21: "F21",
KeyF22: "F22",
KeyF23: "F23",
KeyF24: "F24",
KeyNumlock: "Numlock",
KeyScroll: "Scroll",
KeyLShift: "LShift",
KeyRShift: "RShift",
KeyLControl: "LControl",
KeyRControl: "RControl",
KeyLMenu: "LMenu",
KeyRMenu: "RMenu",
KeyBrowserBack: "BrowserBack",
KeyBrowserForward: "BrowserForward",
KeyBrowserRefresh: "BrowserRefresh",
KeyBrowserStop: "BrowserStop",
KeyBrowserSearch: "BrowserSearch",
KeyBrowserFavorites: "BrowserFavorites",
KeyBrowserHome: "BrowserHome",
KeyVolumeMute: "VolumeMute",
KeyVolumeDown: "VolumeDown",
KeyVolumeUp: "VolumeUp",
KeyMediaNextTrack: "MediaNextTrack",
KeyMediaPrevTrack: "MediaPrevTrack",
KeyMediaStop: "MediaStop",
KeyMediaPlayPause: "MediaPlayPause",
KeyLaunchMail: "LaunchMail",
KeyLaunchMediaSelect: "LaunchMediaSelect",
KeyLaunchApp1: "LaunchApp1",
KeyLaunchApp2: "LaunchApp2",
KeyOEM1: "OEM1",
KeyOEMPlus: "OEMPlus",
KeyOEMComma: "OEMComma",
KeyOEMMinus: "OEMMinus",
KeyOEMPeriod: "OEMPeriod",
KeyOEM2: "OEM2",
KeyOEM3: "OEM3",
KeyOEM4: "OEM4",
KeyOEM5: "OEM5",
KeyOEM6: "OEM6",
KeyOEM7: "OEM7",
KeyOEM8: "OEM8",
KeyOEM102: "OEM102",
KeyProcessKey: "ProcessKey",
KeyPacket: "Packet",
KeyAttn: "Attn",
KeyCRSel: "CRSel",
KeyEXSel: "EXSel",
KeyErEOF: "ErEOF",
KeyPlay: "Play",
KeyZoom: "Zoom",
KeyNoName: "NoName",
KeyPA1: "PA1",
KeyOEMClear: "OEMClear",
}
type Modifiers byte
func (m Modifiers) String() string {
return modifiers2string[m]
}
var modifiers2string = map[Modifiers]string{
ModShift: "Shift",
ModControl: "Ctrl",
ModControl | ModShift: "Ctrl+Shift",
ModAlt: "Alt",
ModAlt | ModShift: "Alt+Shift",
ModAlt | ModControl | ModShift: "Alt+Ctrl+Shift",
}
const (
ModShift Modifiers = 1 << iota
ModControl
ModAlt
)
func ModifiersDown() Modifiers {
var m Modifiers
if ShiftDown() {
m |= ModShift
}
if ControlDown() {
m |= ModControl
}
if AltDown() {
m |= ModAlt
}
return m
}
type Shortcut struct {
Modifiers Modifiers
Key Key
}
func (s Shortcut) String() string {
m := s.Modifiers.String()
if m == "" {
return s.Key.String()
}
b := new(bytes.Buffer)
b.WriteString(m)
b.WriteRune('+')
b.WriteString(s.Key.String())
return b.String()
}
func GetKeyState(nVirtKey int32) int16 {
ret, _, _ := procGetKeyState.Call(
uintptr(nVirtKey),
)
return int16(ret)
}
func AltDown() bool {
return GetKeyState(int32(KeyAlt))>>15 != 0
}
func ControlDown() bool {
return GetKeyState(int32(KeyControl))>>15 != 0
}
func ShiftDown() bool {
return GetKeyState(int32(KeyShift))>>15 != 0
}
var ModifierMap = map[keys.Modifier]Modifiers{
keys.ShiftKey: ModShift,
keys.ControlKey: ModControl,
keys.OptionOrAltKey: ModAlt,
keys.CmdOrCtrlKey: ModControl,
}
var NoShortcut = Shortcut{}
func AcceleratorToShortcut(accelerator *keys.Accelerator) Shortcut {
if accelerator == nil {
return NoShortcut
}
inKey := strings.ToUpper(accelerator.Key)
key, exists := KeyMap[inKey]
if !exists {
return NoShortcut
}
var modifiers Modifiers
if _, exists := shiftMap[inKey]; exists {
modifiers = ModShift
}
for _, mod := range accelerator.Modifiers {
modifiers |= ModifierMap[mod]
}
return Shortcut{
Modifiers: modifiers,
Key: key,
}
}
var shiftMap = map[string]struct{}{
"~": {},
")": {},
"!": {},
"@": {},
"#": {},
"$": {},
"%": {},
"^": {},
"&": {},
"*": {},
"(": {},
"_": {},
"PLUS": {},
"<": {},
">": {},
"?": {},
":": {},
`"`: {},
"{": {},
"}": {},
"|": {},
}
var KeyMap = map[string]Key{
"0": Key0,
"1": Key1,
"2": Key2,
"3": Key3,
"4": Key4,
"5": Key5,
"6": Key6,
"7": Key7,
"8": Key8,
"9": Key9,
"A": KeyA,
"B": KeyB,
"C": KeyC,
"D": KeyD,
"E": KeyE,
"F": KeyF,
"G": KeyG,
"H": KeyH,
"I": KeyI,
"J": KeyJ,
"K": KeyK,
"L": KeyL,
"M": KeyM,
"N": KeyN,
"O": KeyO,
"P": KeyP,
"Q": KeyQ,
"R": KeyR,
"S": KeyS,
"T": KeyT,
"U": KeyU,
"V": KeyV,
"W": KeyW,
"X": KeyX,
"Y": KeyY,
"Z": KeyZ,
"F1": KeyF1,
"F2": KeyF2,
"F3": KeyF3,
"F4": KeyF4,
"F5": KeyF5,
"F6": KeyF6,
"F7": KeyF7,
"F8": KeyF8,
"F9": KeyF9,
"F10": KeyF10,
"F11": KeyF11,
"F12": KeyF12,
"F13": KeyF13,
"F14": KeyF14,
"F15": KeyF15,
"F16": KeyF16,
"F17": KeyF17,
"F18": KeyF18,
"F19": KeyF19,
"F20": KeyF20,
"F21": KeyF21,
"F22": KeyF22,
"F23": KeyF23,
"F24": KeyF24,
"`": KeyOEM3,
",": KeyOEMComma,
".": KeyOEMPeriod,
"/": KeyOEM2,
";": KeyOEM1,
"'": KeyOEM7,
"[": KeyOEM4,
"]": KeyOEM6,
`\`: KeyOEM5,
"~": KeyOEM3,
")": Key0,
"!": Key1,
"@": Key2,
"#": Key3,
"$": Key4,
"%": Key5,
"^": Key6,
"&": Key7,
"*": Key8,
"(": Key9,
"_": KeyOEMMinus,
"PLUS": KeyOEMPlus,
"<": KeyOEMComma,
">": KeyOEMPeriod,
"?": KeyOEM2,
":": KeyOEM1,
`"`: KeyOEM7,
"{": KeyOEM4,
"}": KeyOEM6,
"|": KeyOEM5,
"SPACE": KeySpace,
"TAB": KeyTab,
"CAPSLOCK": KeyCapital,
"NUMLOCK": KeyNumlock,
"SCROLLLOCK": KeyScroll,
"BACKSPACE": KeyBack,
"DELETE": KeyDelete,
"INSERT": KeyInsert,
"RETURN": KeyReturn,
"ENTER": KeyReturn,
"UP": KeyUp,
"DOWN": KeyDown,
"LEFT": KeyLeft,
"RIGHT": KeyRight,
"HOME": KeyHome,
"END": KeyEnd,
"PAGEUP": KeyPrior,
"PAGEDOWN": KeyNext,
"ESCAPE": KeyEscape,
"ESC": KeyEscape,
"VOLUMEUP": KeyVolumeUp,
"VOLUMEDOWN": KeyVolumeDown,
"VOLUMEMUTE": KeyVolumeMute,
"MEDIANEXTTRACK": KeyMediaNextTrack,
"MEDIAPREVIOUSTRACK": KeyMediaPrevTrack,
"MEDIASTOP": KeyMediaStop,
"MEDIAPLAYPAUSE": KeyMediaPlayPause,
"PRINTSCREEN": KeyPrint,
"NUM0": KeyNumpad0,
"NUM1": KeyNumpad1,
"NUM2": KeyNumpad2,
"NUM3": KeyNumpad3,
"NUM4": KeyNumpad4,
"NUM5": KeyNumpad5,
"NUM6": KeyNumpad6,
"NUM7": KeyNumpad7,
"NUM8": KeyNumpad8,
"NUM9": KeyNumpad9,
"nummult": KeyMultiply,
"numadd": KeyAdd,
"numsub": KeySubtract,
"numdec": KeyDecimal,
"numdiv": KeyDivide,
}
type Accelerator struct {
Virtual byte
Key uint16
Cmd uint16
}
func CreateAcceleratorTable(acc []Accelerator) uintptr {
if len(acc) == 0 {
return 0
}
ret, _, _ := procCreateAcceleratorTable.Call(
uintptr(unsafe.Pointer(&acc[0])),
uintptr(len(acc)),
)
return ret
}
func TranslateAccelerator(hwnd HWND, hAccTable uintptr, lpMsg *MSG) bool {
ret, _, _ := procTranslateAccelerator.Call(
uintptr(hwnd),
hAccTable,
uintptr(unsafe.Pointer(lpMsg)),
)
return ret != 0
}

View file

@ -0,0 +1,80 @@
package win32
type Menu HMENU
type PopupMenu Menu
func CreatePopupMenu() PopupMenu {
ret, _, _ := procCreatePopupMenu.Call(0, 0, 0, 0)
return PopupMenu(ret)
}
func (m Menu) Destroy() bool {
ret, _, _ := procDestroyMenu.Call(uintptr(m))
return ret != 0
}
func (p PopupMenu) Destroy() bool {
return Menu(p).Destroy()
}
func (p PopupMenu) Track(flags uint, x, y int, wnd HWND) bool {
ret, _, _ := procTrackPopupMenu.Call(
uintptr(p),
uintptr(flags),
uintptr(x),
uintptr(y),
0,
uintptr(wnd),
0,
)
return ret != 0
}
func (p PopupMenu) Append(flags uintptr, id uintptr, text string) bool {
return Menu(p).Append(flags, id, text)
}
func (m Menu) Append(flags uintptr, id uintptr, text string) bool {
ret, _, _ := procAppendMenuW.Call(
uintptr(m),
flags,
id,
MustStringToUTF16uintptr(text),
)
return ret != 0
}
func (p PopupMenu) Check(id uintptr, checked bool) bool {
return Menu(p).Check(id, checked)
}
func (m Menu) Check(id uintptr, check bool) bool {
var checkState uint = MF_UNCHECKED
if check {
checkState = MF_CHECKED
}
return CheckMenuItem(HMENU(m), id, checkState) != 0
}
func (m Menu) CheckRadio(startID int, endID int, selectedID int) bool {
ret, _, _ := procCheckMenuRadioItem.Call(
uintptr(m),
uintptr(startID),
uintptr(endID),
uintptr(selectedID),
MF_BYCOMMAND)
return ret != 0
}
func CheckMenuItem(menu HMENU, id uintptr, flags uint) uint {
ret, _, _ := procCheckMenuItem.Call(
uintptr(menu),
id,
uintptr(flags),
)
return uint(ret)
}
func (p PopupMenu) CheckRadio(startID, endID, selectedID int) bool {
return Menu(p).CheckRadio(startID, endID, selectedID)
}

View file

@ -0,0 +1,54 @@
package win32
type NOTIFYICONDATA struct {
CbSize uint32
HWnd HWND
UID uint32
UFlags uint32
UCallbackMessage uint32
HIcon HICON
SzTip [128]uint16
DwState uint32
DwStateMask uint32
SzInfo [256]uint16
UVersion uint32
SzInfoTitle [64]uint16
DwInfoFlags uint32
GuidItem GUID
HBalloonIcon HICON
}
type GUID struct {
Data1 uint32
Data2 uint16
Data3 uint16
Data4 [8]byte
}
type WNDCLASSEX struct {
CbSize uint32
Style uint32
LpfnWndProc uintptr
CbClsExtra int32
CbWndExtra int32
HInstance HINSTANCE
HIcon HICON
HCursor HCURSOR
HbrBackground HBRUSH
LpszMenuName *uint16
LpszClassName *uint16
HIconSm HICON
}
type MSG struct {
HWnd HWND
Message uint32
WParam uintptr
LParam uintptr
Time uint32
Pt POINT
}
type POINT struct {
X, Y int32
}

View file

@ -0,0 +1,189 @@
package win32
import (
"golang.org/x/sys/windows/registry"
"unsafe"
)
type DWMWINDOWATTRIBUTE int32
const DwmwaUseImmersiveDarkModeBefore20h1 DWMWINDOWATTRIBUTE = 19
const DwmwaUseImmersiveDarkMode DWMWINDOWATTRIBUTE = 20
const DwmwaBorderColor DWMWINDOWATTRIBUTE = 34
const DwmwaCaptionColor DWMWINDOWATTRIBUTE = 35
const DwmwaTextColor DWMWINDOWATTRIBUTE = 36
const DwmwaSystemBackdropType DWMWINDOWATTRIBUTE = 38
const SPI_GETHIGHCONTRAST = 0x0042
const HCF_HIGHCONTRASTON = 0x00000001
const WCA_ACCENT_POLICY WINDOWCOMPOSITIONATTRIB = 19
type ACCENT_STATE DWORD
const (
ACCENT_DISABLED ACCENT_STATE = 0
ACCENT_ENABLE_GRADIENT ACCENT_STATE = 1
ACCENT_ENABLE_TRANSPARENTGRADIENT ACCENT_STATE = 2
ACCENT_ENABLE_BLURBEHIND ACCENT_STATE = 3
ACCENT_ENABLE_ACRYLICBLURBEHIND ACCENT_STATE = 4 // RS4 1803
ACCENT_ENABLE_HOSTBACKDROP ACCENT_STATE = 5 // RS5 1809
ACCENT_INVALID_STATE ACCENT_STATE = 6
)
type ACCENT_POLICY struct {
AccentState ACCENT_STATE
AccentFlags DWORD
GradientColor DWORD
AnimationId DWORD
}
type WINDOWCOMPOSITIONATTRIBDATA struct {
Attrib WINDOWCOMPOSITIONATTRIB
PvData unsafe.Pointer
CbData uintptr
}
type WINDOWCOMPOSITIONATTRIB DWORD
// BackdropType defines the type of translucency we wish to use
type BackdropType int32
const (
BackdropTypeAuto BackdropType = 0
BackdropTypeNone BackdropType = 1
BackdropTypeMica BackdropType = 2
BackdropTypeAcrylic BackdropType = 3
BackdropTypeTabbed BackdropType = 4
)
func dwmSetWindowAttribute(hwnd HWND, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute unsafe.Pointer, cbAttribute uintptr) {
ret, _, err := procDwmSetWindowAttribute.Call(
uintptr(hwnd),
uintptr(dwAttribute),
uintptr(pvAttribute),
cbAttribute)
if ret != 0 {
_ = err
// println(err.Error())
}
}
func SupportsThemes() bool {
// We can't support Windows versions before 17763
return IsWindowsVersionAtLeast(10, 0, 17763)
}
func SupportsCustomThemes() bool {
return IsWindowsVersionAtLeast(10, 0, 17763)
}
func SupportsBackdropTypes() bool {
return IsWindowsVersionAtLeast(10, 0, 22621)
}
func SupportsImmersiveDarkMode() bool {
return IsWindowsVersionAtLeast(10, 0, 18985)
}
func SetTheme(hwnd HWND, useDarkMode bool) {
if SupportsThemes() {
attr := DwmwaUseImmersiveDarkModeBefore20h1
if SupportsImmersiveDarkMode() {
attr = DwmwaUseImmersiveDarkMode
}
var winDark int32
if useDarkMode {
winDark = 1
}
dwmSetWindowAttribute(hwnd, attr, unsafe.Pointer(&winDark), unsafe.Sizeof(winDark))
}
}
func EnableBlurBehind(hwnd HWND) {
var accent = ACCENT_POLICY{
AccentState: ACCENT_ENABLE_ACRYLICBLURBEHIND,
AccentFlags: 0x2,
}
var data WINDOWCOMPOSITIONATTRIBDATA
data.Attrib = WCA_ACCENT_POLICY
data.PvData = unsafe.Pointer(&accent)
data.CbData = unsafe.Sizeof(accent)
SetWindowCompositionAttribute(hwnd, &data)
}
func SetWindowCompositionAttribute(hwnd HWND, data *WINDOWCOMPOSITIONATTRIBDATA) bool {
if procSetWindowCompositionAttribute != nil {
ret, _, _ := procSetWindowCompositionAttribute.Call(
uintptr(hwnd),
uintptr(unsafe.Pointer(data)),
)
return ret != 0
}
return false
}
func EnableTranslucency(hwnd HWND, backdrop BackdropType) {
if SupportsBackdropTypes() {
dwmSetWindowAttribute(hwnd, DwmwaSystemBackdropType, unsafe.Pointer(&backdrop), unsafe.Sizeof(backdrop))
} else {
println("Warning: Translucency type unavailable on Windows < 22621")
}
}
func SetTitleBarColour(hwnd HWND, titleBarColour int32) {
dwmSetWindowAttribute(hwnd, DwmwaCaptionColor, unsafe.Pointer(&titleBarColour), unsafe.Sizeof(titleBarColour))
}
func SetTitleTextColour(hwnd HWND, titleTextColour int32) {
dwmSetWindowAttribute(hwnd, DwmwaTextColor, unsafe.Pointer(&titleTextColour), unsafe.Sizeof(titleTextColour))
}
func SetBorderColour(hwnd HWND, titleBorderColour int32) {
dwmSetWindowAttribute(hwnd, DwmwaBorderColor, unsafe.Pointer(&titleBorderColour), unsafe.Sizeof(titleBorderColour))
}
func SetWindowTheme(hwnd HWND, appName string, subIdList string) uintptr {
var subID uintptr
if subIdList != "" {
subID = MustStringToUTF16uintptr(subIdList)
}
ret, _, _ := procSetWindowTheme.Call(
uintptr(hwnd),
MustStringToUTF16uintptr(appName),
subID,
)
return ret
}
func IsCurrentlyDarkMode() bool {
key, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE)
if err != nil {
return false
}
defer key.Close()
AppsUseLightTheme, _, err := key.GetIntegerValue("AppsUseLightTheme")
if err != nil {
return false
}
return AppsUseLightTheme == 0
}
type highContrast struct {
CbSize uint32
DwFlags uint32
LpszDefaultScheme *int16
}
func IsCurrentlyHighContrastMode() bool {
var result highContrast
result.CbSize = uint32(unsafe.Sizeof(result))
res, _, err := procSystemParametersInfo.Call(SPI_GETHIGHCONTRAST, uintptr(result.CbSize), uintptr(unsafe.Pointer(&result)), 0)
if res == 0 {
_ = err
return false
}
r := result.DwFlags&HCF_HIGHCONTRASTON == HCF_HIGHCONTRASTON
return r
}

View file

@ -0,0 +1,137 @@
package win32
import (
"fmt"
"github.com/samber/lo"
"golang.org/x/sys/windows"
"syscall"
"unsafe"
)
func LoadIconWithResourceID(instance HINSTANCE, res uintptr) HICON {
ret, _, _ := procLoadIcon.Call(
uintptr(instance),
res)
return HICON(ret)
}
func LoadCursorWithResourceID(instance HINSTANCE, res uintptr) HCURSOR {
ret, _, _ := procLoadCursor.Call(
uintptr(instance),
res)
return HCURSOR(ret)
}
func RegisterClassEx(wndClassEx *WNDCLASSEX) ATOM {
ret, _, _ := procRegisterClassEx.Call(uintptr(unsafe.Pointer(wndClassEx)))
return ATOM(ret)
}
func RegisterClass(className string, wndproc uintptr, instance HINSTANCE) error {
classNamePtr, err := syscall.UTF16PtrFromString(className)
if err != nil {
return err
}
icon := LoadIconWithResourceID(instance, IDI_APPLICATION)
var wc WNDCLASSEX
wc.CbSize = uint32(unsafe.Sizeof(wc))
wc.Style = CS_HREDRAW | CS_VREDRAW
wc.LpfnWndProc = wndproc
wc.HInstance = instance
wc.HbrBackground = COLOR_WINDOW + 1
wc.HIcon = icon
wc.HCursor = LoadCursorWithResourceID(0, IDC_ARROW)
wc.LpszClassName = classNamePtr
wc.LpszMenuName = nil
wc.HIconSm = icon
if ret := RegisterClassEx(&wc); ret == 0 {
return syscall.GetLastError()
}
return nil
}
func CreateWindow(className string, instance HINSTANCE, parent HWND, exStyle, style uint) HWND {
classNamePtr := lo.Must(syscall.UTF16PtrFromString(className))
result := CreateWindowEx(
exStyle,
classNamePtr,
nil,
style,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
parent,
0,
instance,
nil)
if result == 0 {
errStr := fmt.Sprintf("Error occurred in CreateWindow(%s, %v, %d, %d)", className, parent, exStyle, style)
panic(errStr)
}
return result
}
func CreateWindowEx(exStyle uint, className, windowName *uint16,
style uint, x, y, width, height int, parent HWND, menu HMENU,
instance HINSTANCE, param unsafe.Pointer) HWND {
ret, _, _ := procCreateWindowEx.Call(
uintptr(exStyle),
uintptr(unsafe.Pointer(className)),
uintptr(unsafe.Pointer(windowName)),
uintptr(style),
uintptr(x),
uintptr(y),
uintptr(width),
uintptr(height),
uintptr(parent),
uintptr(menu),
uintptr(instance),
uintptr(param))
return HWND(ret)
}
func MustStringToUTF16Ptr(input string) *uint16 {
ret, err := syscall.UTF16PtrFromString(input)
if err != nil {
panic(err)
}
return ret
}
func MustStringToUTF16uintptr(input string) uintptr {
ret, err := syscall.UTF16PtrFromString(input)
if err != nil {
panic(err)
}
return uintptr(unsafe.Pointer(ret))
}
func MustUTF16FromString(input string) []uint16 {
ret, err := syscall.UTF16FromString(input)
if err != nil {
panic(err)
}
return ret
}
func UTF16PtrToString(input uintptr) string {
return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(input)))
}
func SetForegroundWindow(wnd HWND) bool {
ret, _, _ := procSetForegroundWindow.Call(
uintptr(wnd),
)
return ret != 0
}

View file

@ -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
}

View file

@ -0,0 +1,9 @@
package application
type EventType int
const (
StartUp EventType = iota
ShutDown
DomReady
)

View file

@ -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)
}
}

3
v2/pkg/events/events.go Normal file
View file

@ -0,0 +1,3 @@
package events
const ThemeChanged = ":wails:themechanged"

View file

@ -44,16 +44,23 @@ func (m *Menu) AddRadio(label string, checked bool, accelerator *keys.Accelerato
}
// AddSeparator adds a separator to the menu
func (m *Menu) AddSeparator() {
func (m *Menu) AddSeparator() *MenuItem {
item := Separator()
m.Append(item)
return item
}
func (m *Menu) AddSubmenu(label string) *Menu {
func (m *Menu) AddSubmenu(label string) *MenuItem {
submenu := NewMenu()
item := SubMenu(label, submenu)
m.Append(item)
return submenu
return item
}
func (m *Menu) InsertSubmenu(label string, submenu *Menu) *MenuItem {
item := SubMenu(label, submenu)
m.Append(item)
return item
}
func (m *Menu) Prepend(item *MenuItem) {

View file

@ -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{

View file

@ -1,20 +1,95 @@
package menu
import (
"context"
"log"
goruntime "runtime"
"github.com/wailsapp/wails/v2/pkg/events"
)
type TrayMenuAdd interface {
TrayMenuAdd(menu *TrayMenu) TrayMenuImpl
}
type TrayMenuImpl interface {
SetLabel(string)
SetImage(*TrayImage)
SetMenu(*Menu)
}
type EventsImpl interface {
On(eventName string, callback func(...interface{}))
}
type ImagePosition int
const (
ImageLeading ImagePosition = 0
ImageOnly ImagePosition = 1
ImageLeft ImagePosition = 2
ImageRight ImagePosition = 3
ImageBelow ImagePosition = 4
ImageAbove ImagePosition = 5
ImageOverlaps ImagePosition = 6
NoImage ImagePosition = 7
ImageTrailing ImagePosition = 8
)
type TraySizing int
const (
Variable TraySizing = 0
Square TraySizing = 1
)
type TrayImage struct {
// Bitmaps hold images for different scaling factors
// First = 1x, Second = 2x, etc
Bitmaps [][]byte
BitmapsDark [][]byte
IsTemplate bool
Position ImagePosition
}
func (t *TrayImage) getBestBitmap(scale int, isDarkMode bool) []byte {
bitmapsToCheck := t.Bitmaps
if isDarkMode {
bitmapsToCheck = t.BitmapsDark
}
if scale < 1 || scale >= len(bitmapsToCheck) {
return nil
}
for i := scale; i > 0; i-- {
if bitmapsToCheck[i] != nil {
return bitmapsToCheck[i]
}
}
return nil
}
// GetBestBitmap will attempt to return the best bitmap for the theme
// If dark theme is used and no dark theme bitmap exists, then it will
// revert to light theme bitmaps
func (t *TrayImage) GetBestBitmap(scale int, isDarkMode bool) []byte {
var result []byte
if isDarkMode {
result = t.getBestBitmap(scale, true)
if result != nil {
return result
}
}
return t.getBestBitmap(scale, false)
}
// TrayMenu are the options
type TrayMenu struct {
ctx context.Context
// Label is the text we wish to display in the tray
Label string
// Image is the name of the tray icon we wish to display.
// These are read up during build from <projectdir>/trayicons and
// the filenames are used as IDs, minus the extension
// EG: <projectdir>/trayicons/main.png can be referenced here with "main"
// If the image is not a filename, it will be treated as base64 image data
Image string
// MacTemplateImage indicates that on a Mac, this image is a template image
MacTemplateImage bool
Image *TrayImage
// Text Colour
RGBA string
@ -27,7 +102,7 @@ type TrayMenu struct {
Tooltip string
// Callback function when menu clicked
//Click Callback `json:"-"`
Click Callback
// Disabled makes the item unselectable
Disabled bool
@ -40,4 +115,67 @@ type TrayMenu struct {
// OnClose is called when the Menu is closed
OnClose func()
/* Mac Options */
Sizing TraySizing
// This is the reference to the OS specific implementation
impl TrayMenuImpl
// Theme change callback
themeChangeCallback func(data ...interface{})
}
func NewTrayMenu() *TrayMenu {
return &TrayMenu{}
}
func (t *TrayMenu) Show(ctx context.Context) {
if ctx == nil {
log.Fatal("TrayMenu.Show() called before Run()")
}
t.ctx = ctx
result := ctx.Value("frontend")
if result == nil {
pc, _, _, _ := goruntime.Caller(1)
funcName := goruntime.FuncForPC(pc).Name()
log.Fatalf("invalid context at '%s'", funcName)
}
t.impl = result.(TrayMenuAdd).TrayMenuAdd(t)
if t.themeChangeCallback == nil {
t.themeChangeCallback = func(data ...interface{}) {
println("Update button image")
if t.Image != nil {
// Update the image
t.SetImage(t.Image)
}
}
result := ctx.Value("events")
if result != nil {
result.(EventsImpl).On(events.ThemeChanged, t.themeChangeCallback)
}
}
}
func (t *TrayMenu) SetLabel(label string) {
t.Label = label
if t.impl != nil {
t.impl.SetLabel(label)
}
}
func (t *TrayMenu) SetImage(image *TrayImage) {
t.Image = image
if t.impl != nil {
t.impl.SetImage(image)
}
}
func (t *TrayMenu) SetMenu(menu *Menu) {
t.Menu = menu
if t.impl != nil {
t.impl.SetMenu(menu)
}
}

View file

@ -1,12 +1,12 @@
package mac
//type ActivationPolicy int
//
//const (
// NSApplicationActivationPolicyRegular ActivationPolicy = 0
// NSApplicationActivationPolicyAccessory ActivationPolicy = 1
// NSApplicationActivationPolicyProhibited ActivationPolicy = 2
//)
type ActivationPolicy int
const (
NSApplicationActivationPolicyRegular ActivationPolicy = 0
NSApplicationActivationPolicyAccessory ActivationPolicy = 1
NSApplicationActivationPolicyProhibited ActivationPolicy = 2
)
type AboutInfo struct {
Title string
@ -20,7 +20,7 @@ type Options struct {
Appearance AppearanceType
WebviewIsTransparent bool
WindowIsTranslucent bool
//ActivationPolicy ActivationPolicy
About *AboutInfo
About *AboutInfo
ActivationPolicy ActivationPolicy
//URLHandlers map[string]func(string)
}

View file

@ -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
}

View file

@ -80,17 +80,28 @@ type MenuItem struct {
}
```
| Field | Type | Notes |
| ----------- | ---------------------------------- | ------------------------------------------------------------- |
| Label | string | The menu text |
| Accelerator | [\*keys.Accelerator](#accelerator) | Key binding for this menu item |
| Type | [Type](#type) | Type of MenuItem |
| Disabled | bool | Disables the menu item |
| Hidden | bool | Hides this menu item |
| Checked | bool | Adds check to item (Checkbox & Radio types) |
| SubMenu | [\*Menu](#menu) | Sets the submenu |
| Click | [Callback](#callback) | Callback function when menu clicked |
| Role | string | Defines a [role](#role) for this menu item. Mac only for now. |
| Field | Type | Notes |
| ----------- | ---------------------------------- |------------------------------------------------------------------------------------------|
| Label | string | The menu text |
| Accelerator | [\*keys.Accelerator](#accelerator) | Key binding for this menu item |
| Type | [Type](#type) | Type of MenuItem |
| Disabled | bool | Disables the menu item |
| Hidden | bool | Hides this menu item |
| Checked | bool | Adds check to item (Checkbox & Radio types). Updated automatically when item is clicked. |
| SubMenu | [\*Menu](#menu) | Sets the submenu |
| Click | [Callback](#callback) | Callback function when menu clicked |
| Role | string | Defines a [role](#role) for this menu item. Mac only for now. |
### Keeping menu items in sync
A menuitem can be reused in multiple menus. This is useful for keeping menu items in sync. For example, if you have a
"Quit" menu item, you can add it to both the Application menu and a system tray menu. When the user clicks the menu item,
the callback will be called only once.
For checkbox and radio menu items, the state of the menu item will be kept in sync. For example, if you have a "Dark Mode"
menu item in the Application menu and a system tray menu, when the user clicks the menu item in the Application menu, the
state of the menu item in the system tray menu will also be updated. For radio menu items that are used in multiple places,
clicking one will uncheck all others in all groups that the menu item is used in.
### Accelerator
@ -104,7 +115,7 @@ Example:
myShortcut := keys.CmdOrCtrl("o")
```
Keys are any single character on a keyboard with the exception of `+`, which is defined as `plus`.
Keys are any single character on a keyboard except for `+`, which is defined as `plus`.
Some keys cannot be represented as characters so there are a set of named characters that may be used:
| | | | |
@ -219,7 +230,7 @@ type CallbackData struct {
```
The function is given a `CallbackData` struct which indicates which menu item triggered the callback. This is useful when
using radio groups that may share a callback.
using radio groups that may share a callback. If the menuitem is a checkbox, the `Checked` property will be set to the new value of the checkbox.
### Role

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB