Repository: beeware/toga

Issue:
[cocoa] Keyboard shortcuts: copy, paste, etc
## Expected Behavior
One would typically expect from an app on macOS that common keyboard shortcuts such as CMD+C, CMD+V, etc "just work". However, they don't on widgets such as `TextInput`.

## Current Behavior
Keyboard events handled by a widget itself do work (e.g., arrow keys in a `TreeView`, backspace in a `TextInput`). However, globally defined shortcuts such as copy and paste do not work.

## Possible fixes
1. This is typically handled in a NSApplication by adding the respective menu bar items with selectors (e.g., `SEL('copy:')` for CMD+C). This is what users on macOS expect, but it has two disadvantages: 
    - It requires always having those menu entries. But some use cases work better without a menu bar, such as a system tray app which occasionally shows a window.
    - When we add those items to the menu by default, they will be always enabled, unless the menu has set `autoenablesItems = True`.  In the latter case, they will be automatically enabled/disabled when the action becomes available, e.g., 'Minimise' (CMD+M) will only be enabled when the window supports being minimised. However, setting `autoenablesItems = True` means that all user defined items will *always* be enabled (in the current app implementation).

2. One could intercepting the key events in the NSApplication delegate. This has the disadvantage that the default menu bar items will not be visible unless explicitly added by the user. However, the commands will always work, regardless of the chosen menu bar configuration.


Discussion:
I agreed that cut/copy/paste/paste-special should be "available by default" commands in the same way "About", "Preferences", "Quit" and "Visit homepage" are on "normal" apps; and that these commands should be hooked into text widgets (and anywhere else that makes sense). 

I also agree these options *shouldn't* exist on system tray apps. Supporting system tray apps is a long standing feature request (#97). They have completely different UI/UX requirements to a "standard" app - and I'd argue that the way we should accomodate this is a different base class for systray apps. If you're building a systray app, it's not a toga.App with a couple of different flags passed to the constructor - it's a toga.TrayApp (or something similar). We've already got a DocumentApp specifically for this sort of purpose - recognizing that an app that is focused on handling documents is different to an app that provides a control interface.

There may even be a need for several different base classes, differentiating between a system tray app that is little more than a menu (e.g., the battery status), and an app that provides a custom window (e.g., Dropbox). This would also allow for the existence of the copy/paste *commands*, without necessarily adding the command to the *menu*.
Yes, I think having a separate base class for system tray apps makes a lot of sense. I have already outlined a rough API and Cocoa implementation for a system tray (icon + menu) but one issue I stumbled upon is the current API for menus and menu items. This also ties in to supporting Copy / Paste commands:

Currently, there is `toga.Command` which is used both for menubar items and toolbar buttons. But the use cases are sufficiently different that the may deserve separate API's:

* It is common to associate keyboard commands with menu(bar) items but not with toolbar buttons.
* Menu bar items are often enabled depending on the context (e.g., undo command, select all), toolbar buttons are almost always enabled.
* An API for menu items can be shared between context menus, system tray menus and menu bars. This generalisation does not hold for Commands.

On the Implementation layer, `toga.Command` currently does not have a native layer in Cocoa. Instead, the native instances are created and retained by the App during initialisation. This makes it difficult to implement enabling / disabling individual menu bar items while keeping other items in the same menu 'automatic'.
@freakboy3742, I have created a sample implementation for catching keyboard commands [here](https://github.com/SamSchott/toga/tree/macos-keyboard-cmds).

For now, this just subclasses NSApplication and does not add any menubar items. This is the much easier fix since it does not require changing the code to auto enable menubar items. And it will be useful for a future system tray application.

If you are happy with this as a temporary solution, I can create a PR. Otherwise, keyboard shortcuts will need to wait until I (or someone else) finds some more time...

@SamSchott It sounds like the purpose of Command isn't clear (and that's entirely my fault - we need a lot more documentation, and the existing code has some less-than-ideal implementation details).

The idea of Command is to wrap a "thing" that you can do in a GUI. That "thing" can surface in many ways in a GUI - as a menu item, as a toolbar item, or as some other action in a GUI (pressing a button in the normal layout of the app). 

