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

Rework app handling, and add status icons #2238

Closed
wants to merge 12 commits into from

Conversation

freakboy3742
Copy link
Member

@freakboy3742 freakboy3742 commented Nov 20, 2023

  • Adds a SimpleApp base class. This is an MainWindow-based app that has the bare minimum of decoration. For platforms that put a menu in the main window, no menu is created. For platforms that have a global menubar, the menu only contains the bare minimum of required items.

  • Adds the ability for an app to declare status items (i.e., system tray/notification icons). A command Group can be declared as a status item group. The Status item group is a normal menu, but it is not added to the main menu; it's added to a status icon. An app can declare multiple status items, resulting in multiple status icons. The first status item group (by group sorting order) gets default app control items (about, settings, exit) automatically added to it.

  • Adds a WindowlessApp base class. This is an app that doesn't have any window by default. There isn't much use for this by itself, except when used in combination with status icons - a "status tray only" app is a WindowlessApp that puts all its commands in a status item Group.

Fixes:


This isn't ready to be merged yet - I've pushed it (a) so that nobody duplicates any effort, and (b) to get feedback on the general design. There are 2 new sample apps - SimpleApp and StatusIconApp - that demonstrate the new features.

Some other details on the current state of the PR:

  • As noted in the docs, status icons on GTK is... a mess. GNOME doesn't believe in status icons, and the documentation around this part of the ecosystem is so bad I haven't been able to work out how to make them visible even using the shell plugin described. FWIW, the required system package on Ubuntu is gir1.2-ayatanaappindicator3-0.1.
  • Even if you use a Linux desktop that does support status icons (I've done all my testing using Cinnamon, the Linux Mint default desktop), status icons are a bit of a mess. When an app exits (or crashes), it doesn't deregister any status icons... and I can't even find an API to do deregistration. I can't find any way to remove a status icon other than logging out. You can hide a status icon... but for some reason, the call to hide doesn't work when the app is exiting (or, at least, I can't work out how to make it work).
  • There's no implementation of SimpleApp on iOS or Android so far. I imagine the implementation will be "remove the app titlebar", which will likely impact on the availability of navigation views.
  • I don't think there's a meaningful implementation of statusIcons on iOS; but since commands aren't implemented on iOS anyway, that's a moot point at the moment.
  • I'm not convinced there's a meaningful implementation of StatusIcons on Android either. One of the earlier discussions mentions shortcuts ; but I'm not sure that's the right conceptual match. I'll defer to anyone with more platform experience.
  • There's no testbed testing of StatusIcons at present. That's the biggest TODO.
  • However, testbed testing of SimpleApp and WindowlessApp is going to be difficult, for the same reason as DocumentApp - you need a new app. I'm not sure how we can tackle this.

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

@HalfWhitt
Copy link
Contributor

HalfWhitt commented Nov 20, 2023

Wouldn't the analogous thing to Windows system tray icons on macOS be the status menus accessible on the right of the menu bar?

image

There are built-in ones like Bluetooth and WiFi, apps can have their own too. Adobe Creative Cloud, Karabiner Elements, and Lunar, for instance, are accessible from their icon in the menu bar.

They're apparently capable of bringing seemingly arbitrarily complex UI, as evidenced by Lunar:

Screenshot 2023-11-19 at 9 44 20 PM

But they're also certainly capable of presenting straightforward menus, like Karabiner:

image

Clicking on the Creative Cloud icon even opens an entire actual window.

I'm not actually sure what Apple's HIG recommendations for them are...

Edit: I think Lunar is probably just sneakily opening a heavily modified window / pane, especially since the icon doesn't remain "clicked" like for Karibiner.

@freakboy3742
Copy link
Member Author

Wouldn't the analogous thing to Windows system tray icons on macOS be the status menus accessible on the right of the menu bar?

"Icon in the status bar" is how the UI manifests on macOS.

  • macOS: Screenshot 2023-11-20 at 11 08 15 am
  • GTK: Screenshot 2023-11-20 at 11 06 27 am
  • Windows: Screenshot 2023-11-20 at 11 07 13 am

The only question is what (if anything) pops up when the icon is clicked:

  1. A menu (what this implementation does)
  2. An arbitrary GUI, inside a "pull down" menu-like outline (i.e., the Lunar-style GUI you've described)
  3. A window (i.e., the Creative Cloud-style option)

I imagine we could add support for (3) by allowing commands to be status items as well. I'm not sure that's worth the effort, though - I'm not sure what a status icon that can open a window does that a normal macOS app icon can't achieve.

I'm not sure how we'd expose an API for an arbitrary GUI in a popover. There isn't an obvious mapping to Group/Command that I can think of. the only idea I've got is an object that can be added as a "command" that can contain a "content" widget... but that feels messy, as it would only really be usable in a StatusItem context.

I'm not actually sure what Apple's HIG recommendations for them are...

They explicitly encourage using menus :-)

While I can definitely see the use case for having a complex GUI, I'm also (personally) willing to put that into the combination "future enhancements" bucket. You can do a lot with just a menu, and having the feature available at all would be an improvement. My only hesitation would be if the design we have now prevents future alternatives.

@HalfWhitt
Copy link
Contributor

...You know what, maybe I should get some sleep. I totally read iOS in your post as macOS, which is why I was confused!

@freakboy3742
Copy link
Member Author

Ping @samschott for your thoughts, since I know you've done some work in this area with Maestral.

@samschott
Copy link
Member

It's finally arriving in toga! I'll have a look this weekend :)

@freakboy3742
Copy link
Member Author

Filling in some discussion I've had in-person with @mhsmith: There may not be a need for separate SimpleApp, WindowlessApp and DocumentApp base classes.

There are 2 key features competing here:

  1. Being able to create a main window that doesn't have main menu.
  2. Being able to create an app that doesn't have a main window at all.

Thinking about this, the core of the issue may be that we have 2 subtly incompatible uses of Main Window. There's the MainWindow class, which is the physical manifestation of a window that acts as the main window; and there's the window that is assigned to the app as the app.main_window.

One option would be to modify MainWindow slightly so that it doesn't have any specific controls related to closing the app.

A MainWindow then becomes "an window with a menu bar"; a simple "Window" doesn't (or, at least, it doesn't at present - see #2210 for a related topic). Your app can has as many MainWindows as it wants - which is effectively what DocumentApp does - there's a "main" window for each document. In the context of macOS, this doesn't really change anything, because menus aren't bound to windows. On iOS and Android, it controls whether the titlebar is added (and also controls whether a toolbar is available. On that basis, it might be worth migrating toolbar functionality to be specific to MainWindow, leaving Window as a simple container.

