Skip to content
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

Closed
wants to merge 60 commits into from

Conversation

leaanthony
Copy link
Member

@leaanthony leaanthony commented Oct 12, 2022

PR Goals

This PR has been opened to solicit feedback and discussion on system tray support.
The goals of this PR are:

  • Determine if feature set is complete enough for an initial release
  • Determine what likely gaps there are with the other platforms, e.g. Template icons for Mac, HiDPI alternative icons.
  • Get feedback on the experimental programmatic API and where it could be improved
  • Find bugs

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

leaanthony and others added 30 commits April 30, 2022 12:00
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
Refactor ShellNotifyIcon. Application shutdown once.
@leaanthony leaanthony requested review from stffabi and removed request for stffabi October 13, 2022 09:10
@leaanthony leaanthony added the Ready For Testing A fix is available and needs testing label Oct 13, 2022
Copy link

@teddywing teddywing left a 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:

  1. Open the menu by right-clicking the tray icon
  2. Left-click the tray icon
  3. 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, NSStatusItems 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.

v2/pkg/options/systemtray.go Outdated Show resolved Hide resolved
v2/internal/platform/systray/windows.go Outdated Show resolved Hide resolved
@teddywing
Copy link

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?

@leaanthony
Copy link
Member Author

leaanthony commented Oct 22, 2022

Thanks @teddywing for taking the time to test this out and provide feedback 👍

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.

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 asked about gaps with other platforms. In addition to the template icons you mentioned, NSStatusItems 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?

You should be able to call SetIcon() from a Goroutine with a timer (I haven't tried it but it totally should work).

@teddywing
Copy link

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?

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?

You should be able to call SetIcon() from a Goroutine with a timer (I haven't tried it but it totally should work).

Cool, that sounds good.

@leaanthony
Copy link
Member Author

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?

There are now 👍

@leaanthony
Copy link
Member Author

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.

Copy link
Collaborator

@stffabi stffabi left a 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")
Copy link
Collaborator

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?

Copy link
Member Author

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?

Copy link
Collaborator

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 {
Copy link
Collaborator

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are they 1:1?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"errors"
"fmt"
platformMenu "github.com/wailsapp/wails/v2/internal/platform/menu"
"github.com/wailsapp/wails/v2/internal/platform/win32"
Copy link
Collaborator

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 {
Copy link
Collaborator

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.

Copy link
Member Author

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?

Copy link
Collaborator

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
Copy link
Collaborator

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

Copy link
Member Author

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 {
Copy link
Collaborator

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 {
Copy link
Collaborator

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 {
Copy link
Collaborator

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?


import "unsafe"

func MakeIntResource(id uint16) *uint16 {
Copy link
Collaborator

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() {
Copy link
Collaborator

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.

@leaanthony
Copy link
Member Author

The winc/win32 thing I should have made clearer: everything in platform is the target from after we merge this PR. Eventually, as we migrate from frontend we move everything we need into platform/win32 and drop both windows/win32 and the winc directory. I'm looking at frontend/win32 exposing a slightly higher interface than just a 1:1 api onto win32 so I wanted to "port" that over as needed. I hate the duplication but in my head they are different things 😅

@stffabi
Copy link
Collaborator

stffabi commented Oct 26, 2022

The winc/win32 thing I should have made clearer: everything in platform is the target from after we merge this PR. Eventually, as we migrate from frontend we move everything we need into platform/win32 and drop both windows/win32 and the winc directory. I'm looking at frontend/win32 exposing a slightly higher interface than just a 1:1 api onto win32 so I wanted to "port" that over as needed. I hate the duplication but in my head they are different things 😅

Ah great, thanks for clarifying 🙏

@teddywing
Copy link

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?

There are now 👍

Nice, thanks!

@leaanthony
Copy link
Member Author

Closing in favour of #2181 - Thanks for the feedback 👍

@leaanthony leaanthony closed this Dec 5, 2022
@W1M0R W1M0R mentioned this pull request Sep 18, 2023
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Ready For Testing A fix is available and needs testing
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants