Add native macOS menus (fix #135)

* This implements the Cmd+H and Cmd+M keys too:
  https://community.aseprite.org/t/279
* Also Cmd+, has more priority on macOS than Cmd+K to open the
  preferences (so macOS menu shows Cmd+,)
This commit is contained in:
David Capello 2017-09-01 13:32:23 -03:00
parent 607c4d139e
commit 0154a73d36
24 changed files with 994 additions and 72 deletions

View File

@ -50,11 +50,11 @@
<key command="ReplaceColor" shortcut="Shift+R" />
<key command="HueSaturation" shortcut="Ctrl+U" mac="Cmd+U" />
<key command="ConvolutionMatrix" shortcut="F9" />
<key command="ColorCurve" shortcut="Ctrl+M" mac="Cmd+M" />
<key command="ColorCurve" shortcut="Ctrl+M" />
<key command="ColorCurve" shortcut="F10" />
<key command="PasteText" shortcut="T" />
<key command="Options" shortcut="Ctrl+K" mac="Cmd+K" />
<key command="Options" mac="Cmd+," />
<key command="Options" shortcut="Ctrl+K" mac="Cmd+K" />
<key command="KeyboardShortcuts" shortcut="Ctrl+Alt+Shift+K" mac="Cmd+Alt+Shift+K" />
<!-- Sprite -->
<key command="SpriteProperties" shortcut="Ctrl+P" mac="Cmd+P" />
@ -129,7 +129,7 @@
<key command="PaletteEditor">
<param name="popup" value="background" />
</key>
<key command="ShowExtras" shortcut="Ctrl+H" mac="Cmd+H" />
<key command="ShowExtras" shortcut="Ctrl+H" />
<!-- Tabs -->
<key command="GotoNextTab" shortcut="Ctrl+Tab" />
<key command="GotoPreviousTab" shortcut="Ctrl+Shift+Tab" />
@ -837,7 +837,7 @@
<separator />
<item command="Refresh" text="&amp;Refresh &amp;&amp; Reload Skin" />
</menu>
<menu text="&amp;Help">
<menu text="&amp;Help" id="help_menu">
<item command="OpenBrowser" text="Readme">
<param name="filename" value="README.md" />
</item>

View File

@ -10,7 +10,6 @@
#include "app/app_menus.h"
#include "base/string.h"
#include "app/app.h"
#include "app/commands/command.h"
#include "app/commands/commands.h"
@ -23,9 +22,13 @@
#include "app/ui/app_menuitem.h"
#include "app/ui/keyboard_shortcuts.h"
#include "app/ui/main_window.h"
#include "app/ui_context.h"
#include "app/util/filetoks.h"
#include "base/bind.h"
#include "base/fs.h"
#include "base/string.h"
#include "she/menus.h"
#include "she/system.h"
#include "ui/ui.h"
#include "tinyxml.h"
@ -42,6 +45,167 @@ static void destroy_instance(AppMenus* instance)
delete instance;
}
// TODO this should be on "she" library (or we should use
// she::Shortcut instead of ui::Accelerators)
static int from_scancode_to_unicode(KeyScancode scancode)
{
static int map[] = {
0, // kKeyNil
'a', // kKeyA
'b', // kKeyB
'c', // kKeyC
'd', // kKeyD
'e', // kKeyE
'f', // kKeyF
'g', // kKeyG
'h', // kKeyH
'i', // kKeyI
'j', // kKeyJ
'k', // kKeyK
'l', // kKeyL
'm', // kKeyM
'n', // kKeyN
'o', // kKeyO
'p', // kKeyP
'q', // kKeyQ
'r', // kKeyR
's', // kKeyS
't', // kKeyT
'u', // kKeyU
'v', // kKeyV
'w', // kKeyW
'x', // kKeyX
'y', // kKeyY
'z', // kKeyZ
'0', // kKey0
'1', // kKey1
'2', // kKey2
'3', // kKey3
'4', // kKey4
'5', // kKey5
'6', // kKey6
'7', // kKey7
'8', // kKey8
'9', // kKey9
0, // kKey0Pad
0, // kKey1Pad
0, // kKey2Pad
0, // kKey3Pad
0, // kKey4Pad
0, // kKey5Pad
0, // kKey6Pad
0, // kKey7Pad
0, // kKey8Pad
0, // kKey9Pad
0xF704, // kKeyF1 (NSF1FunctionKey)
0xF705, // kKeyF2
0xF706, // kKeyF3
0xF707, // kKeyF4
0xF708, // kKeyF5
0xF709, // kKeyF6
0xF70A, // kKeyF7
0xF70B, // kKeyF8
0xF70C, // kKeyF9
0xF70D, // kKeyF10
0xF70E, // kKeyF11
0xF70F, // kKeyF12
27, // kKeyEsc
'~', // kKeyTilde
'-', // kKeyMinus
'=', // kKeyEquals
8, // kKeyBackspace
9, // kKeyTab
'[', // kKeyOpenbrace
']', // kKeyClosebrace
'\r', // kKeyEnter // 10, kKeyEnter
':', // kKeyColon
'\'', // kKeyQuote
'\\', // kKeyBackslash
0, // kKeyBackslash2
',', // kKeyComma
'.', // kKeyStop
'/', // kKeySlash
' ', // kKeySpace
0xF727, // kKeyInsert (NSInsertFunctionKey)
0xF728, // kKeyDel (NSDeleteFunctionKey)
0xF729, // kKeyHome (NSHomeFunctionKey)
0xF72B, // kKeyEnd (NSEndFunctionKey)
0xF72C, // kKeyPageUp (NSPageUpFunctionKey)
0xF72D, // kKeyPageDown (NSPageDownFunctionKey)
0xF702, // kKeyLeft (NSLeftArrowFunctionKey)
0xF703, // kKeyRight (NSRightArrowFunctionKey)
0xF700, // kKeyUp (NSUpArrowFunctionKey)
0xF701, // kKeyDown (NSDownArrowFunctionKey)
'/', // kKeySlashPad
'*', // kKeyAsterisk
0, // kKeyMinusPad
0, // kKeyPlusPad
0, // kKeyDelPad
0, // kKeyEnterPad
0, // kKeyPrtscr
0, // kKeyPause
0, // kKeyAbntC1
0, // kKeyYen
0, // kKeyKana
0, // kKeyConvert
0, // kKeyNoconvert
0, // kKeyAt
0, // kKeyCircumflex
0, // kKeyColon2
0, // kKeyKanji
0, // kKeyEqualsPad
'`', // kKeyBackquote
0, // kKeySemicolon
0, // kKeyUnknown1
0, // kKeyUnknown2
0, // kKeyUnknown3
0, // kKeyUnknown4
0, // kKeyUnknown5
0, // kKeyUnknown6
0, // kKeyUnknown7
0, // kKeyUnknown8
0, // kKeyLShift
0, // kKeyRShift
0, // kKeyLControl
0, // kKeyRControl
0, // kKeyAlt
0, // kKeyAltGr
0, // kKeyLWin
0, // kKeyRWin
0, // kKeyMenu
0, // kKeyCommand
0, // kKeyScrLock
0, // kKeyNumLock
0, // kKeyCapsLock
};
if (scancode >= 0 && scancode < sizeof(map) / sizeof(map[0]))
return map[scancode];
else
return 0;
}
static void fill_menu_item_info_with_key(she::MenuItemInfo& info,
const char* commandId,
const Params& params = Params())
{
Key* key = KeyboardShortcuts::instance()->command(commandId, params);
if (key)
info.shortcut = get_os_shortcut_from_key(key);
}
she::Shortcut get_os_shortcut_from_key(Key* key)
{
if (key && !key->accels().empty()) {
const ui::Accelerator& accel = key->accels().front();
return she::Shortcut(
(accel.unicodeChar() ? accel.unicodeChar():
from_scancode_to_unicode(accel.scancode())),
accel.modifiers());
}
else
return she::Shortcut();
}
// static
AppMenus* AppMenus::instance()
{
@ -54,13 +218,20 @@ AppMenus* AppMenus::instance()
}
AppMenus::AppMenus()
: m_recentListMenuitem(NULL)
: m_recentListMenuitem(nullptr)
, m_osMenu(nullptr)
{
m_recentFilesConn =
App::instance()->recentFiles()->Changed.connect(
base::Bind(&AppMenus::rebuildRecentList, this));
}
AppMenus::~AppMenus()
{
if (m_osMenu)
m_osMenu->dispose();
}
void AppMenus::reload()
{
XmlDocumentRef doc(GuiXml::instance()->doc());
@ -93,6 +264,8 @@ void AppMenus::reload()
m_palettePopupMenu.reset(loadMenuById(handle, "palette_popup"));
m_inkPopupMenu.reset(loadMenuById(handle, "ink_popup"));
createNativeMenus();
////////////////////////////////////////
// Load keyboard shortcuts for commands
@ -125,7 +298,7 @@ void AppMenus::initTheme()
bool AppMenus::rebuildRecentList()
{
MenuItem* list_menuitem = m_recentListMenuitem;
AppMenuItem* list_menuitem = dynamic_cast<AppMenuItem*>(m_recentListMenuitem);
MenuItem* menuitem;
// Update the recent file list menu item
@ -166,6 +339,16 @@ bool AppMenus::rebuildRecentList()
menuitem->setEnabled(false);
submenu->addChild(menuitem);
}
// Sync native menus
if (list_menuitem->nativeMenuItem()) {
she::Menus* menus = she::instance()->menus();
she::Menu* osMenu = (menus ? menus->createMenu(): nullptr);
if (osMenu) {
createNativeSubmenus(osMenu, submenu);
list_menuitem->nativeMenuItem()->setSubmenu(osMenu);
}
}
}
return true;
@ -248,9 +431,12 @@ Widget* AppMenus::convertXmlelemToMenuitem(TiXmlElement* elem)
const char* id = elem->Attribute("id");
if (id) {
// Recent list menu
if (strcmp(id, "recent_list") == 0) {
if (std::strcmp(id, "recent_list") == 0) {
m_recentListMenuitem = menuitem;
}
else if (std::strcmp(id, "help_menu") == 0) {
m_helpMenuitem = menuitem;
}
}
// Has it a sub-menu (<menu>)?
@ -312,6 +498,24 @@ void AppMenus::applyShortcutToMenuitemsWithCommand(Menu* menu, Command* command,
}
}
void AppMenus::syncNativeMenuItemKeyShortcuts()
{
syncNativeMenuItemKeyShortcuts(m_rootMenu.get());
}
void AppMenus::syncNativeMenuItemKeyShortcuts(Menu* menu)
{
for (auto child : menu->children()) {
if (child->type() == kMenuItemWidget) {
if (AppMenuItem* menuitem = dynamic_cast<AppMenuItem*>(child))
menuitem->syncNativeMenuItemKeyShortcut();
if (Menu* submenu = static_cast<MenuItem*>(child)->getSubmenu())
syncNativeMenuItemKeyShortcuts(submenu);
}
}
}
// TODO redesign the list of popup menus, it might be an
// autogenerated widget from 'gen'
void AppMenus::updateMenusList()
@ -330,4 +534,149 @@ void AppMenus::updateMenusList()
m_menus.push_back(m_inkPopupMenu);
}
void AppMenus::createNativeMenus()
{
she::Menus* menus = she::instance()->menus();
if (!menus) // This platform doesn't support native menu items
return;
if (m_osMenu)
m_osMenu->dispose();
m_osMenu = menus->createMenu();
#ifdef __APPLE__ // Create default macOS app menus (App ... Window)
{
she::MenuItemInfo about("About " PACKAGE);
fill_menu_item_info_with_key(about, CommandId::About);
about.execute = []{
if (Manager::getDefault()->getForegroundWindow() == App::instance()->mainWindow()) {
Command* cmd = CommandsModule::instance()->getCommandByName(CommandId::About);
UIContext::instance()->executeCommand(cmd);
}
};
she::MenuItemInfo preferences("Preferences...");
fill_menu_item_info_with_key(preferences, CommandId::Options);
preferences.execute = []{
if (Manager::getDefault()->getForegroundWindow() == App::instance()->mainWindow()) {
Command* cmd = CommandsModule::instance()->getCommandByName(CommandId::Options);
UIContext::instance()->executeCommand(cmd);
}
};
she::MenuItemInfo hide("Hide " PACKAGE, she::MenuItemInfo::Hide);
hide.shortcut = she::Shortcut('h', she::kKeyCmdModifier);
she::MenuItemInfo quit("Quit " PACKAGE, she::MenuItemInfo::Quit);
quit.shortcut = she::Shortcut('q', she::kKeyCmdModifier);
she::Menu* appMenu = menus->createMenu();
appMenu->addItem(menus->createMenuItem(about));
appMenu->addItem(menus->createMenuItem(she::MenuItemInfo(she::MenuItemInfo::Separator)));
appMenu->addItem(menus->createMenuItem(preferences));
appMenu->addItem(menus->createMenuItem(she::MenuItemInfo(she::MenuItemInfo::Separator)));
appMenu->addItem(menus->createMenuItem(hide));
appMenu->addItem(menus->createMenuItem(she::MenuItemInfo("Hide Others", she::MenuItemInfo::HideOthers)));
appMenu->addItem(menus->createMenuItem(she::MenuItemInfo("Show All", she::MenuItemInfo::ShowAll)));
appMenu->addItem(menus->createMenuItem(she::MenuItemInfo(she::MenuItemInfo::Separator)));
appMenu->addItem(menus->createMenuItem(quit));
she::MenuItem* appItem = menus->createMenuItem(she::MenuItemInfo("App"));
appItem->setSubmenu(appMenu);
m_osMenu->addItem(appItem);
}
#endif
createNativeSubmenus(m_osMenu, m_rootMenu);
#ifdef __APPLE__
{
// Search the index where help menu is located (so the Window menu
// can take its place/index position)
int i = 0, helpIndex = int(m_rootMenu->children().size());
for (const auto child : m_rootMenu->children()) {
if (child == m_helpMenuitem) {
helpIndex = i;
break;
}
++i;
}
she::MenuItemInfo minimize("Minimize", she::MenuItemInfo::Minimize);
minimize.shortcut = she::Shortcut('m', she::kKeyCmdModifier);
she::Menu* windowMenu = menus->createMenu();
windowMenu->addItem(menus->createMenuItem(minimize));
windowMenu->addItem(menus->createMenuItem(she::MenuItemInfo("Zoom", she::MenuItemInfo::Zoom)));
she::MenuItem* windowItem = menus->createMenuItem(she::MenuItemInfo("Window"));
windowItem->setSubmenu(windowMenu);
// We use helpIndex+1 because the first index in m_osMenu is the
// App menu.
m_osMenu->insertItem(helpIndex+1, windowItem);
}
#endif
menus->setAppMenu(m_osMenu);
}
void AppMenus::createNativeSubmenus(she::Menu* osMenu, const ui::Menu* uiMenu)
{
she::Menus* menus = she::instance()->menus();
for (const auto& child : uiMenu->children()) {
she::MenuItemInfo info;
AppMenuItem* appMenuItem = dynamic_cast<AppMenuItem*>(child);
if (child->type() == kSeparatorWidget) {
info.type = she::MenuItemInfo::Separator;
}
else if (child->type() == kMenuItemWidget) {
info.type = she::MenuItemInfo::Normal;
info.text = child->text();
info.execute = [child]{
if (child->manager()->getForegroundWindow() == App::instance()->mainWindow())
((ui::MenuItem*)child)->executeClick();
};
info.validate = [child](she::MenuItem* item) {
if (child->manager()->getForegroundWindow() == App::instance()->mainWindow()) {
((ui::MenuItem*)child)->validateItem();
item->setEnabled(child->isEnabled());
item->setChecked(child->isSelected());
}
else {
// Disable item when there are a modal window
item->setEnabled(false);
}
};
if (appMenuItem && appMenuItem->getCommand()) {
fill_menu_item_info_with_key(
info,
appMenuItem->getCommand()->id().c_str(),
appMenuItem->getParams());
}
}
else {
ASSERT(false); // Unsupported menu item type
continue;
}
she::MenuItem* osItem = menus->createMenuItem(info);
if (osItem) {
osMenu->addItem(osItem);
if (appMenuItem)
appMenuItem->setNativeMenuItem(osItem);
if (child->type() == ui::kMenuItemWidget &&
((ui::MenuItem*)child)->hasSubmenu()) {
she::Menu* osSubmenu = menus->createMenu();
createNativeSubmenus(osSubmenu, ((ui::MenuItem*)child)->getSubmenu());
osItem->setSubmenu(osSubmenu);
}
}
}
}
} // namespace app

