-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Support tray menus (Windows) #1959
Conversation
Support better tray menu API Move object creation to main thread
# Conflicts: # v2/internal/frontend/desktop/windows/win32/consts.go
# Conflicts: # .gitignore # v2/internal/frontend/desktop/windows/win32/consts.go # v2/internal/frontend/devserver/devserver.go # website/docs/introduction.mdx
the option in the docs is the wrong flag you have to add a d at the end
…ctor assethandler/assetserver signatures.
Refactor ShellNotifyIcon. Application shutdown once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks cool!
I tried out and read over the example code in v2/examples/systray/main.go
. The API looks straightforward.
There’s one UI behaviour that I was confused by. If I:
- Open the menu by right-clicking the tray icon
- Left-click the tray icon
- The menu disappears and the window opens
Should the window be opening? I would have thought that the menu should close, and the window would only open if the menu wasn’t already open.
You asked about gaps with other platforms. In addition to the template icons you mentioned, NSStatusItem
s have a behavior
property (https://developer.apple.com/documentation/appkit/nsstatusitembehavior?language=objc). It might be nice to add behaviours to provide additional interaction options for removing the NSStatusItem
.
One more idea: Is there any way to animate the tray icon in order to indicate, for instance, that the application is making a network request? |
Thanks @teddywing for taking the time to test this out and provide feedback 👍
I'm not sure we can be 💯 sure this is the desired behaviour for all system trays. We do have the left and right click callbacks. Do you think this would be sufficient for a developer to produce the outcome you suggested?
👍
You should be able to call |
Yes you’re right, it makes more sense for these behaviours to be defined by the app developer. Are there APIs to know when the menu is visible and to hide & show it?
Cool, that sounds good. |
There are now 👍 |
Considering it's been 2 weeks since this PR was opened, I propose we lock this API down for now and move on to Mac support. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks awesome to me, awesome stuff 🚀.
I think the API is fine as it is, so from my side we can lock that down.
Furthermore I added some comments regarding Windows message loop specific things. It seems like we are getting a bunch of duplicate code between winc and the new platform/win32 package. Maybe we should someday take the opportunity to clean that up, maybe the best time when we do multi-window support.
procGetWindowRect = moduser32.NewProc("GetWindowRect") | ||
procGetMonitorInfo = moduser32.NewProc("GetMonitorInfoW") | ||
procMonitorFromWindow = moduser32.NewProc("MonitorFromWindow") | ||
moduser32 = syscall.NewLazyDLL("user32.dll") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we start using windows.NewLazySystemDLL
in order the prevent preloading attacks as mentioned in the docs of syscall.NewLazyDll
?
IIRC there are a log of places with the old NewLazyDLL
, maybe we could do this in a separate PR and go through the complete code base?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, agree. I think refactors should be their own PRs 👍 Why not open a ticket for it and we can work on that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, will do that.
@@ -41,3 +51,217 @@ func IsWindowsVersionAtLeast(major, minor, buildNumber int) bool { | |||
windowsVersion.Minor >= minor && | |||
windowsVersion.Build >= buildNumber | |||
} | |||
|
|||
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373931.aspx | |||
type GUID struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use windows.GUID
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are they 1:1?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
v2/internal/platform/systray/menu.go
Outdated
"errors" | ||
"fmt" | ||
platformMenu "github.com/wailsapp/wails/v2/internal/platform/menu" | ||
"github.com/wailsapp/wails/v2/internal/platform/win32" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this break builds for non Windows targets currently, without any build constraint?
return result | ||
} | ||
|
||
func (p *Systray) Run() error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we really have several MessageLoops running in the same application? Windows and e.g. .NET almost always runs only one MessageLoop on the main thread. I thank that would make it a lot easier to reason about on which thread one can access GUIs and not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did ask a win32 guy about this and he said that a message loop per thread is fine. What would moving this to a main loop look like? What if this is a tray only application with no window?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is not a problem per se. I'm just used to having only one MessageLoop from my old .NET days 😄. A drawback would be to have several dedicated OS threads that are being run for every MessageLoop.
AFAIK there's no problem of running a message loop if there's no window, it should run without any window. Let's consider the following case, which I think would also be easier to reason about with just one message loop. One has an application with just a tray, for one click another window is created (in the multi-window case).
So the callback of the click is done in the dedicated systray os thread. If we now create a new window, we would need to span a go routine, create the window and start another message loop for that. Otherwise we would implicitly use the message loop of the systray.
Let's consider the same case with just one message loop on the main-thread which is dedicated to the UI. In that case the systray is run in that message loop, a click callback is also run in the main-thread, one can simply create a new window without needing to span a go routine. Show the window and the window is also run on the same main-thread, so do all the callbacks. That would probably also make it easier to interact with WebView2 if there are several windows, since all the COM initialisation would only have to be done once.
} | ||
|
||
func (p *Systray) Run() error { | ||
var msg win32.MSG |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might suffer here from the same problem as we had with our main message loop here: #1083
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oooph that is a monumental catch 👍
|
||
func (p *Systray) Run() error { | ||
var msg win32.MSG | ||
for { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we need to lock down the OS thread in here? There's no guarantee that if run in a Go routine, all calls will be executed on the same OS thread. And we might then pull messages from the wrong OS thread or miss some events if it's not run on the message loop that created the systray.
HBalloonIcon HICON | ||
} | ||
|
||
type GUID struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC this is a duplicate definition in another package, maybe also using windows.GUID
?
@@ -0,0 +1,54 @@ | |||
package win32 | |||
|
|||
type NOTIFYICONDATA struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC this is a duplicate definition with a struct in another package, should we unify this?
v2/internal/platform/win32/winres.go
Outdated
|
||
import "unsafe" | ||
|
||
func MakeIntResource(id uint16) *uint16 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using this one will make problems with the race detector as seen in: #1554
} | ||
} | ||
|
||
func (t *SystemTray) run() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we have to lock down the OS thread here? Since Windows will send the messages of the systray to the message loop of the OS thread that created the window. So we would have to make sure all of the creation of the SysTray will be done on the same OS thread as t.impl.Run() is run.
This is OS specific for windows, so we might need to put that into platform specific code behind a build tag.
The winc/win32 thing I should have made clearer: everything in |
Ah great, thanks for clarifying 🙏 |
Nice, thanks! |
Closing in favour of #2181 - Thanks for the feedback 👍 |
PR Goals
This PR has been opened to solicit feedback and discussion on system tray support.
The goals of this PR are:
What does this PR provide?
This PR provides the ability to add an icon to the Windows system tray area, with the option of adding a menu. The system tray has callbacks for both left and right buttons. By default, both these buttons will show the associated menu. Currently, this capability is provided by an experimental API that builds on what is currently available but provides a more hierarchical model for Wails applications.
An example application is provided in
v2/examples/systray
. It starts with the main window hidden. This can be made visible by left clicking the tray icon. Right clicking the tray icon shows the menu. The menu is a "kitchen sink" example of what is possible. There are plenty of comments in the example which detail more about what is possible with the system tray.It should be noted that there is other code in here related to Mac trays. This is a WIP and should not be considered as part of this "PR".
How can I provide feedback?
Please comment on this PR with any feedback. Thank you! 🙏
Related Links