What then matters for app lifecycle is which window is assigned as app.main_window. When that window is closed (regardless of whether it's a MainWindow or a Window), the app exits. If you never assign a main_window to the app... then the app doesn't care about windows. You don't even need to define one if you don't want to.

All the handling in DocumentApp for defining document types and command line handling can then be moved into the base app as well, allowing any app to respond to document types, respond to argv, and so on. The open/save commands become default commands that only get installed if your app defines document types (or some similar interface).

This also fits in with the use of default app control commands on Status item groups. If you assign a main window, the app control commands go on the main window. If you don't, they get assigned to the first status group. If you don't define a status group, you get an error.

In this context, there might be some benefit to renaming MainWindow (maybe AppWindow?) or renaming app.main_window to something else so that there's a clear conceptual distinction between app.main_window and MainWindow, and makes it clear (or at least clearer) that you can use a non-MainWindow as your app's main_window. ... but I'm not sure if the name churn is worth it, especially in the absence of a good alternate name.

Some notable downsides to this approach:

  1. We lose a safety catch that is currently in place. App currently validates if the user has assigned a MainWindow, since without one, you don't have a way to stop the app. However, you can CTRL-C, so maybe this protection isn't that helpful after all.

  2. There are some subtle changes with how apps are registered. For example, in the "no windows" case on macOS, you probably want to suppress the App icon. This requires some differences in how the app is registered. One fix for this would be to allow an explicit "there is no main_window" marker - i.e., explicitly setting app.main_window = None would change the way the app is registered. Related to this, we probably also need to make main_window a "set only once" attribute.

  3. There's some subtle issues with user experience around DocumentApp. On macOS, if you open a document app without specifying a document, you get a file open dialog; if you close the last document, the dialog is displayed again. The app is persistent, and only exits if you select "Quit" from the app-level menu. This works great with these changes - but on GTK and Windows, it's not 100% clear how DocumentApp should manifest, or how the macOS behavior can be made consistent with GTK and Windows with a common user-space App definition. Anything that removes the DocumentApp base class removes our ability to differentiate implementations on a per-platform basis.

Maybe we can accomodate this with another main_window marker - setting app.main_window = Document (or something similar) alters the app startup behavior, nominating the default document type to use for any "default" empty document.

@freakboy3742 freakboy3742 changed the title Add SimpleApp, WindowlessApp, and status icons Rework app handling, and add status icons Nov 23, 2023
@proneon267
Copy link
Contributor

Hello
For implementation of status icon on mobile platforms like Android, I suggest the status icon to be an expandable notification, like so:
image

image

This would be much more analogous to the desktop behavior.

@freakboy3742
Copy link
Member Author

Hello For implementation of status icon on mobile platforms like Android, I suggest the status icon to be an expandable notification,
....
This would be much more analogous to the desktop behavior.

Really? I'm not an Android user, but a notification - which is transient, and will disappear - doesn't seem like it's a match for status icons, which are permanent features that run in the "background" of the runtime environment.

Can you provide an example of an Android application that uses notifications in this way?

@proneon267
Copy link
Contributor

proneon267 commented Nov 24, 2023

There can be permanent notifications, in which the app runs in the background. These notifications cannot be dismissed while the app is running in the background.
Here is an example of an app:
https://play.google.com/store/apps/details?id=com.delphicoder.flud

Screenshot_2023-11-24-07-34-54-33_294b319c5f3179fc719955d8be3f3bcb

@HalfWhitt
Copy link
Contributor

HalfWhitt commented Nov 24, 2023

It's true, music players in particular, on both Android and iOS, often have a permanent "notification" that appears in notifications and the lock screen, shows what's playing, and offers basic playback controls. I know both Pandora and Apple Music do this. I don't know what the official name or recommended behavior for this type of thing is.

I haven't seen one that expands to a dropdown menu, though.

@freakboy3742
Copy link
Member Author

It's true, music players in particular, on both Android and iOS, often have a permanent "notification" that appears in notifications and the lock screen, shows what's playing, and offers basic playback controls.

On iOS, that functionality is called a "Widget" - and it's definitely not a good match for what has been implemented in this PR. You wouldn't ever see a list of menu options on a Widget - they're designed to be primarily informative, contextual (e.g., the current weather, or the status of an in-progress delivery), with occasionally actions (such as play/pause) exposed.

That might be an argument using Widgets as the manifestation of "arbitrary GUI" status icons; but I'm not sure they're a good match for "menu style" status icons. My inclination is to ignore mobile platforms for now, at least until we've got a better idea for how "arbitrary GUI" status icons can be supported.

@proneon267
Copy link
Contributor

Example of a drop down notification:
Screenshot_2023-11-24-08-26-44-55_e4424258c8b8649f6e67d283a50a2cbc

My inclination is to ignore mobile platforms for now

👍

@samschott
Copy link
Member

samschott commented Nov 26, 2023

Separate app classes

I've had a read through your discussion notes and PR description and my initial thoughts are:

  1. I like having different app classes for App, WindowlessApp, and DocumentApp. Those are sufficiently distinct in client API (and possibly implementation layer) that different subclasses of some "BaseApp" make sense. One can also argue that the app type should not change dynamically, so having parameters modify the app type is not so useful.
  2. Whether a base class needs to be useful or even instantiable by itself is a different question. It could also be an abstract base class that requires at least some methods to still be implemented.
  3. I find WindowslessApp confusing. Many system tray apps do have windows, e.g., for settings, but no main window and now dock icon on macOS. If we want to support system tray apps (an app that provides its primary functionality in the absence of a window and uses the system tray as main UI entry point for the user), why not call it SystemTrayApp?

Windows

Regarding the discussion around windows: As far as the implementation of apps is concerned, the way we think about windows makes a difference. But I would still argue that one should consider the main window question independently and find the best model / API regardless of how you want to represent apps.

  1. If the difference between MainWindow and Window is only the menu bar, maybe it is not worth having different classes vs just having a Window with a menu_bar or has_menu_bar property. Unlike app types, the difference here is smaller.
  2. I agree that the naming of App.main_window is confusing when there is also a MainWindow. This confusion can be overcome by good documentation, or by my previous suggestion, and maybe by having both :)
  3. I've always found the safety check of requiring a main window always a bit superfluous and taking away options from the app developer. But maybe this was just a symptom of the lack of a WindowlessApp :) In any case, I always like the idea of a library or framework encouraging but not forcing best practices.

Edit: I do see the irony in my last point. If we don't require a main window, what distinguishes an App from a WindowlessApp? Maybe not that much after all.

section: int = 0,
order: int = 0,
status_item: bool = False,
Copy link
Member

Choose a reason for hiding this comment

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

IIUC, the group itself will represent the status item. How do you think about this API vs having a light-weight StatusItem class with icon and commands / command_group properties? IMO, this would be more explicit than having a group marked status_item = true, assigning this group to a command, and adding that command to the app to finally get a status icon.

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 consider this; the problem is that I couldn't find an abstraction around the existing usage of Commands that made sense to me.

The current API doesn't create "menu items" and "menus"; we create and register "Commands" (as blocks of functionality that are surfaced by the app somehow). The group structure then sorts those into menus. A toolbar then becomes an alternate way of accessing that command; a command added to a toolbar is automatically registered with the app (if it isn't already registered).

So - the entry point to commands is registering them with the app. You don't register groups explicitly - you need to have them for organization purposes, but you don't explicitly tell your app "these are my groups"; the app discovers them by iterating over commands.

If we introduce a StatusItem class, then the'd need to have an analog of toolbar registration; That's not big problem; but what is registered against this new collection? Either:

  1. StatusItem is a subclass of group. That essentially gives us what we have in this PR, except there's an extra subclass that adds nothing except a mechanism to differentiate it from a normal group... at which point, why do we need a separate registration process?
  2. StatusItem contains a group. This is a departure from the historical norm as the group becomes the point of registration, but you still need to register the commands, as there's no way to get from the group the commands - so you have to register all the commands, then there's a separate process to declare your status item and the group it contains. It also means you register commands with a toolbar, but you register a wrapper around a group with the status item... so we've lost API consistency for command-related tooling.

I came to the conclusion that (2) was a bunch of complexity and conceptual confusion for not a whole lot of gain.

That said, one advantage to (2) is that it opens an obvious door for a separate "view-based" status item base class, so popdown menus that have complex GUIs become a possibility. However, I'm not sure whether the complexity of the registration approach it requires is worth it; and I'm sure we can find some other way to accommodate them that doesn't require the conceptual gymnastics.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, I seem to recall a discussion that we had around the need of a separate MenuItem class some ago, for cases where Command does not fit well.

That being said, maybe Command is indeed the right abstraction for menu items in the status bar.

Another option might be to have a StatusItem class and register Commands with it instead of registering commands with the App and assigning them to a status item group. This would however mean a full separation between status item and app commands. According to the documentation, this already seems to be the case at least in effect, if not in code:

Status item groups are not added to the main menu; instead, an icon will be added to the system tray or notification bar, and the Commands associated with the group will be displayed in a menu when the status icon is clicked.

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 think I can see how that could work; and in many regards, the implementation wouldn't be that different from what is here now. There's a slightly more complex path to look up and create them menu representing the group, but that's resolvable.

However, there are some interesting edge cases in the mapping between status item and menu bar. If I have 2 commands in different groups, but both registered with the same StatusItem - does that produce 2 status items or 1? Are groups on the top-level status item menus ignored? Do they convert into sections in the status menu? A toolbar puts a separator between 2 menu items in different groups, so I guess there's precedent for interpreting groups as "sections"; but the difference with toolbar is that the underlying commands still have their group structure in the main menu. This wouldn't be true for Status Items.

self.on_change = orig_on_change
if self.on_change:
self.on_change()

Copy link
Member

Choose a reason for hiding this comment

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