View File

@ -17,6 +17,11 @@
class TiXmlElement;
class TiXmlHandle;
namespace she {
class Menu;
class Shortcut;
}
namespace app {
class Key;
class Command;
@ -32,6 +37,8 @@ namespace app {
public:
static AppMenus* instance();
~AppMenus();
void reload();
void initTheme();
@ -52,6 +59,7 @@ namespace app {
Menu* getInkPopupMenu() { return m_inkPopupMenu; }
void applyShortcutToMenuitemsWithCommand(Command* command, const Params& params, Key* key);
void syncNativeMenuItemKeyShortcuts();
private:
Menu* loadMenuById(TiXmlHandle& handle, const char *id);
@ -59,10 +67,14 @@ namespace app {
Widget* convertXmlelemToMenuitem(TiXmlElement* elem);
Widget* createInvalidVersionMenuitem();
void applyShortcutToMenuitemsWithCommand(Menu* menu, Command* command, const Params& params, Key* key);
void syncNativeMenuItemKeyShortcuts(Menu* menu);
void updateMenusList();
void createNativeMenus();
void createNativeSubmenus(she::Menu* osMenu, const ui::Menu* uiMenu);
base::UniquePtr<Menu> m_rootMenu;
MenuItem* m_recentListMenuitem;
MenuItem* m_helpMenuitem;
base::UniquePtr<Menu> m_tabPopupMenu;
base::UniquePtr<Menu> m_documentTabPopupMenu;
base::UniquePtr<Menu> m_layerPopupMenu;
@ -75,8 +87,11 @@ namespace app {
base::UniquePtr<Menu> m_inkPopupMenu;
obs::scoped_connection m_recentFilesConn;
std::vector<Menu*> m_menus;
she::Menu* m_osMenu;
};
she::Shortcut get_os_shortcut_from_key(Key* key);
} // namespace app
#endif

View File

@ -690,6 +690,8 @@ void KeyboardShortcutsCommand::onExecute(Context* context)
else {
window.restoreKeys();
}
AppMenus::instance()->syncNativeMenuItemKeyShortcuts();
}
Command* CommandFactory::createKeyboardShortcutsCommand()

View File

@ -429,22 +429,13 @@ bool CustomizedGuiManager::onProcessMessage(Message* msg)
Command* command = key->command();
// Commands are executed only when the main window is
// the current window running at foreground.
for (auto childWidget : children()) {
Window* child = static_cast<Window*>(childWidget);
// There are a foreground window executing?
if (child->isForeground()) {
break;
}
// Is it the desktop and the top-window=
else if (child->isDesktop() && child == App::instance()->mainWindow()) {
// OK, so we can execute the command represented
// by the pressed-key in the message...
UIContext::instance()->executeCommand(
command, key->params());
return true;
}
// the current window running.
if (getForegroundWindow() == App::instance()->mainWindow()) {
// OK, so we can execute the command represented
// by the pressed-key in the message...
UIContext::instance()->executeCommand(
command, key->params());
return true;
}
break;
}

View File

@ -10,11 +10,13 @@
#include "app/ui/app_menuitem.h"
#include "app/app_menus.h"
#include "app/commands/command.h"
#include "app/commands/params.h"
#include "app/modules/gui.h"
#include "app/ui/keyboard_shortcuts.h"
#include "app/ui_context.h"
#include "she/menus.h"
#include "ui/accelerator.h"
#include "ui/menu.h"
#include "ui/message.h"
@ -34,12 +36,30 @@ Params AppMenuItem::s_contextParams;
AppMenuItem::AppMenuItem(const char* text, Command* command, const Params& params)
: MenuItem(text)
, m_key(NULL)
, m_key(nullptr)
, m_command(command)
, m_params(params)
, m_nativeMenuItem(nullptr)
{
}
void AppMenuItem::setKey(Key* key)
{
m_key = key;
syncNativeMenuItemKeyShortcut();
}
void AppMenuItem::setNativeMenuItem(she::MenuItem* nativeMenuItem)
{
m_nativeMenuItem = nativeMenuItem;
}
void AppMenuItem::syncNativeMenuItemKeyShortcut()
{
if (m_nativeMenuItem)
m_nativeMenuItem->setShortcut(get_os_shortcut_from_key(m_key));
}
// static
void AppMenuItem::setContextParams(const Params& params)
{
@ -54,28 +74,6 @@ bool AppMenuItem::onProcessMessage(Message* msg)
// disable the menu (the keyboard shortcuts are processed by "manager_msg_proc")
setEnabled(false);
break;
default:
if (msg->type() == kOpenMessage ||
msg->type() == kOpenMenuItemMessage) {
// Update the context flags after opening the menuitem's
// submenu to update the "enabled" flag of each command
// correctly.
Context* context = UIContext::instance();
context->updateFlags();
if (m_command) {
Params params = m_params;
if (!s_contextParams.empty())
params |= s_contextParams;
m_command->loadParams(params);
setEnabled(m_command->isEnabled(context));
setSelected(m_command->isChecked(context));
}
}
break;
}
return MenuItem::onProcessMessage(msg);
@ -113,11 +111,34 @@ void AppMenuItem::onClick()
if (!s_contextParams.empty())
params |= s_contextParams;
// Load parameters to call Command::isEnabled, so we can check if
// the command is enabled with this parameters.
m_command->loadParams(params);
UIContext* context = UIContext::instance();
if (m_command->isEnabled(context))
if (m_command->isEnabled(context)) {
context->executeCommand(m_command, params);
}
}
}
void AppMenuItem::onValidate()
{
// Update the context flags after opening the menuitem's
// submenu to update the "enabled" flag of each command
// correctly.
Context* context = UIContext::instance();
context->updateFlags();
if (m_command) {
Params params = m_params;
if (!s_contextParams.empty())
params |= s_contextParams;
m_command->loadParams(params);
setEnabled(m_command->isEnabled(context));
setSelected(m_command->isChecked(context));
}
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2001-2015 David Capello
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -11,6 +11,10 @@
#include "app/commands/params.h"
#include "ui/menu.h"
namespace she {
class MenuItem;
}
namespace app {
class Key;
class Command;
@ -25,22 +29,28 @@ namespace app {
AppMenuItem(const char* text, Command* command = nullptr, const Params& params = Params());
Key* key() { return m_key; }
void setKey(Key* key) { m_key = key; }
void setKey(Key* key);
Command* getCommand() { return m_command; }
const Params& getParams() const { return m_params; }
she::MenuItem* nativeMenuItem() { return m_nativeMenuItem; }
void setNativeMenuItem(she::MenuItem* nativeMenuItem);
void syncNativeMenuItemKeyShortcut();
static void setContextParams(const Params& params);
protected:
bool onProcessMessage(ui::Message* msg) override;
void onSizeHint(ui::SizeHintEvent& ev) override;
void onClick() override;
void onValidate() override;
private:
Key* m_key;
Command* m_command;
Params m_params;
she::MenuItem* m_nativeMenuItem;
static Params s_contextParams;
};

View File

@ -227,6 +227,7 @@ endif()
if(APPLE)
list(APPEND SHE_SOURCES
osx/logger.mm
osx/menus.mm
osx/native_dialogs.mm)
endif()

View File

@ -1,5 +1,9 @@
# SHE - Simplified Hardware Entry-point
SHE is an abstraction layer to access in different way to the
hardware/operating system. It will use Allegro 4, Allegro 5 or SDL
libraries, but will be easily portable to other back-ends.
`she` is an abstraction layer to access in different way to the
hardware/operating system. It can be implemented with different
back-ends:
* Previous version were using Allegro 4 (it still uses Allegro 4 on Linux)
* Now we use our own implementation on Windows and macOS to handle
events, and [Skia](https://skia.org/) to render graphics.

View File

@ -11,6 +11,7 @@
#ifdef _WIN32
#include "she/win/native_dialogs.h"
#elif defined(__APPLE__)
#include "she/osx/menus.h"
#include "she/osx/native_dialogs.h"
#elif defined(ASEPRITE_WITH_GTK_FILE_DIALOG_SUPPORT) && defined(__linux__)
#include "she/gtk/native_dialogs.h"
@ -33,11 +34,13 @@ Logger* getOsxLogger();
class CommonSystem : public System {
public:
CommonSystem()
: m_nativeDialogs(nullptr) {
: m_nativeDialogs(nullptr)
, m_menus(nullptr) {
}
~CommonSystem() {
delete m_nativeDialogs;
delete m_menus;
}
void dispose() override {
@ -52,6 +55,14 @@ public:
#endif
}
Menus* menus() override {
#ifdef __APPLE__
if (!m_menus)
m_menus = new MenusOSX();
#endif
return m_menus;
}
NativeDialogs* nativeDialogs() override {
#ifdef _WIN32
if (!m_nativeDialogs)
@ -103,6 +114,7 @@ public:
private:
NativeDialogs* m_nativeDialogs;
Menus* m_menus;
base::UniquePtr<ft::Lib> m_ft;
};

View File

@ -1,5 +1,5 @@
// SHE library
// Copyright (C) 2012-2016 David Capello
// Copyright (C) 2012-2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -22,7 +22,9 @@ namespace she {
};
// TODO These are virtual key code (not scancodes), we should rename
// it to KeyCodes or something similar.
// it to KeyCodes or use Unicode directly as on macOS (some
// special keys like F1, arrow keys, etc. have a special
// unicode value).
enum KeyScancode {
kKeyNil = 0,
kKeyA = 1,
@ -150,7 +152,7 @@ namespace she {
kKeyLWin = 120,
kKeyRWin = 121,
kKeyMenu = 122,
kKeyCommand = 123, // macOS - TODO This should be inside the modifiers range
kKeyCommand = 123,
kKeyScrLock = 124,
kKeyNumLock = 125,
kKeyCapsLock = 126,

92
src/she/menus.h Normal file
View File

@ -0,0 +1,92 @@
// SHE library
// Copyright (C) 2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef SHE_MENUS_H_INCLUDED
#define SHE_MENUS_H_INCLUDED
#pragma once
#include "she/keys.h"
#include "she/shortcut.h"
#include <functional>
#include <string>
namespace she {
class MenuItem;
struct MenuItemInfo {
enum Type {
Normal,
Separator
};
enum Action {
UserDefined,
// macOS standard commands
Hide,
HideOthers,
ShowAll,
Quit,
Minimize,
Zoom,
};
Type type;
Action action;
std::string text;
Shortcut shortcut;
std::function<void()> execute;
std::function<void(she::MenuItem*)> validate;
explicit MenuItemInfo(const Type type = Normal,
const Action action = UserDefined)
: type(type)
, action(action) {
}
explicit MenuItemInfo(const char* text,
const Action action = UserDefined)
: type(Normal)
, action(action)
, text(text) {
}
};
class Menu;
class MenuItem;
class Menus;
class MenuItem {
public:
virtual ~MenuItem() { }
virtual void dispose() = 0;
virtual void setText(const std::string& text) = 0;
virtual void setSubmenu(Menu* submenu) = 0;
virtual void setEnabled(bool state) = 0;
virtual void setChecked(bool state) = 0;
virtual void setShortcut(const Shortcut& shortcut) = 0;
};
class Menu {
public:
virtual ~Menu() { }
virtual void dispose() = 0;
virtual void addItem(MenuItem* item) = 0;
virtual void insertItem(const int index, MenuItem* item) = 0;
};
class Menus {
public:
virtual ~Menus() { }
virtual Menu* createMenu() = 0;
virtual MenuItem* createMenuItem(const MenuItemInfo& info) = 0;
virtual void setAppMenu(Menu* menu) = 0;
};
} // namespace she
#endif

View File

@ -1,5 +1,5 @@
// SHE library
// Copyright (C) 2012-2016 David Capello
// Copyright (C) 2012-2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -17,6 +17,8 @@
- (void)applicationWillResignActive:(NSNotification*)notification;
- (void)applicationDidBecomeActive:(NSNotification*)notification;
- (BOOL)application:(NSApplication*)app openFiles:(NSArray*)filenames;
- (void)executeMenuItem:(id)sender;
- (BOOL)validateMenuItem:(NSMenuItem*)menuItem;
@end
#endif

View File

@ -1,5 +1,5 @@
// SHE library
// Copyright (C) 2012-2016 David Capello
// Copyright (C) 2012-2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -20,11 +20,18 @@
#include "she/osx/view.h"
#include "she/system.h"
@protocol OSXValidateMenuItemProtocol
- (void)validateMenuItem;
@end
@implementation OSXAppDelegate
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender
{
return NSTerminateNow;
she::Event ev;
ev.setType(she::Event::CloseDisplay);
she::queue_event(ev);
return NSTerminateCancel;
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)app
@ -34,9 +41,6 @@
- (void)applicationWillTerminate:(NSNotification*)notification
{
she::Event ev;
ev.setType(she::Event::CloseDisplay);
she::queue_event(ev);
}
- (void)applicationWillResignActive:(NSNotification*)notification
@ -61,4 +65,19 @@
return YES;
}
- (void)executeMenuItem:(id)sender
{
[sender executeMenuItem:sender];
}
- (BOOL)validateMenuItem:(NSMenuItem*)menuItem
{
if ([menuItem respondsToSelector:@selector(validateMenuItem)]) {
[((id<OSXValidateMenuItemProtocol>)menuItem) validateMenuItem];
return menuItem.enabled;
}
else
return [super validateMenuItem:menuItem];
}
@end

25
src/she/osx/menus.h Normal file
View File

@ -0,0 +1,25 @@
// SHE library
// Copyright (C) 2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef SHE_OSX_MENUS_H_INCLUDED
#define SHE_OSX_MENUS_H_INCLUDED
#pragma once
#include "she/menus.h"
namespace she {
class MenusOSX : public Menus {
public:
MenusOSX();
Menu* createMenu() override;
MenuItem* createMenuItem(const MenuItemInfo& info) override;
void setAppMenu(Menu* menu) override;
};
} // namespace she
#endif

288
src/she/osx/menus.mm Normal file
View File

@ -0,0 +1,288 @@
// SHE library
// Copyright (C) 2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#include <Cocoa/Cocoa.h>
#include "base/debug.h"
#include "base/string.h"
#include "she/osx/menus.h"
#include "she/shortcut.h"
namespace she {
class MenuItemOSX;
}
@interface OSXNSMenu : NSMenu
- (BOOL)performKeyEquivalent:(NSEvent*)event;
@end
@interface OSXNSMenuItem : NSMenuItem {
she::MenuItemOSX* original;
}
+ (OSXNSMenuItem*)alloc:(she::MenuItemOSX*)original;
- (void)executeMenuItem:(id)sender;
- (void)validateMenuItem;
@end
namespace she {
extern bool g_keyEquivalentUsed;
class MenuItemOSX : public MenuItem {
public:
MenuItemOSX(const MenuItemInfo& info);
void dispose() override;
void setText(const std::string& text) override;
void setSubmenu(Menu* submenu) override;
void setEnabled(bool state) override;
void setChecked(bool state) override;
void setShortcut(const Shortcut& shortcut) override;
NSMenuItem* handle() { return m_handle; }
// Called by OSXNSMenuItem.executeMenuItem
void execute();
void validate();
private:
void syncTitle();
NSMenuItem* m_handle;
Menu* m_submenu;
std::function<void()> m_execute;
std::function<void(she::MenuItem*)> m_validate;
};
class MenuOSX : public Menu {
public:
MenuOSX();
void dispose() override;
void addItem(MenuItem* item) override;
void insertItem(const int index, MenuItem* item) override;
NSMenu* handle() { return m_handle; }
private:
NSMenu* m_handle;
};
} // namespace she
@implementation OSXNSMenu
- (BOOL)performKeyEquivalent:(NSEvent*)event
{
BOOL result = [super performKeyEquivalent:event];
if (result)
she::g_keyEquivalentUsed = true;
return result;
}
@end
@implementation OSXNSMenuItem
+ (OSXNSMenuItem*)alloc:(she::MenuItemOSX*)original
{
OSXNSMenuItem* item = [super alloc];
item->original = original;
return item;
}
- (void)executeMenuItem:(id)sender
{
original->execute();
}
- (void)validateMenuItem
{
original->validate();
}
@end
namespace she {
MenuItemOSX::MenuItemOSX(const MenuItemInfo& info)
: m_handle(nullptr)
, m_submenu(nullptr)
{
switch (info.type) {
case MenuItemInfo::Normal: {
SEL sel = nil;
id target = nil;
switch (info.action) {
case MenuItemInfo::UserDefined:
sel = @selector(executeMenuItem:);
// TODO this is strange, it doesn't work, we receive the
// message in OSXAppDelegate anyway. So
// OSXAppDelegate.executeMenuItem: will redirect the message
// to OSXNSMenuItem.executeMenuItem:
target = m_handle;
break;
case MenuItemInfo::Hide:
sel = @selector(hide:);
break;
case MenuItemInfo::HideOthers:
sel = @selector(hideOtherApplications:);
break;
case MenuItemInfo::ShowAll:
sel = @selector(unhideAllApplications:);
break;
case MenuItemInfo::Quit:
sel = @selector(terminate:);
break;
case MenuItemInfo::Minimize:
sel = @selector(performMiniaturize:);
break;
case MenuItemInfo::Zoom:
sel = @selector(performZoom:);
break;
}
m_handle =
[[OSXNSMenuItem alloc:this]
initWithTitle:[NSString stringWithUTF8String:info.text.c_str()]
action:sel
keyEquivalent:@""];
m_handle.target = target;
m_execute = info.execute;
m_validate = info.validate;
if (!info.shortcut.isEmpty())
setShortcut(info.shortcut);
break;
}
case MenuItemInfo::Separator:
m_handle = [NSMenuItem separatorItem];
break;
}
}
void MenuItemOSX::dispose()
{
delete this;
}
void MenuItemOSX::setText(const std::string& text)
{
[m_handle setTitle:[NSString stringWithUTF8String:text.c_str()]];
syncTitle();
}
void MenuItemOSX::setSubmenu(Menu* submenu)
{
m_submenu = submenu;
if (submenu) {
[m_handle setSubmenu:((MenuOSX*)submenu)->handle()];
syncTitle();
}
else
[m_handle setSubmenu:nil];
}
void MenuItemOSX::setEnabled(bool state)
{
m_handle.enabled = state;
}
void MenuItemOSX::setChecked(bool state)
{
if (state)
m_handle.state = NSOnState;
else
m_handle.state = NSOffState;
}
void MenuItemOSX::setShortcut(const Shortcut& shortcut)
{
KeyModifiers mods = shortcut.modifiers();
NSEventModifierFlags nsFlags = 0;
if (mods & kKeyShiftModifier) nsFlags |= NSEventModifierFlagShift;
if (mods & kKeyCtrlModifier) nsFlags |= NSEventModifierFlagControl;
if (mods & kKeyAltModifier) nsFlags |= NSEventModifierFlagOption;
if (mods & kKeyCmdModifier) nsFlags |= NSEventModifierFlagCommand;
NSString* keyStr;
if (shortcut.unicode()) {
unichar chr = shortcut.unicode();
keyStr = [NSString stringWithCharacters:&chr length:1];
}
else
keyStr = @"";
m_handle.keyEquivalent = keyStr;
m_handle.keyEquivalentModifierMask = nsFlags;
}
void MenuItemOSX::execute()
{
if (m_execute)
m_execute();
}
void MenuItemOSX::validate()
{
if (m_validate)
m_validate(this);
}
void MenuItemOSX::syncTitle()
{
// On macOS the submenu title is the one that is displayed in the
// UI instead of the MenuItem title (so here we copy the menu item
// title to the submenu title)
if (m_submenu)
[((MenuOSX*)m_submenu)->handle() setTitle:m_handle.title];
}
MenuOSX::MenuOSX()
{
m_handle = [[OSXNSMenu alloc] init];
}
void MenuOSX::dispose()
{
delete this;
}
void MenuOSX::addItem(MenuItem* item)
{
ASSERT(item);
[m_handle addItem:((MenuItemOSX*)item)->handle()];
}
void MenuOSX::insertItem(const int index, MenuItem* item)
{
ASSERT(item);
[m_handle insertItem:((MenuItemOSX*)item)->handle()
atIndex:index];
}
MenusOSX::MenusOSX()
{
}
Menu* MenusOSX::createMenu()
{
return new MenuOSX;
}
MenuItem* MenusOSX::createMenuItem(const MenuItemInfo& info)
{
return new MenuItemOSX(info);
}
void MenusOSX::setAppMenu(Menu* menu)
{
if (menu)
[NSApp setMainMenu:((MenuOSX*)menu)->handle()];
else
[NSApp setMainMenu:nil];
}
} // namespace she

View File

@ -4,6 +4,8 @@
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#define KEY_TRACE(...)
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
@ -20,6 +22,12 @@
namespace she {
// Global variable used between View and OSXNSMenu to check if the
// keyDown: event was used by a key equivalent in the menu.
//
// TODO I'm not proud of this, but it does the job
bool g_keyEquivalentUsed = false;
bool osx_is_key_pressed(KeyScancode scancode);
namespace {
@ -167,8 +175,14 @@ using namespace she;
- (void)keyDown:(NSEvent*)event
{
g_keyEquivalentUsed = false;
[super keyDown:event];
// If a key equivalent used the keyDown event, we don't generate
// this she::KeyDown event.
if (g_keyEquivalentUsed)
return;
KeyScancode scancode = scancode_from_nsevent(event);
Event ev;
ev.setType(Event::KeyDown);
@ -212,6 +226,10 @@ using namespace she;
}
}
KEY_TRACE("View keyDown: unicode=%d (%c) scancode=%d modifiers=%d\n",
ev.unicodeChar(), ev.unicodeChar(),
ev.scancode(), ev.modifiers());
if (sendMsg)
queue_event(ev);
}

View File

@ -79,13 +79,13 @@ static KeyScancode from_char_to_scancode(int chr)
kKey7, // 55 = 37 = 7
kKey8, // 56 = 38 = 8
kKey9, // 57 = 39 = 9
kKeyNil, // 58 = 3A = :
kKeyColon, // 58 = 3A = :
kKeySemicolon, // 59 = 3B = ;
kKeyNil, // 60 = 3C = <
kKeyEquals, // 61 = 3D = =
kKeyNil, // 62 = 3E = >
kKeyNil, // 63 = 3F = ?
kKeyNil, // 64 = 40 = @
kKeyAt, // 64 = 40 = @
kKeyA, // 65 = 41 = A
kKeyB, // 66 = 42 = B
kKeyC, // 67 = 43 = C
@ -115,9 +115,9 @@ static KeyScancode from_char_to_scancode(int chr)
kKeyOpenbrace, // 91 = 5B = [
kKeyBackslash, // 92 = 5C = backslash
kKeyClosebrace, // 93 = 5D = ]
kKeyNil, // 94 = 5E = ^
kKeyCircumflex, // 94 = 5E = ^
kKeyNil, // 95 = 5F = _
kKeyNil, // 96 = 60 = `
kKeyBackquote, // 96 = 60 = `
kKeyA, // 97 = 61 = a
kKeyB, // 98 = 62 = b
kKeyC, // 99 = 63 = c
@ -147,7 +147,7 @@ static KeyScancode from_char_to_scancode(int chr)
kKeyOpenbrace, // 123 = 7B = {
kKeyBackslash, // 124 = 7C = |
kKeyClosebrace, // 125 = 7D = }
kKeyNil, // 126 = 7E = ~
kKeyTilde, // 126 = 7E = ~
kKeyNil, // 127 = 7F = DEL
};

View File

@ -53,6 +53,10 @@ using namespace she;
[self center];
[self makeKeyAndOrderFront:self];
// Hide the "View > Show Tab Bar" menu item
if ([self respondsToSelector:@selector(setTabbingMode:)])
[self setTabbingMode:NSWindowTabbingModeDisallowed];
return self;
}

View File

@ -1,5 +1,5 @@
// SHE library
// Copyright (C) 2012-2016 David Capello
// Copyright (C) 2012-2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -8,13 +8,24 @@
#define SHE_H_INCLUDED
#pragma once
#include "she/capabilities.h"
#include "she/display.h"
#include "she/display_handle.h"
#include "she/draw_text.h"
#include "she/error.h"
#include "she/event.h"
#include "she/event_queue.h"
#include "she/font.h"
#include "she/keys.h"
#include "she/logger.h"
#include "she/menus.h"
#include "she/native_cursor.h"
#include "she/native_dialogs.h"
#include "she/pointer_type.h"
#include "she/scoped_handle.h"
#include "she/shortcut.h"
#include "she/surface.h"
#include "she/surface_format.h"
#include "she/system.h"
#endif

35
src/she/shortcut.h Normal file
View File

@ -0,0 +1,35 @@
// SHE library
// Copyright (C) 2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef SHE_SHORTCUT_H_INCLUDED
#define SHE_SHORTCUT_H_INCLUDED
#pragma once
#include "she/keys.h"
namespace she {
class Shortcut {
public:
Shortcut(int unicode = 0,
KeyModifiers modifiers = kKeyNoneModifier)
: m_unicode(unicode)
, m_modifiers(modifiers) {
}
int unicode() const { return m_unicode; }
KeyModifiers modifiers() const { return m_modifiers; }
bool isEmpty() const { return m_unicode == 0; }
private:
int m_unicode;
KeyModifiers m_modifiers;
};
} // namespace she
#endif

View File

@ -1,5 +1,5 @@
// SHE library
// Copyright (C) 2012-2016 David Capello
// Copyright (C) 2012-2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -20,6 +20,7 @@ namespace she {
class EventQueue;
class Font;
class Logger;
class Menus;
class NativeDialogs;
class Surface;
@ -37,6 +38,7 @@ namespace she {
virtual void finishLaunching() = 0;
virtual Capabilities capabilities() const = 0;
virtual Logger* logger() = 0;
virtual Menus* menus() = 0;
virtual NativeDialogs* nativeDialogs() = 0;
virtual EventQueue* eventQueue() = 0;
virtual bool gpuAcceleration() const = 0;

View File

@ -721,7 +721,12 @@ bool MenuItem::onProcessMessage(Message* msg)
break;
default:
if (msg->type() == kOpenMenuItemMessage) {
if (msg->type() == kOpenMessage) {
validateItem();
}
else if (msg->type() == kOpenMenuItemMessage) {
validateItem();
MenuBaseData* base = get_base(this);
bool select_first = static_cast<OpenMenuItemMessage*>(msg)->select_first();
@ -895,6 +900,12 @@ void MenuItem::onClick()
Click();
}
void MenuItem::onValidate()
{
// Here the user can customize the automatic validation of the menu
// item before it's shown.
}
void MenuItem::onSizeHint(SizeHintEvent& ev)
{
Size size(0, 0);
@ -1220,6 +1231,11 @@ void MenuItem::executeClick()
Manager::getDefault()->enqueueMessage(msg);
}
void MenuItem::validateItem()
{
onValidate();
}
static MenuItem* check_for_letter(Menu* menu, const KeyMessage* keymsg)
{
for (auto child : menu->children()) {

View File

@ -123,6 +123,9 @@ namespace ui {
return m_submenu_menubox;
}
void executeClick();
void validateItem();
// Fired when the menu item is clicked.
obs::signal<void()> Click;
@ -132,6 +135,7 @@ namespace ui {
void onPaint(PaintEvent& ev) override;
void onSizeHint(SizeHintEvent& ev) override;
virtual void onClick();
virtual void onValidate();
bool inBar();
@ -140,7 +144,6 @@ namespace ui {
void closeSubmenu(bool last_of_close_chain);
void startTimer();
void stopTimer();
void executeClick();
bool m_highlighted; // Is it highlighted?
Menu* m_submenu; // The sub-menu