Merge pull request #105 from atom/windows-menu

Implement menu API on Windows, fixes #75.
This commit is contained in:
Cheng Zhao 2013-10-06 17:58:42 -07:00
commit 6a712d4db4
21 changed files with 530 additions and 200 deletions

View file

@ -351,6 +351,10 @@ void Menu::Initialize(v8::Handle<v8::Object> target) {
NODE_SET_PROTOTYPE_METHOD(t, "popup", Popup); NODE_SET_PROTOTYPE_METHOD(t, "popup", Popup);
#if defined(OS_WIN)
NODE_SET_PROTOTYPE_METHOD(t, "attachToWindow", AttachToWindow);
#endif
target->Set(v8::String::NewSymbol("Menu"), t->GetFunction()); target->Set(v8::String::NewSymbol("Menu"), t->GetFunction());
#if defined(OS_MACOSX) #if defined(OS_MACOSX)

View file

@ -68,7 +68,9 @@ class Menu : public EventEmitter,
static v8::Handle<v8::Value> Popup(const v8::Arguments &args); static v8::Handle<v8::Value> Popup(const v8::Arguments &args);
#if defined(OS_MACOSX) #if defined(OS_WIN)
static v8::Handle<v8::Value> AttachToWindow(const v8::Arguments &args);
#elif defined(OS_MACOSX)
static v8::Handle<v8::Value> SetApplicationMenu(const v8::Arguments &args); static v8::Handle<v8::Value> SetApplicationMenu(const v8::Arguments &args);
static v8::Handle<v8::Value> SendActionToFirstResponder( static v8::Handle<v8::Value> SendActionToFirstResponder(
const v8::Arguments &args); const v8::Arguments &args);

View file

@ -4,8 +4,11 @@
#include "browser/api/atom_api_menu_win.h" #include "browser/api/atom_api_menu_win.h"
#include "browser/native_window_win.h"
#include "browser/ui/win/menu_2.h" #include "browser/ui/win/menu_2.h"
#include "common/v8_conversions.h"
#include "ui/gfx/point.h" #include "ui/gfx/point.h"
#include "ui/gfx/screen.h"
namespace atom { namespace atom {
@ -19,8 +22,26 @@ MenuWin::~MenuWin() {
} }
void MenuWin::Popup(NativeWindow* native_window) { void MenuWin::Popup(NativeWindow* native_window) {
gfx::Point cursor = gfx::Screen::GetNativeScreen()->GetCursorScreenPoint();
menu_.reset(new atom::Menu2(model_.get())); menu_.reset(new atom::Menu2(model_.get()));
menu_->RunContextMenuAt(gfx::Point(0, 0)); menu_->RunContextMenuAt(cursor);
}
// static
v8::Handle<v8::Value> Menu::AttachToWindow(const v8::Arguments& args) {
v8::HandleScope scope;
Menu* self = ObjectWrap::Unwrap<Menu>(args.This());
if (self == NULL)
return node::ThrowError("Menu is already destroyed");
NativeWindow* native_window;
if (!FromV8Arguments(args, &native_window))
return node::ThrowTypeError("Bad argument");
static_cast<NativeWindowWin*>(native_window)->SetMenu(self->model_.get());
return v8::Undefined();
} }
// static // static

View file

@ -101,6 +101,9 @@ v8::Handle<v8::Value> Window::New(const v8::Arguments &args) {
new Window(args.This(), static_cast<base::DictionaryValue*>(options.get())); new Window(args.This(), static_cast<base::DictionaryValue*>(options.get()));
// Give js code a chance to do initialization.
node::MakeCallback(args.This(), "_init", 0, NULL);
return args.This(); return args.This();
} }

View file

@ -9,6 +9,15 @@ app = new Application
app.getHomeDir = -> app.getHomeDir = ->
process.env[if process.platform is 'win32' then 'USERPROFILE' else 'HOME'] process.env[if process.platform is 'win32' then 'USERPROFILE' else 'HOME']
app.getBrowserWindows = ->
require('../../atom/objects-registry.js').getAllWindows()
app.setApplicationMenu = (menu) ->
require('menu').setApplicationMenu menu
app.getApplicationMenu = ->
require('menu').getApplicationMenu()
app.commandLine = app.commandLine =
appendSwitch: bindings.appendSwitch, appendSwitch: bindings.appendSwitch,
appendArgument: bindings.appendArgument appendArgument: bindings.appendArgument

View file

@ -1,10 +1,16 @@
EventEmitter = require('events').EventEmitter EventEmitter = require('events').EventEmitter
app = require 'app'
v8Util = process.atomBinding 'v8_util' v8Util = process.atomBinding 'v8_util'
objectsRegistry = require '../../atom/objects-registry.js'
BrowserWindow = process.atomBinding('window').BrowserWindow BrowserWindow = process.atomBinding('window').BrowserWindow
BrowserWindow::__proto__ = EventEmitter.prototype BrowserWindow::__proto__ = EventEmitter.prototype
BrowserWindow::_init = ->
# Simulate the application menu on platforms other than OS X.
if process.platform isnt 'darwin'
menu = app.getApplicationMenu()
@setMenu menu if menu?
BrowserWindow::toggleDevTools = -> BrowserWindow::toggleDevTools = ->
opened = v8Util.getHiddenValue this, 'devtoolsOpened' opened = v8Util.getHiddenValue this, 'devtoolsOpened'
if opened if opened
@ -17,12 +23,20 @@ BrowserWindow::toggleDevTools = ->
BrowserWindow::restart = -> BrowserWindow::restart = ->
@loadUrl(@getUrl()) @loadUrl(@getUrl())
BrowserWindow::setMenu = (menu) ->
throw new Error('BrowserWindow.setMenu is only available on Windows') unless process.platform is 'win32'
throw new TypeError('Invalid menu') unless menu?.constructor?.name is 'Menu'
@menu = menu # Keep a reference of menu in case of GC.
@menu.attachToWindow this
BrowserWindow.getFocusedWindow = -> BrowserWindow.getFocusedWindow = ->
windows = objectsRegistry.getAllWindows() windows = app.getBrowserWindows()
return window for window in windows when window.isFocused() return window for window in windows when window.isFocused()
BrowserWindow.fromProcessIdAndRoutingId = (processId, routingId) -> BrowserWindow.fromProcessIdAndRoutingId = (processId, routingId) ->
windows = objectsRegistry.getAllWindows() windows = app.getBrowserWindows()
return window for window in windows when window.getProcessId() == processId and return window for window in windows when window.getProcessId() == processId and
window.getRoutingId() == routingId window.getRoutingId() == routingId

View file

@ -1,3 +1,5 @@
BrowserWindow = require 'browser-window'
nextCommandId = 0 nextCommandId = 0
class MenuItem class MenuItem
@ -26,7 +28,7 @@ class MenuItem
@commandId = ++nextCommandId @commandId = ++nextCommandId
@click = => @click = =>
if typeof click is 'function' if typeof click is 'function'
click.apply this, arguments click this, BrowserWindow.getFocusedWindow()
else if typeof @selector is 'string' else if typeof @selector is 'string'
Menu.sendActionToFirstResponder @selector Menu.sendActionToFirstResponder @selector

View file

@ -3,6 +3,7 @@ EventEmitter = require('events').EventEmitter
IDWeakMap = require 'id-weak-map' IDWeakMap = require 'id-weak-map'
MenuItem = require 'menu-item' MenuItem = require 'menu-item'
app = require 'app'
bindings = process.atomBinding 'menu' bindings = process.atomBinding 'menu'
Menu = bindings.Menu Menu = bindings.Menu
@ -39,13 +40,22 @@ Menu::insert = (pos, item) ->
getAcceleratorForCommandId: (commandId) => @commandsMap[commandId]?.accelerator getAcceleratorForCommandId: (commandId) => @commandsMap[commandId]?.accelerator
executeCommand: (commandId) => executeCommand: (commandId) =>
activeItem = @commandsMap[commandId] activeItem = @commandsMap[commandId]
activeItem.click(activeItem) if activeItem? activeItem.click() if activeItem?
@items.splice pos, 0, item @items.splice pos, 0, item
@commandsMap[item.commandId] = item @commandsMap[item.commandId] = item
applicationMenu = null
Menu.setApplicationMenu = (menu) -> Menu.setApplicationMenu = (menu) ->
throw new TypeError('Invalid menu') unless menu?.constructor is Menu throw new TypeError('Invalid menu') unless menu?.constructor is Menu
bindings.setApplicationMenu menu applicationMenu = menu # Keep a reference.
if process.platform is 'darwin'
bindings.setApplicationMenu menu
else
windows = app.getBrowserWindows()
w.setMenu menu for w in windows
Menu.getApplicationMenu = -> applicationMenu
Menu.sendActionToFirstResponder = bindings.sendActionToFirstResponder Menu.sendActionToFirstResponder = bindings.sendActionToFirstResponder

View file

@ -1,7 +1,9 @@
BrowserWindow = require 'browser-window' BrowserWindow = require 'browser-window'
EventEmitter = require('events').EventEmitter
IDWeakMap = require 'id-weak-map' IDWeakMap = require 'id-weak-map'
v8Util = process.atomBinding 'v8_util' v8Util = process.atomBinding 'v8_util'
# Class to reference all objects.
class ObjectsStore class ObjectsStore
@stores = {} @stores = {}
@ -37,46 +39,57 @@ class ObjectsStore
key = "#{processId}_#{routingId}" key = "#{processId}_#{routingId}"
delete @stores[key] delete @stores[key]
# Objects in weak map will be not referenced (so we won't leak memory), and class ObjectsRegistry extends EventEmitter
# every object created in browser will have a unique id in weak map. constructor: ->
objectsWeakMap = new IDWeakMap # Objects in weak map will be not referenced (so we won't leak memory), and
objectsWeakMap.add = (obj) -> # every object created in browser will have a unique id in weak map.
id = IDWeakMap::add.call this, obj @objectsWeakMap = new IDWeakMap
v8Util.setHiddenValue obj, 'atomId', id @objectsWeakMap.add = (obj) ->
id id = IDWeakMap::add.call this, obj
v8Util.setHiddenValue obj, 'atomId', id
id
windowsWeakMap = new IDWeakMap # Remember all windows in the weak map.
@windowsWeakMap = new IDWeakMap
process.on 'ATOM_BROWSER_INTERNAL_NEW', (obj) =>
if obj.constructor is BrowserWindow
id = @windowsWeakMap.add obj
obj.on 'destroyed', => @windowsWeakMap.remove id
process.on 'ATOM_BROWSER_INTERNAL_NEW', (obj) -> # Register a new object, the object would be kept referenced until you release
# Remember all windows. # it explicitly.
if obj.constructor is BrowserWindow add: (processId, routingId, obj) ->
id = windowsWeakMap.add obj # Some native objects may already been added to objectsWeakMap, be care not
obj.on 'destroyed', -> # to add it twice.
windowsWeakMap.remove id @objectsWeakMap.add obj unless v8Util.getHiddenValue obj, 'atomId'
id = v8Util.getHiddenValue obj, 'atomId'
exports.add = (processId, routingId, obj) -> # Store and reference the object, then return the storeId which points to
# Some native objects may already been added to objectsWeakMap, be care not # where the object is stored. The caller can later dereference the object
# to add it twice. # with the storeId.
objectsWeakMap.add obj unless v8Util.getHiddenValue obj, 'atomId' # We use a difference key because the same object can be referenced for
id = v8Util.getHiddenValue obj, 'atomId' # multiple times by the same renderer view.
store = ObjectsStore.forRenderView processId, routingId
storeId = store.add obj
# Store and reference the object, then return the storeId which points to [id, storeId]
# where the object is stored. The caller can later dereference the object
# with the storeId.
store = ObjectsStore.forRenderView processId, routingId
storeId = store.add obj
[id, storeId] # Get an object according to its id.
get: (id) ->
@objectsWeakMap.get id
exports.get = (id) -> # Remove an object according to its storeId.
objectsWeakMap.get id remove: (processId, routingId, storeId) ->
ObjectsStore.forRenderView(processId, routingId).remove storeId
exports.getAllWindows = () -> # Clear all references to objects from renderer view.
keys = windowsWeakMap.keys() clear: (processId, routingId) ->
windowsWeakMap.get key for key in keys @emit "release-renderer-view-#{processId}-#{routingId}"
ObjectsStore.releaseForRenderView processId, routingId
exports.remove = (processId, routingId, storeId) -> # Return an array of all browser windows.
ObjectsStore.forRenderView(processId, routingId).remove storeId getAllWindows: ->
keys = @windowsWeakMap.keys()
@windowsWeakMap.get key for key in keys
exports.clear = (processId, routingId) -> module.exports = new ObjectsRegistry
ObjectsStore.releaseForRenderView processId, routingId

View file

@ -52,9 +52,15 @@ unwrapArgs = (processId, routingId, args) ->
returnValue = metaToValue meta.value returnValue = metaToValue meta.value
-> returnValue -> returnValue
when 'function' when 'function'
rendererReleased = false
objectsRegistry.once "release-renderer-view-#{processId}-#{routingId}", ->
rendererReleased = true
ret = -> ret = ->
throw new Error('Calling a callback of released renderer view') if rendererReleased
ipc.sendChannel processId, routingId, 'ATOM_RENDERER_CALLBACK', meta.id, valueToMeta(processId, routingId, arguments) ipc.sendChannel processId, routingId, 'ATOM_RENDERER_CALLBACK', meta.id, valueToMeta(processId, routingId, arguments)
v8Util.setDestructor ret, -> v8Util.setDestructor ret, ->
return if rendererReleased
ipc.sendChannel processId, routingId, 'ATOM_RENDERER_RELEASE_CALLBACK', meta.id ipc.sendChannel processId, routingId, 'ATOM_RENDERER_RELEASE_CALLBACK', meta.id
ret ret
else throw new TypeError("Unknown type: #{meta.type}") else throw new TypeError("Unknown type: #{meta.type}")

View file

@ -31,7 +31,6 @@ app.on('finish-launching', function() {
}); });
mainWindow.on('closed', function() { mainWindow.on('closed', function() {
console.log('closed');
mainWindow = null; mainWindow = null;
}); });
@ -39,129 +38,167 @@ app.on('finish-launching', function() {
console.log('unresponsive'); console.log('unresponsive');
}); });
var template = [ if (process.platform == 'darwin') {
{ var template = [
label: 'Atom Shell', {
submenu: [ label: 'Atom Shell',
{ submenu: [
label: 'About Atom Shell', {
selector: 'orderFrontStandardAboutPanel:' label: 'About Atom Shell',
}, selector: 'orderFrontStandardAboutPanel:'
{ },
type: 'separator' {
}, type: 'separator'
{ },
label: 'Hide Atom Shell', {
accelerator: 'Command+H', label: 'Hide Atom Shell',
selector: 'hide:' accelerator: 'Command+H',
}, selector: 'hide:'
{ },
label: 'Hide Others', {
accelerator: 'Command+Shift+H', label: 'Hide Others',
selector: 'hideOtherApplications:' accelerator: 'Command+Shift+H',
}, selector: 'hideOtherApplications:'
{ },
label: 'Show All', {
selector: 'unhideAllApplications:' label: 'Show All',
}, selector: 'unhideAllApplications:'
{ },
type: 'separator' {
}, type: 'separator'
{ },
label: 'Quit', {
accelerator: 'Command+Q', label: 'Quit',
click: function() { app.quit(); } accelerator: 'Command+Q',
}, click: function() { app.quit(); }
] },
}, ]
{ },
label: 'Edit', {
submenu: [ label: 'Edit',
{ submenu: [
label: 'Undo', {
accelerator: 'Command+Z', label: 'Undo',
selector: 'undo:' accelerator: 'Command+Z',
}, selector: 'undo:'
{ },
label: 'Redo', {
accelerator: 'Shift+Command+Z', label: 'Redo',
selector: 'redo:' accelerator: 'Shift+Command+Z',
}, selector: 'redo:'
{ },
type: 'separator' {
}, type: 'separator'
{ },
label: 'Cut', {
accelerator: 'Command+X', label: 'Cut',
selector: 'cut:' accelerator: 'Command+X',
}, selector: 'cut:'
{ },
label: 'Copy', {
accelerator: 'Command+C', label: 'Copy',
selector: 'copy:' accelerator: 'Command+C',
}, selector: 'copy:'
{ },
label: 'Paste', {
accelerator: 'Command+V', label: 'Paste',
selector: 'paste:' accelerator: 'Command+V',
}, selector: 'paste:'
{ },
label: 'Select All', {
accelerator: 'Command+A', label: 'Select All',
selector: 'selectAll:' accelerator: 'Command+A',
}, selector: 'selectAll:'
] },
}, ]
{ },
label: 'View', {
submenu: [ label: 'View',
{ submenu: [
label: 'Reload', {
accelerator: 'Command+R', label: 'Reload',
click: function() { BrowserWindow.getFocusedWindow().restart(); } accelerator: 'Command+R',
}, click: function() { BrowserWindow.getFocusedWindow().restart(); }
{ },
label: 'Enter Fullscreen', {
click: function() { BrowserWindow.getFocusedWindow().setFullscreen(true); } label: 'Enter Fullscreen',
}, click: function() { BrowserWindow.getFocusedWindow().setFullscreen(true); }
{ },
label: 'Toggle DevTools', {
accelerator: 'Alt+Command+I', label: 'Toggle DevTools',
click: function() { BrowserWindow.getFocusedWindow().toggleDevTools(); } accelerator: 'Alt+Command+I',
}, click: function() { BrowserWindow.getFocusedWindow().toggleDevTools(); }
] },
}, ]
{ },
label: 'Window', {
submenu: [ label: 'Window',
{ submenu: [
label: 'Minimize', {
accelerator: 'Command+M', label: 'Minimize',
selector: 'performMiniaturize:' accelerator: 'Command+M',
}, selector: 'performMiniaturize:'
{ },
label: 'Close', {
accelerator: 'Command+W', label: 'Close',
selector: 'performClose:' accelerator: 'Command+W',
}, selector: 'performClose:'
{ },
type: 'separator' {
}, type: 'separator'
{ },
label: 'Bring All to Front', {
selector: 'arrangeInFront:' label: 'Bring All to Front',
}, selector: 'arrangeInFront:'
] },
}, ]
]; },
];
menu = Menu.buildFromTemplate(template); menu = Menu.buildFromTemplate(template);
if (process.platform == 'darwin')
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
} else {
var template = [
{
label: 'File',
submenu: [
{
label: 'Open',
accelerator: 'Command+O',
},
{
label: 'Close',
accelerator: 'Command+W',
click: function() { mainWindow.close(); }
},
]
},
{
label: 'View',
submenu: [
{
label: 'Reload',
accelerator: 'Command+R',
click: function() { mainWindow.restart(); }
},
{
label: 'Enter Fullscreen',
click: function() { mainWindow.setFullscreen(true); }
},
{
label: 'Toggle DevTools',
accelerator: 'Alt+Command+I',
click: function() { mainWindow.toggleDevTools(); }
},
]
},
];
menu = Menu.buildFromTemplate(template);
mainWindow.setMenu(menu);
}
ipc.on('message', function(processId, routingId, type) { ipc.on('message', function(processId, routingId, type) {
console.log(type);
if (type == 'menu') if (type == 'menu')
menu.popup(mainWindow); menu.popup(mainWindow);
}); });

View file

@ -4,8 +4,12 @@
#include "browser/native_window_win.h" #include "browser/native_window_win.h"
#include "base/stl_util.h"
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
#include "base/values.h" #include "base/values.h"
#include "browser/api/atom_api_menu.h"
#include "browser/ui/win/menu_2.h"
#include "browser/ui/win/native_menu_win.h"
#include "common/draggable_region.h" #include "common/draggable_region.h"
#include "common/options_switches.h" #include "common/options_switches.h"
#include "content/public/browser/native_web_keyboard_event.h" #include "content/public/browser/native_web_keyboard_event.h"
@ -14,8 +18,10 @@
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_view.h" #include "content/public/browser/web_contents_view.h"
#include "ui/gfx/path.h" #include "ui/gfx/path.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/views/controls/webview/webview.h" #include "ui/views/controls/webview/webview.h"
#include "ui/views/widget/widget.h" #include "ui/views/widget/widget.h"
#include "ui/views/widget/native_widget_win.h"
#include "ui/views/window/client_view.h" #include "ui/views/window/client_view.h"
#include "ui/views/window/native_frame_view.h" #include "ui/views/window/native_frame_view.h"
@ -26,24 +32,49 @@ namespace {
const int kResizeInsideBoundsSize = 5; const int kResizeInsideBoundsSize = 5;
const int kResizeAreaCornerSize = 16; const int kResizeAreaCornerSize = 16;
// Wrapper of NativeWidgetWin to handle WM_MENUCOMMAND messages, which are
// triggered by window menus.
class MenuCommandNativeWidget : public views::NativeWidgetWin {
public:
explicit MenuCommandNativeWidget(NativeWindowWin* delegate)
: views::NativeWidgetWin(delegate->window()),
delegate_(delegate) {}
virtual ~MenuCommandNativeWidget() {}
protected:
virtual bool PreHandleMSG(UINT message,
WPARAM w_param,
LPARAM l_param,
LRESULT* result) OVERRIDE {
if (message == WM_MENUCOMMAND) {
delegate_->OnMenuCommand(w_param, reinterpret_cast<HMENU>(l_param));
*result = 0;
return true;
} else {
return false;
}
}
private:
NativeWindowWin* delegate_;
DISALLOW_COPY_AND_ASSIGN(MenuCommandNativeWidget);
};
class NativeWindowClientView : public views::ClientView { class NativeWindowClientView : public views::ClientView {
public: public:
NativeWindowClientView(views::Widget* widget, NativeWindowClientView(views::Widget* widget,
views::View* contents_view, NativeWindowWin* contents_view)
NativeWindowWin* shell) : views::ClientView(widget, contents_view) {
: views::ClientView(widget, contents_view),
shell_(shell) {
} }
virtual ~NativeWindowClientView() {} virtual ~NativeWindowClientView() {}
virtual bool CanClose() OVERRIDE { virtual bool CanClose() OVERRIDE {
shell_->CloseWebContents(); static_cast<NativeWindowWin*>(contents_view())->CloseWebContents();
return false; return false;
} }
private: private:
NativeWindowWin* shell_;
DISALLOW_COPY_AND_ASSIGN(NativeWindowClientView); DISALLOW_COPY_AND_ASSIGN(NativeWindowClientView);
}; };
@ -173,6 +204,7 @@ NativeWindowWin::NativeWindowWin(content::WebContents* web_contents,
resizable_(true) { resizable_(true) {
views::Widget::InitParams params(views::Widget::InitParams::TYPE_WINDOW); views::Widget::InitParams params(views::Widget::InitParams::TYPE_WINDOW);
params.delegate = this; params.delegate = this;
params.native_widget = new MenuCommandNativeWidget(this);
params.remove_standard_frame = !has_frame_; params.remove_standard_frame = !has_frame_;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
window_->set_frame_type(views::Widget::FRAME_TYPE_FORCE_NATIVE); window_->set_frame_type(views::Widget::FRAME_TYPE_FORCE_NATIVE);
@ -329,6 +361,17 @@ gfx::NativeWindow NativeWindowWin::GetNativeWindow() {
return window_->GetNativeView(); return window_->GetNativeView();
} }
void NativeWindowWin::OnMenuCommand(int position, HMENU menu) {
DCHECK(menu_);
menu_->wrapper()->OnMenuCommand(position, menu);
}
void NativeWindowWin::SetMenu(ui::MenuModel* menu_model) {
menu_.reset(new atom::Menu2(menu_model, true));
::SetMenu(GetNativeWindow(), menu_->GetNativeMenu());
RegisterAccelerators();
}
void NativeWindowWin::UpdateDraggableRegions( void NativeWindowWin::UpdateDraggableRegions(
const std::vector<DraggableRegion>& regions) { const std::vector<DraggableRegion>& regions) {
if (has_frame_) if (has_frame_)
@ -356,12 +399,54 @@ void NativeWindowWin::UpdateDraggableRegions(
void NativeWindowWin::HandleKeyboardEvent( void NativeWindowWin::HandleKeyboardEvent(
content::WebContents*, content::WebContents*,
const content::NativeWebKeyboardEvent& event) { const content::NativeWebKeyboardEvent& event) {
if (event.type == WebKit::WebInputEvent::KeyUp) {
ui::Accelerator accelerator(
static_cast<ui::KeyboardCode>(event.windowsKeyCode),
content::GetModifiersFromNativeWebKeyboardEvent(event));
if (GetFocusManager()->ProcessAccelerator(accelerator)) {
return;
}
}
// Any unhandled keyboard/character messages should be defproced. // Any unhandled keyboard/character messages should be defproced.
// This allows stuff like F10, etc to work correctly. // This allows stuff like F10, etc to work correctly.
DefWindowProc(event.os_event.hwnd, event.os_event.message, DefWindowProc(event.os_event.hwnd, event.os_event.message,
event.os_event.wParam, event.os_event.lParam); event.os_event.wParam, event.os_event.lParam);
} }
void NativeWindowWin::Layout() {
DCHECK(web_view_);
web_view_->SetBounds(0, 0, width(), height());
OnViewWasResized();
}
void NativeWindowWin::ViewHierarchyChanged(bool is_add,
views::View* parent,
views::View* child) {
if (is_add && child == this)
AddChildView(web_view_);
}
bool NativeWindowWin::AcceleratorPressed(
const ui::Accelerator& accelerator) {
if (ContainsKey(accelerator_table_, accelerator)) {
const MenuItem& item = accelerator_table_[accelerator];
item.model->ActivatedAt(item.position);
return true;
} else {
return false;
}
}
void NativeWindowWin::DeleteDelegate() {
// Do nothing, window is managed by users.
}
views::View* NativeWindowWin::GetInitiallyFocusedView() {
return web_view_;
}
bool NativeWindowWin::CanResize() const { bool NativeWindowWin::CanResize() const {
return resizable_; return resizable_;
} }
@ -391,7 +476,7 @@ const views::Widget* NativeWindowWin::GetWidget() const {
} }
views::ClientView* NativeWindowWin::CreateClientView(views::Widget* widget) { views::ClientView* NativeWindowWin::CreateClientView(views::Widget* widget) {
return new NativeWindowClientView(widget, web_view_, this); return new NativeWindowClientView(widget, this);
} }
views::NonClientFrameView* NativeWindowWin::CreateNonClientFrameView( views::NonClientFrameView* NativeWindowWin::CreateNonClientFrameView(
@ -432,6 +517,44 @@ void NativeWindowWin::OnViewWasResized() {
web_contents->GetRenderViewHost()->GetView()->SetClickthroughRegion(rgn); web_contents->GetRenderViewHost()->GetView()->SetClickthroughRegion(rgn);
} }
void NativeWindowWin::RegisterAccelerators() {
views::FocusManager* focus_manager = GetFocusManager();
accelerator_table_.clear();
focus_manager->UnregisterAccelerators(this);
GenerateAcceleratorTable();
for (AcceleratorTable::const_iterator iter = accelerator_table_.begin();
iter != accelerator_table_.end(); ++iter) {
focus_manager->RegisterAccelerator(
iter->first, ui::AcceleratorManager::kNormalPriority, this);
}
}
void NativeWindowWin::GenerateAcceleratorTable() {
DCHECK(menu_);
ui::SimpleMenuModel* model = static_cast<ui::SimpleMenuModel*>(
menu_->model());
FillAcceleratorTable(&accelerator_table_, model);
}
void NativeWindowWin::FillAcceleratorTable(AcceleratorTable* table,
ui::MenuModel* model) {
int count = model->GetItemCount();
for (int i = 0; i < count; ++i) {
ui::MenuModel::ItemType type = model->GetTypeAt(i);
if (type == ui::MenuModel::TYPE_SUBMENU) {
ui::MenuModel* submodel = model->GetSubmenuModelAt(i);
FillAcceleratorTable(table, submodel);
} else {
ui::Accelerator accelerator;
if (model->GetAcceleratorAt(i, &accelerator)) {
MenuItem item = { i, model };
(*table)[accelerator] = item;
}
}
}
}
// static // static
NativeWindow* NativeWindow::Create(content::WebContents* web_contents, NativeWindow* NativeWindow::Create(content::WebContents* web_contents,
base::DictionaryValue* options) { base::DictionaryValue* options) {

View file

@ -12,6 +12,10 @@
#include "ui/gfx/size.h" #include "ui/gfx/size.h"
#include "ui/views/widget/widget_delegate.h" #include "ui/views/widget/widget_delegate.h"
namespace ui {
class MenuModel;
}
namespace views { namespace views {
class WebView; class WebView;
class Widget; class Widget;
@ -19,8 +23,10 @@ class Widget;
namespace atom { namespace atom {
class Menu2;
class NativeWindowWin : public NativeWindow, class NativeWindowWin : public NativeWindow,
public views::WidgetDelegate { public views::WidgetDelegateView {
public: public:
explicit NativeWindowWin(content::WebContents* web_contents, explicit NativeWindowWin(content::WebContents* web_contents,
base::DictionaryValue* options); base::DictionaryValue* options);
@ -61,6 +67,12 @@ class NativeWindowWin : public NativeWindow,
virtual bool IsKiosk() OVERRIDE; virtual bool IsKiosk() OVERRIDE;
virtual gfx::NativeWindow GetNativeWindow() OVERRIDE; virtual gfx::NativeWindow GetNativeWindow() OVERRIDE;
void OnMenuCommand(int position, HMENU menu);
// Set the native window menu.
void SetMenu(ui::MenuModel* menu_model);
views::Widget* window() const { return window_.get(); }
SkRegion* draggable_region() { return draggable_region_.get(); } SkRegion* draggable_region() { return draggable_region_.get(); }
protected: protected:
@ -72,7 +84,16 @@ class NativeWindowWin : public NativeWindow,
content::WebContents*, content::WebContents*,
const content::NativeWebKeyboardEvent&) OVERRIDE; const content::NativeWebKeyboardEvent&) OVERRIDE;
// Overridden from views::View:
virtual void Layout() OVERRIDE;
virtual void ViewHierarchyChanged(bool is_add,
views::View* parent,
views::View* child) OVERRIDE;
virtual bool AcceleratorPressed(const ui::Accelerator& accelerator) OVERRIDE;
// Overridden from views::WidgetDelegate: // Overridden from views::WidgetDelegate:
virtual void DeleteDelegate() OVERRIDE;
virtual views::View* GetInitiallyFocusedView() OVERRIDE;
virtual bool CanResize() const OVERRIDE; virtual bool CanResize() const OVERRIDE;
virtual bool CanMaximize() const OVERRIDE; virtual bool CanMaximize() const OVERRIDE;
virtual string16 GetWindowTitle() const OVERRIDE; virtual string16 GetWindowTitle() const OVERRIDE;
@ -85,11 +106,30 @@ class NativeWindowWin : public NativeWindow,
views::Widget* widget) OVERRIDE; views::Widget* widget) OVERRIDE;
private: private:
typedef struct { int position; ui::MenuModel* model; } MenuItem;
typedef std::map<ui::Accelerator, MenuItem> AcceleratorTable;
void OnViewWasResized(); void OnViewWasResized();
// Register accelerators supported by the menu model.
void RegisterAccelerators();
// Generate a table that contains memu model's accelerators and command ids.
void GenerateAcceleratorTable();
// Helper to fill the accelerator table from the model.
void FillAcceleratorTable(AcceleratorTable* table,
ui::MenuModel* model);
scoped_ptr<views::Widget> window_; scoped_ptr<views::Widget> window_;
views::WebView* web_view_; // managed by window_. views::WebView* web_view_; // managed by window_.
// The window menu.
scoped_ptr<atom::Menu2> menu_;
// Map from accelerator to menu item's command id.
AcceleratorTable accelerator_table_;
scoped_ptr<SkRegion> draggable_region_; scoped_ptr<SkRegion> draggable_region_;
bool resizable_; bool resizable_;

View file

@ -15,10 +15,9 @@ namespace accelerator_util {
namespace { namespace {
// For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other // Convert "Command" to "Ctrl" on non-Mac
// platforms leave the shortcut untouched.
std::string NormalizeShortcutSuggestion(const std::string& suggestion) { std::string NormalizeShortcutSuggestion(const std::string& suggestion) {
#if !defined(OS_MACOSX) #if defined(OS_MACOSX)
return suggestion; return suggestion;
#endif #endif
@ -26,9 +25,7 @@ std::string NormalizeShortcutSuggestion(const std::string& suggestion) {
std::vector<std::string> tokens; std::vector<std::string> tokens;
base::SplitString(suggestion, '+', &tokens); base::SplitString(suggestion, '+', &tokens);
for (size_t i = 0; i < tokens.size(); i++) { for (size_t i = 0; i < tokens.size(); i++) {
if (tokens[i] == "Ctrl") if (tokens[i] == "Command")
tokens[i] = "Command";
else if (tokens[i] == "MacCtrl")
tokens[i] = "Ctrl"; tokens[i] = "Ctrl";
} }
return JoinString(tokens, '+'); return JoinString(tokens, '+');

View file

@ -77,6 +77,7 @@ class Menu2 {
// Accessors. // Accessors.
ui::MenuModel* model() const { return model_; } ui::MenuModel* model() const { return model_; }
NativeMenuWin* wrapper() const { return wrapper_.get(); }
// Sets the minimum width of the menu. // Sets the minimum width of the menu.
void SetMinimumWidth(int width); void SetMinimumWidth(int width);

View file

@ -92,6 +92,20 @@ class NativeMenuWin::MenuHostWindow {
DestroyWindow(hwnd_); DestroyWindow(hwnd_);
} }
// Called when the user selects a specific item.
void OnMenuCommand(int position, HMENU menu) {
NativeMenuWin* menu_win = GetNativeMenuWinFromHMENU(menu);
ui::MenuModel* model = menu_win->model_;
NativeMenuWin* root_menu = menu_win;
while (root_menu->parent_)
root_menu = root_menu->parent_;
// Only notify the model if it didn't already send out notification.
// See comment in MenuMessageHook for details.
if (root_menu->menu_action_ == MENU_ACTION_NONE)
model->ActivatedAt(position);
}
HWND hwnd() const { return hwnd_; } HWND hwnd() const { return hwnd_; }
private: private:
@ -146,20 +160,6 @@ class NativeMenuWin::MenuHostWindow {
return reinterpret_cast<NativeMenuWin::ItemData*>(item_data); return reinterpret_cast<NativeMenuWin::ItemData*>(item_data);
} }
// Called when the user selects a specific item.
void OnMenuCommand(int position, HMENU menu) {
NativeMenuWin* menu_win = GetNativeMenuWinFromHMENU(menu);
ui::MenuModel* model = menu_win->model_;
NativeMenuWin* root_menu = menu_win;
while (root_menu->parent_)
root_menu = root_menu->parent_;
// Only notify the model if it didn't already send out notification.
// See comment in MenuMessageHook for details.
if (root_menu->menu_action_ == MENU_ACTION_NONE)
model->ActivatedAt(position);
}
// Called as the user moves their mouse or arrows through the contents of the // Called as the user moves their mouse or arrows through the contents of the
// menu. // menu.
void OnMenuSelect(WPARAM w_param, HMENU menu) { void OnMenuSelect(WPARAM w_param, HMENU menu) {
@ -529,6 +529,10 @@ void NativeMenuWin::SetMinimumWidth(int width) {
NOTIMPLEMENTED(); NOTIMPLEMENTED();
} }
void NativeMenuWin::OnMenuCommand(int position, HMENU menu) {
host_window_->OnMenuCommand(position, menu);
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// NativeMenuWin, private: // NativeMenuWin, private:

View file

@ -56,6 +56,9 @@ class NativeMenuWin {
void RemoveMenuListener(views::MenuListener* listener); void RemoveMenuListener(views::MenuListener* listener);
void SetMinimumWidth(int width); void SetMinimumWidth(int width);
// Called by user to generate a menu command event.
void OnMenuCommand(int position, HMENU menu);
// Flag to create a window menu instead of popup menu. // Flag to create a window menu instead of popup menu.
void set_create_as_window_menu(bool flag) { create_as_window_menu_ = flag; } void set_create_as_window_menu(bool flag) { create_as_window_menu_ = flag; }
bool create_as_window_menu() const { return create_as_window_menu_; } bool create_as_window_menu() const { return create_as_window_menu_; }

View file

@ -79,12 +79,16 @@ code will not run.
Returns the version of current bundle or executable. Returns the version of current bundle or executable.
## app.getBrowserWindows()
Returns an array of all browser windows.
## app.commandLine.appendSwitch(switch, [value]) ## app.commandLine.appendSwitch(switch, [value])
Append a switch [with optional value] to Chromium's command line. Append a switch [with optional value] to Chromium's command line.
**Note:** This will not affect `process.argv`, and is mainly used by **Note:** This will not affect `process.argv`, and is mainly used by developers
**developers to control some low-level Chromium behaviors. to control some low-level Chromium behaviors.
## app.commandLine.appendArgument(value) ## app.commandLine.appendArgument(value)

View file

@ -23,5 +23,5 @@
## Notes on accelerator ## Notes on accelerator
On OS X, the `Ctrl` would automatically translated to `Command`, if you really On Linux and Windows, the `Command` would be translated to `Ctrl`, so usually
want `Ctrl` on OS X, you should use `MacCtrl`. you can use `Command` for most of the commands.

View file

@ -1,6 +1,7 @@
var app = require('app'); var app = require('app');
var ipc = require('ipc'); var ipc = require('ipc');
var BrowserWindow = require('browser-window'); var BrowserWindow = require('browser-window');
var Menu = require('menu');
var window = null; var window = null;
@ -39,6 +40,45 @@ app.on('window-all-closed', function() {
}); });
app.on('finish-launching', function() { app.on('finish-launching', function() {
var template = [
{
label: 'File',
submenu: [
{
label: 'Open',
accelerator: 'Command+O',
},
{
label: 'Close',
accelerator: 'Command+W',
click: function(item, window) { window.close(); }
},
]
},
{
label: 'View',
submenu: [
{
label: 'Reload',
accelerator: 'Command+R',
click: function(item, window) { window.restart(); }
},
{
label: 'Enter Fullscreen',
click: function(item, window) { window.setFullScreen(true); }
},
{
label: 'Toggle DevTools',
accelerator: 'Alt+Command+I',
click: function(item, window) { window.toggleDevTools(); }
},
]
},
];
var menu = Menu.buildFromTemplate(template);
app.setApplicationMenu(menu);
// Test if using protocol module would crash. // Test if using protocol module would crash.
require('protocol').registerProtocol('test-if-crashes', function() {}); require('protocol').registerProtocol('test-if-crashes', function() {});

View file

@ -1,7 +1,4 @@
describe 'http', -> describe 'http', ->
describe 'sending request of http protocol urls', -> describe 'sending request of http protocol urls', ->
it 'should not crash', (done) -> it 'should not crash', ->
$.ajax $.get 'https://api.github.com/zen'
url: 'http://127.0.0.1'
success: -> done()
error: -> done()