Out of curiosity, what is the intended use-case or concern that this is addressing? Do we have something similar for other classes with on_change handlers?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's an optimisation for app startup. On Cocoa, the app instance is created and configured as part of the App constructor. The last thing the app constructor does is install an on_change handler on the app's command set, so on_change is only triggered when there is an actual change after initial setup.

However, on GTK, the app is created during the constructor, but isn't configured until the GTK startup signal is received. This means the on_change handler is installed before the app commands have been fully constructed, so you end up invoking create_menus multiple times as the user's startup menu is invoked.

An analogous problem exists on Winforms; the user's startup() method isn't invoked until the main loop is started.

Thinking about it with a clear head, I guess an alternate approach would be to introduce a "finalise app construction" method on the core app, so that anything that needs to happen once the user's configuration code has all executed, and split core's constructor into two, using the call to the factory that constructs the App impl as the split point. At present, the only thing that needs to be done "late" is installing the on_change handler, but I guess there could be others.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the explanation!

I think my preference would be to indeed keep this only as a public API if you think it is useful to toga users, for example when modifying commands after startup, and not because of implementation details for some backends.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's reasonable. I think there's definitely a use case for a public "suspend" API for events as an optimisation, but that needs a more general approach. Plus, reworking startup() so there's a "finalise" stage makes more architectural to me sense anyway.

@freakboy3742
Copy link
Member Author

Separate app classes

I've had a read through your discussion notes and PR description and my initial thoughts are:

  1. I like having different app classes for App, WindowlessApp, and DocumentApp. Those are sufficiently distinct in client API (and possibly implementation layer) that different subclasses of some "BaseApp" make sense. One can also argue that the app type should not change dynamically, so having parameters modify the app type is not so useful.

So - it turns out (see #2244) that there is no difference in implementation. The only differentiation is:

  1. Does closing the window close the app?
  2. Does the window have a menu bar that exposes app commands (if appropriate)
  3. Does the app show an app-level icon?

All of these concepts can be independently accommodated without having different App types (e.g., you can make the window not close the app, but still have a main menu).

It's also worth noting that none of the APIs we're wrapping have different App types. That's not a strong argument in itself, but it's an interesting data point.

  1. I find WindowslessApp confusing. Many system tray apps do have windows, e.g., for settings, but no main window and now dock icon on macOS. If we want to support system tray apps (an app that provides its primary functionality in the absence of a window and uses the system tray as main UI entry point for the user), why not call it SystemTrayApp?

A reasonable argument; however, it's a moot point if there aren't app subclasses :-)

Windows

Regarding the discussion around windows: As far as the implementation of apps is concerned, the way we think about windows makes a difference. But I would still argue that one should consider the main window question independently and find the best model / API regardless of how you want to represent apps.

  1. If the difference between MainWindow and Window is only the menu bar, maybe it is not worth having different classes vs just having a Window with a menu_bar or has_menu_bar property. Unlike app types, the difference here is smaller.

It's entirely likely that we will need to introduce a menu_bar attribute on Window (see #2210). The distinction I'd make is whether the window's menu includes the "app level" commands. On a Main Window, they are included; on a "normal" window, they're not.

  1. I've always found the safety check of requiring a main window always a bit superfluous and taking away options from the app developer. But maybe this was just a symptom of the lack of a WindowlessApp :) In any case, I always like the idea of a library or framework encouraging but not forcing best practices.

Edit: I do see the irony in my last point. If we don't require a main window, what distinguishes an App from a WindowlessApp? Maybe not that much after all.

This is effectively resolved in #2244 - you need to assign a value to main_window, but that value can be None.

@samschott
Copy link
Member

You are indeed convincing me that separate app classes are not needed after all :)

The distinction I'd make is whether the window's menu includes the "app level" commands. On a Main Window, they are included; on a "normal" window, they're not.

Yes, that does indeed make sense!

@freakboy3742
Copy link
Member Author

I'm going to close this PR; the status icon code is the only part of this that is likely to survive, and it will be dependent on #2244 (or it's derivatives) landing first.

@mhsmith mhsmith mentioned this pull request Aug 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Complete implementation and testing of DocumentApp Add a Toga.SimpleApp base class Systray support ?
4 participants