diff options
author | oshima@chromium.org <oshima@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-10-04 23:59:23 +0000 |
---|---|---|
committer | oshima@chromium.org <oshima@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-10-04 23:59:23 +0000 |
commit | 3a051a4b8a96fb3edd00ebddcd5eb48e8ecf4396 (patch) | |
tree | 7728a74b027dced0037b18f142329c10954bb7a0 /chrome/browser/resources | |
parent | 23714db12751e6ba10a33a5110359af139d5566c (diff) | |
download | chromium_src-3a051a4b8a96fb3edd00ebddcd5eb48e8ecf4396.zip chromium_src-3a051a4b8a96fb3edd00ebddcd5eb48e8ecf4396.tar.gz chromium_src-3a051a4b8a96fb3edd00ebddcd5eb48e8ecf4396.tar.bz2 |
DOMUI implementation of Menu2.
A few key design points.
* Uses DOMView to render menu content.
* MenuUI (subclass of DOMUI) uses GtkWiget tree to access
WidgetGtk and MenuModel. Alternative way was to change
TabContents::GetDOMUIForCurrentState() public and pass them throught it, but this seems to work fine.
* Input/Focus is controlled by gtk_grab_add. Input is grabbed by the root widget, and then sent to each active menu widget via gtk_grab_add.
* Menu gets shown after the renderer rendered the content
(thus has some size) to avoid showing small/empty menu.
Mouse processing is blocked until the menu is shown (see Dispatcher).
A few issues that will be addressed in separate CL
* RTL (will be after beta)
* menu scroll. working on it now.
* menu sometimes show scroll bar. (I just need to disable it)
* unit test for native_menu_domui
BUG=chromiun-os:6497
TEST=manual for now. all menu2 based meus should work as before (web pages's context menu, system menu, forward/back menu and several chromeos specific one) except for following two:
* mnemonic should work now on context menus.
* menu larger than screen does not scroll. I'll implement this in separate CL
Review URL: http://codereview.chromium.org/3442018
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@61445 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/resources')
-rw-r--r-- | chrome/browser/resources/menu.css | 63 | ||||
-rw-r--r-- | chrome/browser/resources/menu.html | 16 | ||||
-rw-r--r-- | chrome/browser/resources/menu.js | 474 |
3 files changed, 553 insertions, 0 deletions
diff --git a/chrome/browser/resources/menu.css b/chrome/browser/resources/menu.css new file mode 100644 index 0000000..44e584f --- /dev/null +++ b/chrome/browser/resources/menu.css @@ -0,0 +1,63 @@ +body { + margin: 0px; + background: -webkit-gradient(linear, left top, left bottom, + from(white), + to(#EEE)); + margin: 0px 0px 0px 0px; +} + +.menu_item { + margin-left: 0px; + margin-right: 0px; + margin-top: 0px; + white-space: nowrap; + padding: 0px 20px 0px 5px; +} + +.disabled { + color: #b7b7b7; +} + +.noicon { + padding-left: 20px; +} + +.menu_label { + display: inline-block; + vertical-align: middle; +} + +.left_icon { + vertical-align: middle; + margin-right: 10px; +} + +.right_icon { + vertical-align: middle; + right: 10px; +} + +#menu { + width: 100%; + height: 100%; +} + +.separator { + width: 85%; + height: 1px; + margin-top: 3px; + margin-bottom: 3px; + background: -webkit-gradient(linear, left top, right top, + from(transparent), + color-stop(0.3, rgba(20, 20, 20, 0.5)), + color-stop(0.7, rgba(20, 20, 20, 0.5)), + to(transparent)); +} + +.mnemonic_enabled .mnemonic { + text-decoration: underline; +} + +.selected { + background: #DCE4FA; +} diff --git a/chrome/browser/resources/menu.html b/chrome/browser/resources/menu.html new file mode 100644 index 0000000..4cea45c --- /dev/null +++ b/chrome/browser/resources/menu.html @@ -0,0 +1,16 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"/> + <link rel="stylesheet" href="menu.css"/> + <script src="shared/js/class_list.js"></script> + <script src="menu.js"></script> + </head> + <body> + <div id="menu"> </div> + <script> + setMenu(new Menu()); + </script> + </body> +</html> + diff --git a/chrome/browser/resources/menu.js b/chrome/browser/resources/menu.js new file mode 100644 index 0000000..f843e656 --- /dev/null +++ b/chrome/browser/resources/menu.js @@ -0,0 +1,474 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// How long to wait to open submenu when mouse hovers. +var SUBMENU_OPEN_DELAY_MS = 200; +// How long to wait to close submenu when mouse left. +var SUBMENU_CLOSE_DELAY_MS = 500; +// Regular expression to match/find mnemonic key. +var MNEMONIC_REGEXP = /&(.)/; + +/** + * Sends 'click' DOMUI message. + */ +function sendClick(id) { + chrome.send('click', [ id + '' ]); +} + +/** + * MenuItem class. + */ +function MenuItem(menu, id, attrs) { + this.menu_ = menu; + this.id = id; + this.attrs = attrs; +} + +MenuItem.prototype = { + /** + * Initialize the MenuItem. + * @param {boolean} has_icon True if the menu has left icon. + * @public + */ + init: function(has_icon) { + this.div = document.createElement('div'); + var attrs = this.attrs; + if (attrs.type == 'separator') { + this.div.className = 'separator'; + } else if (attrs.type == 'command' || + attrs.type == 'submenu' || + attrs.type == 'check' || + attrs.type == 'radio') { + this.initMenuItem(has_icon); + } else { + this.div.className = 'menu_item disabled'; + this.div.innerHTML = 'unknown'; + } + this.div.classList.add(has_icon ? 'has_icon' : 'noicon'); + }, + + /** + * Select this item. + * @public + */ + select: function() { + this.div.classList.add('selected'); + this.menu_.setSelection(this); + }, + + /** + * Unselect this item. + * @public + */ + unselect: function() { + this.div.classList.remove('selected'); + }, + + /** + * Activat the menu item. + * @public + */ + activate: function() { + if (this.attrs.type == 'submenu') { + this.menu_.openSubmenu(this); + } else if (this.attrs.type != 'separator') { + sendClick(this.id); + } + }, + + /** + * Sends open_submenu DOMUI message. + * @public + */ + sendOpenSubmenuCommand: function() { + chrome.send('open_submenu', [ this.id + '', this.getYCoord_() + '']); + }, + + /** + * Returns y coordinate of this menu item + * in the menu window. + * @private + */ + getYCoord_: function() { + var element = this.div; + var y = 0; + while (element != null) { + y += element.offsetTop; + element = element.offsetParent; + } + return y; + }, + + /** + * Internal method to initiailze the MenuItem's div element. + * @private + */ + initMenuItem: function(has_icon) { + var attrs = this.attrs; + this.div.className = 'menu_item ' + attrs.type; + this.menu_.addHandlers(this); + var mnemonic = MNEMONIC_REGEXP.exec(attrs.label); + if (mnemonic) { + var c = mnemonic[1]; + this.menu_.registerMnemonicKey(c, this); + } + var text = attrs.label.replace( + MNEMONIC_REGEXP, '<span class="mnemonic">$1</span>'); + if (has_icon) { + var icon = document.createElement('image'); + icon.className = 'left_icon'; + + if (attrs.type == 'radio') { + icon.src = attrs.checked ? + menu_.config_.radioOnUrl : menu_.config_.radioOffUrl; + } else if (attrs.icon) { + icon.src = attrs.icon; + } else if (attrs.type == 'check' && attrs.checked) { + icon.src = menu_.config_.checkUrl; + } else { + icon.style.width = '12px'; + } + this.div.appendChild(icon); + } + var label = document.createElement('div'); + label.className = 'menu_label'; + label.innerHTML = text; + + if (attrs.font) { + label.style.font = attrs.font; + } + this.div.appendChild(label); + + if (attrs.type == 'submenu') { + var icon = document.createElement('image'); + icon.src = menu_.config_.arrowUrl; + icon.className = 'right_icon'; + this.div.appendChild(icon); + } + }, +}; + +/** + * Menu class. + */ +function Menu() { + /* configuration object */ + this.config_ = null; + /* currently selected menu item */ + this.current_ = null; + /* the id of last element */ + this.last_id_ = -1; + /* timers for opening/closing submenu */ + this.open_submenu_timer_ = 0; + this.close_submenu_timer_ = 0; + /* pointer to a submenu currently shown, if any */ + this.submenu_shown_ = null; + /* list of menu items */ + this.items_ = []; + /* map from mnemonic character to item to activate */ + this.mnemonics_ = {}; + /* true if this menu is root */ + this.is_root_ = false; +} + +Menu.prototype = { + /** + * Initialize the menu. + * @public + */ + init: function(config) { + this.config_ = config; + document.getElementById('menu').onmouseout = this.onMouseout_.bind(this); + document.addEventListener('keydown', this.onKeydown_.bind(this)); + /* disable text select */ + document.onselectstart = function() { return false; } + }, + + /** + * A template method to create MenuItem object. + * Subclass class can override to return custom menu item. + * @public + */ + createMenuItem: function(id, attrs) { + return new MenuItem(this, id, attrs); + }, + + /** + * Update and display the new model. + * @public + */ + updateModel: function(model) { + this.is_root = model.is_root; + this.current_ = null; + this.items_ = []; + this.mnemonics_ = {}; + + var menu = document.getElementById('menu'); + menu.innerHTML = ''; // remove menu items + + var id = 0; + + for (i in model.items) { + var attrs = model.items[i]; + var item = this.createMenuItem(id++, attrs); + this.items_[item.id] = item; + this.last_id_ = item.id; + if (!item.attrs.visible) { + continue; + } + + item.init(model.has_icon); + menu.appendChild(item.div); + } + }, + + /** + * Highlights the currently selected item, or + * select the 1st selectable item if none is selected. + * @public + */ + showSelection: function() { + if (this.current_) { + this.current_.select(); + } else { + this.findNextEnabled_(1).select(); + } + }, + + /** + * Registers mnemonic key. + * @param {c} a mnemonic key to activate item. + * @param {item} an item to be activated when {c} is pressed. + * @public + */ + registerMnemonicKey: function(c, item) { + this.mnemonics_[c.toUpperCase()] = item; + this.mnemonics_[c.toLowerCase()] = item; + }, + + /** + * Add event handlers for the item. + * @public + */ + addHandlers: function(item) { + var menu = this; + item.div.addEventListener('mouseover', function(event) { + menu.onMouseover_(event, item); + }); + if (item.attrs.enabled) { + item.div.addEventListener('mouseup', function(event) { + menu.onClick_(event, item); + }); + } else { + item.div.classList.add('disabled'); + } + }, + + /** + * Set the selected item. This also start or cancel + * @public + */ + setSelection: function(item) { + if (this.current_ == item) + return; + + if (this.current_ != null) + this.current_.unselect(); + + this.current_ = item; + + var menu = this; + if (item.attrs.type == 'submenu') { + if (this.submenu_shown_ != item) { + this.open_submenu_timer_ = + setTimeout(function() { menu.openSubmenu(item); }, + SUBMENU_OPEN_DELAY_MS); + } else { + this.cancelSubmenuTimer_(); + } + } else if (this.submenu_shown_) { + this.cancelSubmenuTimer_(); + this.close_submenu_timer_ = + setTimeout(function() { menu.closeSubmenu_(item); }, + SUBMENU_CLOSE_DELAY_MS); + } + }, + + /** + * Open submenu {item}. It does nothing if the submenu is + * already opened. + * @param {item} the submenu item to open. + * @public + */ + openSubmenu: function(item) { + this.cancelSubmenuTimer_(); + if (this.submenu_shown_ != item) { + this.submenu_shown_ = item; + item.sendOpenSubmenuCommand(); + } + }, + + /** + * Handle keyboard navigatio and mnemonic keys. + * @private + */ + onKeydown_: function(event) { + switch (event.keyCode) { + case 27: /* escape */ + sendClick(-1); // -1 closes the menu. + break; + case 37: /* left */ + this.moveToParent_(); + break; + case 39: /* right */ + this.moveToSubmenu_(); + break; + case 38: /* up */ + document.getElementById('menu').className = 'mnemonic_enabled'; + this.findNextEnabled_(-1).select(); + break; + case 40: /* down */ + document.getElementById('menu').className = 'mnemonic_enabled'; + this.findNextEnabled_(1).select(); + break; + case 9: /* tab */ + // TBD. + break; + case 13: /* return */ + case 32: /* space */ + if (this.current_) { + this.current_.activate(); + } + break; + default: + // Handles mnemonic. + var c = String.fromCharCode(event.keyCode); + var item = this.mnemonics_[c]; + if (item) item.activate(); + } + }, + + // Mouse Event handlers + onClick_: function(event, item) { + item.activate(); + }, + + onMouseover_: function(event, item) { + this.cancelSubmenuTimer_(); + if (this.current_ != item && item.attrs.enabled) { + item.select(); + } + }, + + onMouseout_: function(event) { + if (this.current_) { + this.current_.unselect(); + this.current_ = null; + } + }, + + /** + * Closes the submenu. + * a submenu. + * @private + */ + closeSubmenu_: function(item) { + this.submenu_shown_ = null; + this.cancelSubmenuTimer_(); + chrome.send('close_submenu', []); + }, + + /** + * Move the selection to parent menu if the current menu is + * a submenu. + * @private + */ + moveToParent_: function() { + if (!this.is_root) { + if (this.current_) { + this.current_.unselect(); + this.current_ = null; + } + chrome.send('move_to_parent', []); + } + }, + + /** + * Move the selection to submenu if the currently selected + * menu is a submenu. + * @private + */ + moveToSubmenu_: function () { + var current = this.current_; + if(current && current.attrs.type == 'submenu') { + this.openSubmenu(current); + chrome.send('move_to_submenu', []); + } + }, + + /** + * Find a next selectable item. If nothing is selected, the 1st + * selectable item will be chosen. Returns null if nothing is + * selectable. + * @param {number} incr specifies the direction to search, 1 to + * downwards and -1 for upwards. + * @private + */ + findNextEnabled_: function(incr) { + if (this.current_) { + var id = parseInt(this.current_.id); + } else { + var id = incr > 0 ? -1 : this.last_id_ + 1; + } + for (var i = 0; i <= this.last_id_; i++) { + if (id == 0 && incr < 0) { + id = this.last_id_; + } else if (id == this.last_id_ && incr > 0) { + id = 0; + } else { + id += incr; + } + var item = this.items_[id]; + if (item.attrs.enabled && item.attrs.type != 'separator') + return item; + } + return null; + }, + + /** + * Cancels timers to open/close submenus. + * @private + */ + cancelSubmenuTimer_: function() { + if (this.open_submenu_timer_) { + clearTimeout(this.open_submenu_timer_); + this.open_submenu_timer_ = 0; + } + if (this.close_submenu_timer_) { + clearTimeout(this.close_submenu_timer_); + this.close_submenu_timer_ = 0; + } + }, +}; + +/** + * functions to be called from C++. + */ +function init(config) { + menu_.init(config); +} + +function selectItem() { + menu_.showSelection(); +} + +function updateModel(model) { + menu_.updateModel(model); +} + +var menu_; + +function setMenu(menu) { + menu_ = menu; +} |