The key abstraction is that command can be active or inactive - but the active status of the *Command* should drive the enabled/disabled status of the widgets that expose it. If the *command* is disabled, then any control activating that action should also be disabled. You shouldn't need to remember that you have to disable both the menu item *and* the toolbar item.

In the current implementation, I've assumed that all commands manifest as a menu item. The intention is to aid discoverability; menus showing that something *can* be done are a key way for end-users to discover the capabilities of an app. 

Looking at the Cocoa implementation of Command - there's no *default* native object; but it will accumulate native objects as the command is deployed (e.g., as a menu, as a toolbar etc). 

Within this framework, I'd argue that Cut, Copy and Paste are excellent examples of behaviors that *should* be wrapped as commands, and installed as part of the construction of a toga.App (I'll also note that the current backends all independently define the core application commands like About, Help and Quit; these *should* be defined at the interface level, since they're common to all apps - only the command grouping and shortcuts are platform specific). They'll acquire their app shortcuts by virtue of being installed as menu items in a standard app; once we start looking at system tray apps, we can investigate the best way to attach those keyboard bindings.

Does that make any sense?
That does clarify the concept of a Command. At the moment they can only be used in the menu bar and toolbar, right? Looking through the docs, I have found no way of attaching a Command to a regular button...

> In the current implementation, I've assumed that all commands manifest as a menu item.

You have also assumed, as far as I can see, that every menu item is a command and therefore omitted a separate API for menu items. This is valid in most cases, but there are some exceptions to this (submenus, 'widgets' such as the help search field on macOS, etc). But this is digressing form the original issue...

> Within this framework, I'd argue that Cut, Copy and Paste are excellent examples of behaviors that should be wrapped as commands, and installed as part of the construction of a toga.App

Agreed! 

> At the moment they can only be used in the menu bar and toolbar, right? Looking through the docs, I have found no way of attaching a Command to a regular button...

Correct. Chalk this up as one more thing on the "want to add" list :-)

More broadly, there's a design pattern that I've used with Tree and Table that I want to use more broadly - the concept of separating the "data" from the underlying widget.

In the simple case, you can give a List widget a list of tuples, and it will display them. However, in a more realistic example, the list will be backed by some actual data source, and if that data changes, any visualization of the data should also change. The natural way to store the underlying data might not be a Python list, either - it might be an API call or something similar. Toga handles this with the ListSource API - you can implement the data source however you want, as long as it adheres to the base API; and if the data in the source changes, the widget updates itself.

Although I've only implemented this for List, Tree and DetailedList, the same relationship exists on much simpler inputs, too. For example, a text input will usually display some simple name/value; if the underlying name/value changes, then the contents of the text input should change, too. Even a simple Label will sometimes be a reflection of some data state. And in the case of widgets that have "on activation" actions, the enabled/disabled status of the button can be dependent on whether the underlying action is actually possible at any given point in time. 

> > In the current implementation, I've assumed that all commands manifest as a menu item.
> 
> You have also assumed, as far as I can see, that every menu item is a command and therefore omitted a separate API for menu items. This is valid in most cases, but there are some exceptions to this (submenus, 'widgets' such as the help search field on macOS, etc). But this is digressing form the original issue...

This is technically true, but misses the significance of UI discoverability. My underlying argument is that anything that *can* be done should be visible in the menu.

It should be possible to support submenus by defining commands that have subcommands, or with a sub-grouping 

That said - I'm willing to entertain the idea that maybe we need to change the Command API to allow for explicit menu creation (and ordering/grouping). 

However, in any API change, there are 3 key features I want to preserve:

1. Avoiding the need for end-users to define menu items tied to key application capabilities.Every application needs a Quit, About, Cut, Copy, Paste, and Visit Website option; most will need a Preferences or Help item; and document-based apps will need Open/Save, etc. End users shouldn't need to define these commands - they should "just exist".

2. Avoiding the need for end-users to specifically *position* key application menu items. For example - where does the *Quit* menu item appear? On macOS, it's under the app menu; on Windows, it's at the bottom of the File menu. But *every* application needs a Quit item. The end user shouldn't need to write code defining where this item appears - it should always be in the platform-appropriate place.
 
3. It should be easy (and obvious) that defining a single command is possible, and that a command-based approach is preferable to defining a menu item, a toolbar item, and a button handler, and cross-coordinating between them all to ensure that enabled/disable state is accurately represented.

Both of these points are digressions, but hopefully they're digressions that illustrate the underlying idea I'm aiming at, and might provide some guidance about how we can add cut/copy/paste in a way that will fit the overall picture (and possibly even move other pieces of the bigger picture in the right direction).
Is there any update on this? My app needs copy paste to work, but I'm not sure how to use `SEL('copy:')`. I can see it is a `toga_cocoa` import, but if I use that, doesn't that break compatibility with windows and linux?
@whateverforever This is a volunteer-driven project; we can't really offer any estimates for when a any given bug will be fixed, or feature implemented, because it's entirely dependent on contributions provided by the community.

You're correct in your understanding about using `SEL('copy:')` though - if you import and use that call, the code will become macOS specific, and won't run on Windows and Linux. 
Well, but finally, please, where can I find information about how to implement Copy/Paste to `TextInput`? All I want just when user press Cmd-V in focused text field the clipboard text has been pasted to it. But I cannot find the description. Does it support 'on key press' event or something like this? I don't know where to read this. Please, can somebody help me and answer? Thanks in advance!

Resolving patch:
diff --git a/src/cocoa/toga_cocoa/app.py b/src/cocoa/toga_cocoa/app.py
--- a/src/cocoa/toga_cocoa/app.py
+++ b/src/cocoa/toga_cocoa/app.py
@@ -132,6 +132,8 @@ def create(self):
         formal_name = self.interface.formal_name
 
         self.interface.commands.add(
+
+            # ---- App menu -----------------------------------
             toga.Command(
                 lambda _: self.interface.about(),
                 'About ' + formal_name,
@@ -176,6 +178,71 @@ def create(self):
                 section=sys.maxsize
             ),
 
+            # ---- Edit menu ----------------------------------
+            toga.Command(
+                NativeHandler(SEL('undo:')),
+                'Undo',
+                shortcut=toga.Key.MOD_1 + 'z',
+                group=toga.Group.EDIT,
+                order=10,
+            ),
+            toga.Command(
+                NativeHandler(SEL('redo:')),
+                'Redo',
+                shortcut=toga.Key.SHIFT + toga.Key.MOD_1 + 'z',
+                group=toga.Group.EDIT,
+                order=20,
+            ),
+
+            toga.Command(
+                NativeHandler(SEL('cut:')),
+                'Cut',
+                shortcut=toga.Key.MOD_1 + 'x',
+                group=toga.Group.EDIT,
+                section=10,
+                order=10,
+            ),
+            toga.Command(
+                NativeHandler(SEL('copy:')),
+                'Copy',
+                shortcut=toga.Key.MOD_1 + 'c',
+                group=toga.Group.EDIT,
+                section=10,
+                order=20,
+            ),
+            toga.Command(
+                NativeHandler(SEL('paste:')),
+                'Paste',
+                shortcut=toga.Key.MOD_1 + 'v',
+                group=toga.Group.EDIT,
+                section=10,
+                order=30,
+            ),
+            toga.Command(
+                NativeHandler(SEL('pasteAsPlainText:')),
+                'Paste and Match Style',
+                shortcut=toga.Key.MOD_2 + toga.Key.SHIFT + toga.Key.MOD_1 + 'v',
+                group=toga.Group.EDIT,
+                section=10,
+                order=40,
+            ),
+            toga.Command(
+                NativeHandler(SEL('delete:')),
+                'Delete',
+                group=toga.Group.EDIT,
+                section=10,
+                order=50,
+            ),
+            toga.Command(
+                NativeHandler(SEL('selectAll:')),
+                'Select All',
+                shortcut=toga.Key.MOD_1 + 'a',
+                group=toga.Group.EDIT,
+                section=10,
+                order=60,
+            ),
+
+            # ---- Help menu ----------------------------------
             toga.Command(
                 lambda _: self.interface.visit_homepage(),
                 'Visit homepage',


Test patch: