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

Systray support ? #97

Closed
toyg opened this issue Jan 27, 2017 · 22 comments · Fixed by #2768
Closed

Systray support ? #97

toyg opened this issue Jan 27, 2017 · 22 comments · Fixed by #2768
Labels
enhancement New features, or improvements to existing features.

Comments

@toyg
Copy link

toyg commented Jan 27, 2017

Most desktop-managers have some sort of system tray where to keep icons for long-running programs and notifications. This doesn't seem to be supported in Toga at the moment.

@freakboy3742
Copy link
Member

You're correct that it isn't currently supported - we're still trying to get a basic spread of widgets working. But this is absolutely on the roadmap.

@ghost
Copy link

ghost commented Apr 25, 2017

Does this fit as a widget of some kind? Or do we need a different category for "support" features - tray icons, notifications, progress indicators (see https://msdn.microsoft.com/en-us/library/windows/desktop/dd378460(v=vs.85).aspx)

It's going to be a little bit of a pain to implement in Windows due to the changes from Win7 -> Win10.

@freakboy3742
Copy link
Member

I don't think this is a "widget" - it's more of a feature of the app itself. In the same way an app has a main_window, it may also have a system tray representation (and, in some cases, it may have a sys tray but not a main window, or vice versa)

For macOS and Linux, it makes sense that it's a "menu attached to an icon"; iOS and Android will be a menu as well, with different activation modes (possibly with a slide out tray on iOS, and a toolbar menu on Android). I'm guessing the same would be true of Win7; not sure what the interpretation on Win10 would be.

Any suggestions on API are welcome. If you want to take this on as a mini project, it would be a great addition.

@ghost
Copy link

ghost commented Apr 25, 2017

I'm not confident enough on the API design yet but I'll keep it in mind.

@gutierri
Copy link
Contributor

But can this be incorporated into the project?

I get confused at organizing this in the "core", systray on the smartphone would be status bar icons. Maybe it's more a matter of name and organizing in the project.

@ibigpapa
Copy link
Contributor

Looks like I created a duplicate of this issue
#481

I've lined out somewhat of a spec for this and how it might be implemented on Android/IOS as the system tray icon doesn't exactly fit. Since most of the time it's to know an application is running and provide quick access / info without having to have the main window open. So the Android and IOS might be a couple of items to accomplish the same behaviors.

@ibigpapa ibigpapa mentioned this issue May 15, 2018
7 tasks
@ibigpapa
Copy link
Contributor

Tray Icon Proposal

Tray Icons are usually used to show an application is running but not display a window. Sometimes the window is visible with the TrayIcon as well. The Icon also usually allows for a context menu to select some actions.

Proposed Properties

  • TrayIcon.Icon
    • Description: Sets the Icon
    • Type: src.core.toga.widgets.icon
    • Access: RW
  • TrayIcon.Title
    • Description: Sets the Title
    • Type: Str
    • Access: RW
  • TrayIcon.Tooltip
    • Description: Sets the Tooltip (some platforms may not support this)
    • Type: Str (or New Widget)
    • Access: RW
  • TrayIcon.Menu
    • Description: Adds a context menu widget
    • Type: ContextMenu (New Widget)
    • Access: RW
  • Review all implementations to ensure Proposed abstraction is generic enough.
  • Create a ContextMenu widget
  • Create Implementation in Windows forms
  • Create Implementation in MacOS
  • Create Implementation in IOS
  • Create Implementation in Android
  • Create Implementation in GTK

Resources

@freakboy3742 freakboy3742 added enhancement New features, or improvements to existing features. up-for-grabs labels Jun 4, 2020
@gaozhidf
Copy link

how about integrating with pystray - https://pystray.readthedocs.io/en/latest/usage.html

@freakboy3742
Copy link
Member

@gaozhidf That's a nice idea, but the problem is that their macOS support is provided with PyObjC, which overlaps responsibilities with the Rubicon ObjC library that Toga uses.

It may be worth examining their API design for ideas, though. Looking at their API, there's also some possible overlap with #907.

@mhsmith
Copy link
Member

mhsmith commented Nov 29, 2023

Design discussion: #2108

@freakboy3742
Copy link
Member

freakboy3742 commented Jul 28, 2024

I did an initial pass of adding status icons with #2238; this revealed the need for a bunch of other structural changes around app lifecycle. Those changes became #2244, which then landed as a series of separate PRs - most notably (for the purposes of this PR) #2651. So - we're now in a position to return to adding status icons.

The implementation in #2238 will need to be massively reworked to accomodate these changes, and revealed some gaps in the proposed API, but it did allow me to work out the internal details of how status icons are implemented on each (desktop) platform.

  • On macOS, a status icon is an object with a menu with an icon. Items (including separators and submenus) can be added to the menu as they would be to a normal app menu. Menus can also hold Views, potentially allowing for full widget layouts.
  • On Windows, a status icon is an icon in itself; what happens when you click on that icon is up to the app. Displaying a context menu is the obvious choice; but you could also use it to display an arbitrary window.
  • On GTK, the story is complicated. Philosophically, GNOME doesn't believe in status icons. Apparently they can be made visible with shell plugins, but the documentation around how to do this is so bad that I haven't been able to make them work. If you use a different GTK-based desktop (e.g. Cinnamon), they're still a bit of a mess, and they require a system package (gir1.2-ayatanaappindicator3-0.1). However, once you do this, the status icon is an object with a menu and an icon - although, frustratingly, it's different type of menu to the one used in menubar (Gtk.Menu rather than Gio.Menu), with almost the same API, but different class names (sigh). I can't seen an obvious way to be able to add complex widgets or attach arbitrary views to a popup... but I'll also admit I was getting more than a little frustrated at GTK by this point.

The question we're now faced is the design of the public API for creating status icons.


There are essentially 3 use cases for status icons that I can see:

  1. A bare "icon only" status item; this is essentially a button in the status bar.
  2. A menu-based status item; click on the item, and a menu is displayed.
  3. A container-based status item; click on the item, and a view of some kind (that is "attached" to the icon in some way) is displayed.

I propose that we introduce a base class to represent status icons, with subclasses for each of these use cases. We also add a status_icons container to app, allowing status icons to be registered, and then looked up by id, by index, or by iteration. Instances of the StatusIcon classes will be registered with the app, with each instance represents one icon.

Usage would look something like:

        status_1 = toga.MenuStatusIcon(icon="...")
        app.status_icons.add(
            status_1,
            toga.MenuStatusIcon(),  # fall back to the app icon as a default
            toga.StatusIcon(icon="...", on_press=do_stuff),
            toga.ContainerStatusIcon(content=toga.Box(children=[...], size=(200, 300))
        )

If you want an "icon only" status icon, you register a StatusIcon, and attach an "on_press" handler.

If you want a menu-based status icon, you register a MenuStatusIcon. This class is defined as a subclass of both StatusIcon and Group, so it can be used as the group for any commands (or subgroups) that we want to appear as menus. Commands are registered with the app (not the status icon), reflecting the fact that they're normal commands, just with a different root menu:

        cmd1 = toga.Command(
            do_stuff,
            text="Action 1",
            group=status_1,
        )

        sub_menu = toga.Group("Sub Menu", parent=status_1)
        cmd2 = toga.Command(
            self.do_stuff,
            text="Action 2",
            group=sub_menu,
        )
        app.commands.add(cmd1, cmd2)

Creating the status icon creates the icon representation; the menus are created (and modified) as part of the existing menu item handling. The only difference to the existing code is that we need to account for a new type of root menu (when the group is a MenuStatusIcon).

We might not implement ContainerStatusIcon in this initial pass (if only because the implementation on GTK is unclear); but on Windows and macOS, the implementation path is reasonably clear - we need a create container to hold a toga.Box in response to clicking the icon.

This structure provides a clear API point where we could add any status icon-specific APIs (like show/hide, or modifying the icon in response to status changes). Again, we might not implement any of these APIs in the first pass, but it's fairly clear where the functionality would live if we chose to add it.


There's one other major design decision - the handling of "default" menu items. In all the options described above, there's no guarantee that, as a user, you have registered an "Exit" menu item. This could lead to "unquittable apps". Unlike menus, there's no strong UX conventions around what happens with status-icon-only apps, so it's not immediately obvious to me what approach we should take.

The easy option is to nothing. If the user doesn't define EXIT, then their app is unquittable. Sucks to be them :-)

However, we could take a similar approach with apps, and guarantee registration of some commands - this would be EXIT at a minimum; you could probably make the argument for ABOUT and PREFERENCES as well (once we have default preferences handling). If we do this, I'd argue these commands are added to the end of the first status item, after a separator. It's not clear what we'd do if the only status item is "widget-based" or "icon only".

A variation of this would be to only install the defaults if the app main_window is BACKGROUND. There's nothing stopping an app having a "real" GUI and status icons; if you've got a main window, you've also got a way to quit the app, so there's no need to force an quit item onto a menu.

My preference here is the latter - to guarantee basic commands on the first status item if the main window is BACKGROUND. This still leaves the user with an unquittable app if they don't define at least one menu status icon, but that's better than nothing.


FWIW, I've got a prototype implementation of this API for macOS to prove out the edge cases. However, I'd be interested in other opinions (or alternative API suggestions).

@mhsmith
Copy link
Member

mhsmith commented Jul 28, 2024

Thanks, I'll look over this on Tuesday.

@mhsmith
Copy link
Member

mhsmith commented Aug 6, 2024

Overall this design looks good. Just a few comments:

On GTK, the story is complicated. Philosophically, GNOME doesn't believe in status icons.

So is the proposal that we simply wouldn't support them on GTK? Based on that page and the other pages it links to, it looks like they're hidden in the default configuration, so it wouldn't be worth the effort for us to support them.

We might not implement ContainerStatusIcon in this initial pass

I agree, let's wait and see if there's any demand for it.

MenuStatusIcon [...] is defined as a subclass of both StatusIcon and Group, so it can be used as the group for any commands (or subgroups) that we want to appear as menus. Commands are registered with the app (not the status icon), reflecting the fact that they're normal commands, just with a different root menu:

I understand that logic, but I think it's still easier for each icon to have its own CommandSet, as proposed for per-window commands in #2210:

  • The status icon's commands can't be displayed in the app menu bar, or vice versa, because many of them would be duplicated, e.g. there would be an Exit command in both the app menu and the status icon menu, each with a different group.
  • So there's no benefit in them sharing the same CommandSet, but there is a cost, because the code that handles the app menu would need to exclude commands from the status icons, and vice versa. This would complicate Toga's own code, and potentially app code as well. For example, it would give no simple way of clearing a status icon's menu, while if it had its own CommandSet, it would simply be icon.commands.clear().

If each status icon had its own CommandSet, then making it a subclass of Group would be redundant. Instead, we could require that all status icon commands have a top-level group of COMMANDS. Since this is already the default in the Command constructor, it wouldn't usually need to be specified. Sub-menus within that group would still be possible.

My preference here is the latter - to guarantee basic commands on the first status item if the main window is BACKGROUND.

I think keeping track of all these conditions is needless work both for us and for the app developer. What if we simply say that ALL MenuStatusIcons get the default menu items, regardless of whether they're the first icon or what the value of main_window is. If the app doesn't want them, they're simple to remove with icon.commands.clear(), as discussed above.

@freakboy3742
Copy link
Member

So is the proposal that we simply wouldn't support them on GTK? Based on that page and the other pages it links to, it looks like they're hidden in the default configuration, so it wouldn't be worth the effort for us to support them.

The problem is that GTK != GNOME. If you're a Mint user, for example, you can have status icons, because the Cinnamon desktop is GTK, but not GNOME.

FWIW, I've got a working implementation on GTK - the implementation has some interesting bugs (which are all GTK's problem, AFAICT), but it works. So, IMHO we might as well include an implementation - but it will need some hefty platform notes for GTK users.

MenuStatusIcon [...] is defined as a subclass of both StatusIcon and Group
...
If each status icon had its own CommandSet, then making it a subclass of Group would be redundant. Instead, we could require that all status icon commands have a top-level group of COMMANDS. Since this is already the default in the Command constructor, it wouldn't usually need to be specified. Sub-menus within that group would still be possible.

I'll have a play with this and see what falls out. I did do some experiments with having a single "status icons" commandset, but I don't think I tried a command set per status icon. I agree there's some nice symmetry with #2210; and being able to clear one icon's commands is also potentially useful. The complication will come around top-level group handling/validation, and adding commands to both the app and the status icon.

My preference here is the latter - to guarantee basic commands on the first status item if the main window is BACKGROUND.

I think keeping track of all these conditions is needless work both for us and for the app developer. What if we simply say that ALL MenuStatusIcons get the default menu items, regardless of whether they're the first icon or what the value of main_window is. If the app doesn't want them, they're simple to remove with icon.commands.clear(), as discussed above.

In this vein, I guess there's another option: make "include default menu commands" an argument on the MenuStatusIcon constructor. That way the commands are easy to get if you need them, and easy to opt out of if you don't. I guess the only question then would be whether the default value for that argument is On or Off. My argument would be to default to it being On, so that icons "do the right thing" by default, but if you have more than one (or have a status icon in an app that also has windows), you can easily opt out of those default menu items.

@mhsmith
Copy link
Member

mhsmith commented Aug 7, 2024

I agree including the standard commands by default is better, because this feature will most often be used in situations where there's one status icon and no visible windows.

I guess there's another option: make "include default menu commands" an argument on the MenuStatusIcon constructor

We don't have such an argument on the App class, and there's no need for one as long as the default commands can be removed with a simple call to clear, or trimmed down with del.

In fact, that's another argument for each status icon having its own CommandSet – it allows the standard commands to be accessed via their standard IDs, because they'd be unique within that CommandSet.

@freakboy3742
Copy link
Member

I guess there's another option: make "include default menu commands" an argument on the MenuStatusIcon constructor

We don't have such an argument on the App class, and there's no need for one as long as the default commands can be removed with a simple call to clear, or trimmed down with del.

In the case of App, I'd argue the use case for removing the standard commands is extremely niche, if it exists at all. Apps should have an exit command (et al), and the corresponding menu item(s). Not having the standard commands is very much the exception, not the rule.

If a status bar-only app manifests as 2 icons, one of them needs to have the standard commands, but the other one doesn't. And if a window-based app also has status icons, then it doesn't need any standard commands in the status menus. While this could be accommodated with "a clear the defaults before adding your own commands" pattern, it strikes me as such a predictable usage pattern that it makes sense to promote to a first-class API.

In fact, that's another argument for each status icon having its own CommandSet – it allows the standard commands to be accessed via their standard IDs, because they'd be unique within that CommandSet.

Agreed it's a good argument in favor of isolated CommandSets.

@mhsmith
Copy link
Member

mhsmith commented Aug 8, 2024

While this could be accommodated with "a clear the defaults before adding your own commands" pattern, it strikes me as such a predictable usage pattern that it makes sense to promote to a first-class API.

OK, fair enough.

@mhsmith
Copy link
Member

mhsmith commented Aug 15, 2024

We talked the other day about what the Group of a status icon command should be, and mentioned 3 options:

  • MenuStatusIcon inherits from Group
  • MenuStausIcon has a group property
  • Use the COMMANDS group

In all cases, the icon would require that every command in its CommandSet was directly or indirectly contained within the expected group.

I'm still leaning towards the COMMANDS group. It may seem arbitrary, but it has one big advantage: it's the default in the Command constructor, so for top-level menu items, you wouldn't need to specify a group at all. And for sub-menus, it would be equally simple as the other two options.

@freakboy3742 freakboy3742 mentioned this issue Aug 15, 2024
4 tasks
@freakboy3742
Copy link
Member

In the process of implementing #2768, I came up with a potential hybrid option - make MenuStatusIcon a group, but make it sort equivalent to COMMANDS. That way, you don't have to register a command as having a specific group (falling back to the COMMANDS default).

Then, when the menu is created, the top-level groups (ie., any group with parent=None) is ignored rather than being turned into an explicit menu item. That technically means any group could be used as the parent for submenu purposes - the only difference will be ordering (FILE will order before COMMANDS). This also allows you to register any standard command with the status icon, and it will be rendered in an order that is consistent with how they'd appear in main menus... just without the top level labels (so "File > Open" would appear as "Open" if it were added to a status icon).

The only downside I see is that technically, you could register a subgroup with a parent of status_icon_1, create a command in that subgroup, then add that command to status_icon_2.commands. Conceptually this doesn't make any sense, but in terms of "ignore all top level commands", it would be consistent.

We avoid that inconsistency if we don't make MenuStatusIcon a group, but at the cost of it not being entirely clear why Group.COMMANDS is a reasonable default parent for submenus. I guess we could possible work around this by aliasing Group.COMMANDS to a name that makes more sense in context... say, Group.STATUS_ICON?

FWIW, the difference between the hybrid I've implemented in #2768, and a COMMANDS/aliased COMMANDS approach is fairly minimal - a one word change in the example app, some minor simplification after removing Group as a base class of MenuStatusIcon, and optionally a new alias for COMMANDS.

@mhsmith
Copy link
Member

mhsmith commented Aug 15, 2024

Just now we agreed the following:

  • MenuStatusIcon would inherit from Group.
  • App.status_icons.commands would be a CommandSet shared between all status icons, which requires all of its commands to be contained within one of those groups.

Compared to each icon having its own CommandSet, this would lose the ability to clear a single icon's menu with a single method call, or to have multiple commands with the same ID in different icons, although there are workarounds for both of these.

It also loses some clarity in that a MenuStatusIcon's menu is no longer contained within the MenuStatusIcon object, but instead has to be managed elsewhere.

What I don't remember is, what do we gain in exchange for these losses? What does this design have over each icon having its own CommandSet, which requires all of its commands to be contained within COMMANDS?

It may not be immediately clear to the developer why it has to be COMMANDS, but the answer is just "it has to be something, and that was already the default in the Command constructor". As long as we state this rule in the documentation, and give a clear error message when it's broken, it shouldn't be any more confusing than either of the alternatives above.

@freakboy3742
Copy link
Member

What I don't remember is, what do we gain in exchange for these losses? What does this design have over each icon having its own CommandSet, which requires all of its commands to be contained within COMMANDS?

It may not be immediately clear to the developer why it has to be COMMANDS, but the answer is just "it has to be something, and that was already the default in the Command constructor". As long as we state this rule in the documentation, and give a clear error message when it's broken, it shouldn't be any more confusing than either of the alternatives above.

To my mind, there's several benefits:

Firstly, if each icon has its own commands, then we have to take either the "top level group is ignored" interpretation, or the "top level must be one specific group" interpretation, because every command must have a group.

It also means that when you're setting up a submenu of the status icon menu, the group you use must have a parent, so you can't avoid the "parent is COMMANDS" situation - except that it literally doesn't matter which parent you use, because the top level parent is ignored. This can be explained, but the fact that it needs to be explained at all is a mild red flag.

However, for me, the biggest argument is that it's more consistent with main menus. You don't "add a File menu" then "add commands to the file menu's command set" - you have a single collection of commands; those commands are grouped, and the top-level grouping determines what menus exist. To my mind, that's a really close analog with having a single commandset for all menu-based status icons, with the top level grouping determining which icons are in use.

The only special case that needs to be explained here is how top-level groups that aren't registered as status icons are handled - but that's a special case based on "a thing that doesn't exist", rather than "a thing that exists we're going to ignore", which is much less of a special case.

We also have to deal with non-menu status icons; but that doesn't really change the core mechanic - it's just an oddity that you have to deal with in iteration.

@mhsmith
Copy link
Member

mhsmith commented Aug 19, 2024

OK, that makes sense. Just a couple of questions:

with main menus [...] you have a single collection of commands; those commands are grouped, and the top-level grouping determines what menus exist. To my mind, that's a really close analog with having a single commandset for all menu-based status icons, with the top level grouping determining which icons are in use.

Do we really want a MenuStatusIcon to disappear when it doesn't have any commands? I think that would be confusing. Unlike a regular Group with no commands, it can still convey information by changing its icon.

The only special case that needs to be explained here is how top-level groups that aren't registered as status icons are handled

How would they be handled? Since this must be a programming error, throwing an exception seems reasonable.

I see you've already specified this in #2768, so I'll post there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New features, or improvements to existing features.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants