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

Menu improvements #423

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open

Menu improvements #423

wants to merge 18 commits into from

Conversation

simonhamp
Copy link
Member

@simonhamp simonhamp commented Nov 20, 2024

Heads up, this introduces some significant breaking changes!

Depends on NativePHP/electron#139

We've been discussing internally for a while how the Menu API sucks a little. There are a few quirks, it doesn't really expose the stuff you probably want, and it just feels awkward, it doesn't feel very "Laravel".

  1. You new() up a menu and then you have to register() it, which is what sets it as the application menu. It's not immediately obvious (imo) that you can omit register() and pass the resulting Menu to other things like Windows, the MenuBar and the Dock.

  2. Defining items that should be in a list and can be nested inside each other as part of fluent chain feels really strange. You have to 'break' the chain (conceptually) in places that make it feel awkward and inconsistent, and the code ends up looking jarring rather than familiar.

  3. The fluent chain is nice, but because we're immediately adding sparse instances to the chained Menu we lose access to the underlying MenuItem classes and so can't make deeper customisations easily.

  4. It's currently impossible to change the application Menu once it's been defined.

So this is my attempt to improve on all of these issues by:

  • Introducing a new Menu facade which wraps a new MenuBuilder class
    This menu builder let's you access the underlying MenuItem objects more easily and build your menus with a more expressive DX. More on this in a moment...
  • More MenuItem types that map to standard OS functions (called Roles)
  • Making Link menu items default to changing the currently-focused window's URL onclick without any extra wiring.
    If you want to open in the user's browser (the current default), you will be able to chain the new ->openInBrowser() method to Menu::link()
  • Making submenus (which are just Menu objects, which are a MenuItem too) definable in a more consistent, expressive way
  • Adding a new default() helper to create the default menu even more easily
  • Enhance event data coming back from menu item clicks, such as forwarding other data like combo keys
  • Allow adding of an ID to MenuItem instances - useful in handling generic MenuItemClicked events on the Laravel side
  • Allow the app menu to be completely recreated at any point in the app lifecycle 💪🏼

Here's what this looks like:

Create the application menu

Before

use Native\Laravel\Menu\Menu;

Menu::new()
    ->appMenu()
    ->editMenu()
    ->register();

After

use Native\Laravel\Facades\Menu;

Menu::create(
    Menu::app(),
    Menu::edit(),
);

Menu::create builds the menu and registers it all in one shot.

Create a menu for use elsewhere (e.g. as a context menu, Dock menu, or MenuBar menu)

Before

use Native\Laravel\Menu\Menu;

Menu::new()
    ->link('https://nativephp.com', 'Documentation');

After

use Native\Laravel\Facades\Menu;

Menu::make(
    Menu::link('https://nativephp.com', 'Documentation')
        ->openInBrowser(),
);

Define the default menu

Before

use Native\Laravel\Menu\Menu;

Menu::new()
    ->appMenu()
    ->editMenu()
    ->viewMenu()
    ->windowMenu()
    ->register();

After

use Native\Laravel\Facades\Menu;

Menu::default();

Menu::default() allows you to easily return your menu to the default that would be provided if you didn't register a menu at all.

Define a custom menu

Before

use Native\Laravel\Menu\Menu;

Menu::new()
    ->appMenu()
    ->submenu(
        'My Submenu',
        Menu::new()
            ->link('https://nativephp.com', 'Documentation')
    )
    ->register();

After

use Native\Laravel\Facades\Menu;

Menu::create(
    Menu::app(),
    Menu::make(
        Menu::link('https://nativephp.com')
            ->label('Documentation')
            ->openInBrowser(),
        // or: Menu::link('https://nativephp.com', 'Documentation'),
    )->label('My Submenu'),
);

Menu::make() returns a Native\Laravel\Menu\Menu instance.

Many of the other helpers return a MenuItem subclass, from which you can chain calls to their other methods.

This gives you far more control over your menus. Building your menus has never been more straightforward.

Define a blank menu

Before

use Native\Laravel\Menu\Menu;

Menu::new()
    ->register();

After

use Native\Laravel\Facades\Menu;

Menu::create();

Copy link
Contributor

@SRWieZ SRWieZ left a comment

Choose a reason for hiding this comment

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

It's quite a change in syntax, but I believe this PR improves it.

  • Some docblocks are incorrect or "incomplete"
  • I prefer using "hotkey" instead of "accelerator."
  • I got a bug with my custom event on a menu item throwing an exception because of a missing named argument on my constructor. I think we should either ask devs to extend MenuItemClicked with their event or better create a contract that they should implement with the following constructor: public function __construct(public array $item, public array $combo = [])

Overall it's great!

Also we should add a note in the documentation stating that we cannot change the name of the first menu in macOS. Those two examples does not work, always shows with config('app.name'):

Menu::create(Menu::edit())  
Menu::create(Menu::make()->label('My label'))  
Capture d’écran 2024-11-22 à 20 41 33

use Native\Laravel\Menu\Items\Separator;

/**
* @method static \Native\Laravel\Menu\Menu make(MenuItem ...$items)
Copy link
Contributor

Choose a reason for hiding this comment

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

This references the wrong MenuItem, it should reference Native\Laravel\Menu\Items\MenuItem

* @method static \Native\Laravel\Menu\Menu make(MenuItem ...$items)
* @method static Checkbox checkbox(string $label, bool $checked = false, ?string $hotkey = null)
* @method static Label label(string $label)
* @method static Link link(string $url, string $label = null, ?string $hotkey = null)
Copy link
Contributor

Choose a reason for hiding this comment

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

It's named $hotkey here and $accelerator in the class.
I prefer $hotkey

@@ -3,11 +3,15 @@
namespace Native\Laravel\Menu\Items;

use Native\Laravel\Contracts\MenuItem as MenuItemContract;
use Native\Laravel\Facades\Menu as MenuFacade;
use Native\Laravel\Menu\Menu;

abstract class MenuItem implements MenuItemContract
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we create an alias to accelerator() as hotkey() ?

@@ -12,7 +12,7 @@ class MenuItemClicked implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;

public function __construct(public array $item) {}
public function __construct(public array $item, public array $combo = []) {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add some PHP Doc here to represent:

array:2 [▼
  "item" => array:2 [▼
    "label" => "Settings"
    "checked" => false
  ]
  "combo" => array:5 [▼
    "shiftKey" => false
    "ctrlKey" => false
    "altKey" => false
    "metaKey" => false
    "triggeredByAccelerator" => false
  ]
]

@gwleuverink
Copy link
Contributor

gwleuverink commented Nov 22, 2024

Honestly this works great! I've tinkered with it a bit now.

One thing I might be missing. Context menu's were mentioned. Is it possible to trigger a menu in-place on a button click? This doesn't work right now and could be a cool addition.

I've got a alpine magic I use for this. I'm happy to extract that to the Native helper if you want.

Right now it looks like this:

<button
    type="button"
    x-on:click="
        $contextMenu([
            new MenuItem({
                label: 'Foo',
            }),
            new MenuItem({
                label: 'Bar',
            }),
        ])
    "
>

It does involve adding the MenuItem class to the window scope. But that can be namespaced under the Native helper if needed.

I haven't tested this, but it might be feasible to add this to the php api instead. The code is pretty slim.

@gwleuverink
Copy link
Contributor

gwleuverink commented Nov 22, 2024

Extracted to the Native helper I imagine something like this:

<button
    type="button"
    onclick="Native.contextMenu([
        new MenuItem({
            label: 'Foo',
        }),
        new MenuItem({
            label: 'Bar',
        }),
    ])"
>

Example of what that looks like:

image

What's cool about this is that you can invoke Livewire actions asynchronously from the MenuItem click callback:

new MenuItem({
    label: 'Restart run',
    click: async () => $wire.restartRun({{ $run->remote_id }}),
}),

@gwleuverink
Copy link
Contributor

gwleuverink commented Nov 22, 2024

It does involve adding the MenuItem class to the window scope. But that can be namespaced under the Native helper if needed.

Turns out using Menu.buildFromTemplate I can construct this menu from a array of plain objects. So no need to expose more than needed to the window

@simonhamp
Copy link
Member Author

@gwleuverink that context menu stuff looks super cool! Fancy wrapping whatever you've needed to modify there into a new PR?

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.

3 participants