diff options
author | oshima@chromium.org <oshima@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-10-07 17:28:57 +0000 |
---|---|---|
committer | oshima@chromium.org <oshima@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-10-07 17:28:57 +0000 |
commit | 2fb382fd01716825e026c7833b6d170b5f412a08 (patch) | |
tree | 6d2d210583518861224a1ce40c4050e10b2675d1 /chrome/browser | |
parent | 632363c0049b3c1d54b12f5b8e2a2b16de5a97b3 (diff) | |
download | chromium_src-2fb382fd01716825e026c7833b6d170b5f412a08.zip chromium_src-2fb382fd01716825e026c7833b6d170b5f412a08.tar.gz chromium_src-2fb382fd01716825e026c7833b6d170b5f412a08.tar.bz2 |
Let menu scroll when the menu content exceeds screen height.
Menu locator limits the size of window, and scrolling is handled in DOMUI.
This does not implement mouse wheel yet as the current implementation doesn't support it. I also haven't enable this yet for dropdown as I need to figure out how this should work for OOBE. I'll address it in separate CL.
Added command line parameter to specify DOMUI html page for testing.
BUG=none
TEST=none
Review URL: http://codereview.chromium.org/3608006
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@61800 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser')
-rw-r--r-- | chrome/browser/chromeos/dom_ui/menu_ui.cc | 239 | ||||
-rw-r--r-- | chrome/browser/chromeos/dom_ui/menu_ui.h | 5 | ||||
-rw-r--r-- | chrome/browser/chromeos/views/menu_locator.cc | 36 | ||||
-rw-r--r-- | chrome/browser/chromeos/views/native_menu_domui.cc | 4 | ||||
-rw-r--r-- | chrome/browser/resources/menu.css | 80 | ||||
-rw-r--r-- | chrome/browser/resources/menu.html | 16 | ||||
-rw-r--r-- | chrome/browser/resources/menu.js | 500 |
7 files changed, 560 insertions, 320 deletions
diff --git a/chrome/browser/chromeos/dom_ui/menu_ui.cc b/chrome/browser/chromeos/dom_ui/menu_ui.cc index c0c9856..2fb86d8 100644 --- a/chrome/browser/chromeos/dom_ui/menu_ui.cc +++ b/chrome/browser/chromeos/dom_ui/menu_ui.cc @@ -7,6 +7,7 @@ #include "app/menus/menu_model.h" #include "app/resource_bundle.h" #include "base/callback.h" +#include "base/command_line.h" #include "base/json/json_writer.h" #include "base/message_loop.h" #include "base/singleton.h" @@ -18,9 +19,13 @@ #include "chrome/browser/chromeos/views/domui_menu_widget.h" #include "chrome/browser/chromeos/views/native_menu_domui.h" #include "chrome/browser/dom_ui/dom_ui_util.h" +#include "chrome/browser/profile.h" #include "chrome/browser/tab_contents/tab_contents.h" #include "chrome/browser/tab_contents/tab_contents_delegate.h" #include "chrome/common/url_constants.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/net/url_fetcher.h" +#include "gfx/canvas_skia.h" #include "grit/app_resources.h" #include "grit/browser_resources.h" #include "views/controls/menu/menu_config.h" @@ -29,17 +34,111 @@ namespace { -std::string GetImageUrlForRadioOn() { - return dom_ui_util::GetImageDataUrl(*views::GetRadioButtonImage(true)); +// Creates scroll button's up image when |up| is true or +// down image if |up| is false. +SkBitmap CreateMenuScrollArrowImage(bool up) { + const views::MenuConfig& config = views::MenuConfig::instance(); + + int height = config.scroll_arrow_height; + gfx::CanvasSkia canvas(height * 2, height, false); + + if (!up) { + // flip the direction. + canvas.scale(1.0, -1.0); + canvas.translate(0, -height); + } + // Draw triangle. + SkPath path; + path.moveTo(height, 0); + path.lineTo(0, height); + path.lineTo(height * 2, height); + SkPaint paint; + paint.setColor(SK_ColorBLACK); + paint.setStyle(SkPaint::kFill_Style); + paint.setAntiAlias(true); + canvas.drawPath(path, paint); + return canvas.ExtractBitmap(); +} + +// Returns the scroll button's up image if |up| is true, or +// "down" image otherwise. +const std::string& GetImageDataUrlForMenuScrollArrow(bool up) { + static const std::string upImage = + dom_ui_util::GetImageDataUrl(CreateMenuScrollArrowImage(true)); + static const std::string downImage = + dom_ui_util::GetImageDataUrl(CreateMenuScrollArrowImage(false)); + return up ? upImage : downImage; +} + +// Returns the radio button's "on" image if |on| is true, or +// "off" image otherwise. +const std::string& GetImageDataUrlForRadio(bool on) { + static const std::string onImage = + dom_ui_util::GetImageDataUrl(*views::GetRadioButtonImage(true)); + static const std::string offImage = + dom_ui_util::GetImageDataUrl(*views::GetRadioButtonImage(false)); + return on ? onImage : offImage; +} + +std::string GetMenuUIHTMLSourceFromString(const std::string& menu_html) { +#define SET_INTEGER_PROPERTY(prop) \ + value_config.SetInteger(#prop, menu_config.prop) + + const views::MenuConfig& menu_config = views::MenuConfig::instance(); + + DictionaryValue value_config; + value_config.SetString("radioOnUrl", GetImageDataUrlForRadio(true)); + value_config.SetString("radioOffUrl", GetImageDataUrlForRadio(false)); + value_config.SetString( + "arrowUrl", dom_ui_util::GetImageDataUrlFromResource(IDR_MENU_ARROW)); + value_config.SetString( + "checkUrl", dom_ui_util::GetImageDataUrlFromResource(IDR_MENU_CHECK)); + + value_config.SetString( + "scrollUpUrl", GetImageDataUrlForMenuScrollArrow(true)); + value_config.SetString( + "scrollDownUrl", GetImageDataUrlForMenuScrollArrow(false)); + + SET_INTEGER_PROPERTY(item_top_margin); + SET_INTEGER_PROPERTY(item_bottom_margin); + SET_INTEGER_PROPERTY(item_no_icon_top_margin); + SET_INTEGER_PROPERTY(item_no_icon_bottom_margin); + SET_INTEGER_PROPERTY(item_left_margin); + SET_INTEGER_PROPERTY(label_to_arrow_padding); + SET_INTEGER_PROPERTY(arrow_to_edge_padding); + SET_INTEGER_PROPERTY(icon_to_label_padding); + SET_INTEGER_PROPERTY(gutter_to_label); + SET_INTEGER_PROPERTY(check_width); + SET_INTEGER_PROPERTY(check_height); + SET_INTEGER_PROPERTY(radio_width); + SET_INTEGER_PROPERTY(radio_height); + SET_INTEGER_PROPERTY(arrow_height); + SET_INTEGER_PROPERTY(arrow_width); + SET_INTEGER_PROPERTY(gutter_width); + SET_INTEGER_PROPERTY(separator_height); + SET_INTEGER_PROPERTY(render_gutter); + SET_INTEGER_PROPERTY(show_mnemonics); + SET_INTEGER_PROPERTY(scroll_arrow_height); + SET_INTEGER_PROPERTY(label_to_accelerator_padding); + + std::string json_config; + base::JSONWriter::Write(&value_config, false, &json_config); + return menu_html + "<script>init(" + json_config + ");</script>"; } -std::string GetImageUrlForRadioOff() { - return dom_ui_util::GetImageDataUrl(*views::GetRadioButtonImage(false)); +// Returns the menu's html code given by the resource id with the code +// to intialization the menu. The resource string should be pure code +// and should not contain i18n string. +std::string GetMenuUIHTMLSourceFromResource(int res) { + const base::StringPiece menu_html( + ResourceBundle::GetSharedInstance().GetRawDataResource(res)); + return GetMenuUIHTMLSourceFromString(menu_html.as_string()); } -class MenuUIHTMLSource : public ChromeURLDataManager::DataSource { +class MenuUIHTMLSource : public ChromeURLDataManager::DataSource, + public URLFetcher::Delegate { public: - MenuUIHTMLSource(); + explicit MenuUIHTMLSource(Profile* profile); // Called when the network layer has requested a resource underneath // the path we registered. @@ -50,9 +149,20 @@ class MenuUIHTMLSource : public ChromeURLDataManager::DataSource { return "text/html"; } + // URLFetcher::Delegate implements: + virtual void OnURLFetchComplete(const URLFetcher* source, + const GURL& url, + const URLRequestStatus& status, + int response_code, + const ResponseCookies& cookies, + const std::string& data); + private: virtual ~MenuUIHTMLSource() {} - +#ifndef NDEBUG + int request_id_; + Profile* profile_; +#endif DISALLOW_COPY_AND_ASSIGN(MenuUIHTMLSource); }; @@ -117,15 +227,32 @@ class MenuHandler : public chromeos::MenuHandlerBase, // //////////////////////////////////////////////////////////////////////////////// -MenuUIHTMLSource::MenuUIHTMLSource() - : DataSource(chrome::kChromeUIMenu, MessageLoop::current()) { +MenuUIHTMLSource::MenuUIHTMLSource(Profile* profile) + : DataSource(chrome::kChromeUIMenu, MessageLoop::current()) +#ifndef NDEBUG + , request_id_(-1), + profile_(profile) +#endif + { } void MenuUIHTMLSource::StartDataRequest(const std::string& path, bool is_off_the_record, int request_id) { +#ifndef NDEBUG + std::string url = CommandLine::ForCurrentProcess()->GetSwitchValueASCII( + switches::kDOMUIMenuUrl); + if (!url.empty()) { + request_id_ = request_id; + URLFetcher* fetcher = new URLFetcher(GURL(url), URLFetcher::GET, this); + fetcher->set_request_context(profile_->GetRequestContext()); + fetcher->Start(); + return; + } +#endif + static const std::string menu_html = - chromeos::GetMenuUIHTMLSourceFromResource(IDR_MENU_HTML); + GetMenuUIHTMLSourceFromResource(IDR_MENU_HTML); scoped_refptr<RefCountedBytes> html_bytes(new RefCountedBytes); @@ -136,6 +263,27 @@ void MenuUIHTMLSource::StartDataRequest(const std::string& path, SendResponse(request_id, html_bytes); } +void MenuUIHTMLSource::OnURLFetchComplete(const URLFetcher* source, + const GURL& url, + const URLRequestStatus& status, + int response_code, + const ResponseCookies& cookies, + const std::string& data) { + const std::string menu_html = + GetMenuUIHTMLSourceFromString(data); + + scoped_refptr<RefCountedBytes> html_bytes(new RefCountedBytes); + + // TODO(oshima): Eliminate boilerplate code. See http://crbug.com/57583 . + html_bytes->data.resize(menu_html.size()); + std::copy(menu_html.begin(), menu_html.end(), + html_bytes->data.begin()); + SendResponse(request_id_, html_bytes); + + delete source; +} + + //////////////////////////////////////////////////////////////////////////////// // // MenuHandler @@ -178,14 +326,12 @@ void MenuHandler::RegisterMessages() { void MenuHandler::HandleClick(const ListValue* values) { CHECK_EQ(1U, values->GetSize()); - std::string value; - bool success = values->GetString(0, &value); + std::string index_str; + bool success = values->GetString(0, &index_str); DCHECK(success); - int index; - success = base::StringToInt(value, &index); - DCHECK(success) << " Faild to convert string to int " << value; - + success = base::StringToInt(index_str, &index); + DCHECK(success); chromeos::DOMUIMenuControl* control = GetMenuControl(); if (control) { menus::MenuModel* model = GetMenuModel(); @@ -199,14 +345,19 @@ void MenuHandler::HandleClick(const ListValue* values) { } void MenuHandler::HandleOpenSubmenu(const ListValue* values) { + CHECK_EQ(2U, values->GetSize()); std::string index_str; - values->GetString(0, &index_str); + bool success = values->GetString(0, &index_str); + DCHECK(success); std::string y_str; - values->GetString(1, &y_str); + success = values->GetString(1, &y_str); + DCHECK(success); int index; + success = base::StringToInt(index_str, &index); + DCHECK(success); int y; - base::StringToInt(index_str, &index); - base::StringToInt(y_str, &y); + success = base::StringToInt(y_str, &y); + DCHECK(success); chromeos::DOMUIMenuControl* control = GetMenuControl(); if (control) control->OpenSubmenu(index, y); @@ -293,53 +444,7 @@ MenuUI::MenuUI(TabContents* contents) : DOMUI(contents) { } ChromeURLDataManager::DataSource* MenuUI::CreateDataSource() { - return new MenuUIHTMLSource(); -} - -std::string GetMenuUIHTMLSourceFromResource(int res) { -#define SET_INTEGER_PROPERTY(prop) \ - value_config.SetInteger(#prop, menu_config.prop) - - const base::StringPiece menu_html( - ResourceBundle::GetSharedInstance().GetRawDataResource(res)); - - const views::MenuConfig& menu_config = views::MenuConfig::instance(); - - DictionaryValue value_config; - value_config.SetString("radioOnUrl", GetImageUrlForRadioOn()); - value_config.SetString("radioOffUrl", GetImageUrlForRadioOff()); - value_config.SetString( - "arrowUrl", dom_ui_util::GetImageDataUrlFromResource(IDR_MENU_ARROW)); - value_config.SetString( - "checkUrl", dom_ui_util::GetImageDataUrlFromResource(IDR_MENU_CHECK)); - - SET_INTEGER_PROPERTY(item_top_margin); - SET_INTEGER_PROPERTY(item_bottom_margin); - SET_INTEGER_PROPERTY(item_no_icon_top_margin); - SET_INTEGER_PROPERTY(item_no_icon_bottom_margin); - SET_INTEGER_PROPERTY(item_left_margin); - SET_INTEGER_PROPERTY(label_to_arrow_padding); - SET_INTEGER_PROPERTY(arrow_to_edge_padding); - SET_INTEGER_PROPERTY(icon_to_label_padding); - SET_INTEGER_PROPERTY(gutter_to_label); - SET_INTEGER_PROPERTY(check_width); - SET_INTEGER_PROPERTY(check_height); - SET_INTEGER_PROPERTY(radio_width); - SET_INTEGER_PROPERTY(radio_height); - SET_INTEGER_PROPERTY(arrow_height); - SET_INTEGER_PROPERTY(arrow_width); - SET_INTEGER_PROPERTY(gutter_width); - SET_INTEGER_PROPERTY(separator_height); - SET_INTEGER_PROPERTY(render_gutter); - SET_INTEGER_PROPERTY(show_mnemonics); - SET_INTEGER_PROPERTY(scroll_arrow_height); - SET_INTEGER_PROPERTY(label_to_accelerator_padding); - - std::string json_config; - base::JSONWriter::Write(&value_config, false, &json_config); - - return menu_html.as_string() + - "<script>init(" + json_config + ");</script>"; + return new MenuUIHTMLSource(GetProfile()); } } // namespace chromeos diff --git a/chrome/browser/chromeos/dom_ui/menu_ui.h b/chrome/browser/chromeos/dom_ui/menu_ui.h index 99cd7b7..1d61b4a 100644 --- a/chrome/browser/chromeos/dom_ui/menu_ui.h +++ b/chrome/browser/chromeos/dom_ui/menu_ui.h @@ -48,11 +48,6 @@ class MenuHandlerBase : public DOMMessageHandler { DISALLOW_COPY_AND_ASSIGN(MenuHandlerBase); }; -// Returns the menu's html code given by the resource id with the code -// to intialization the menu. The resource string should be pure code -// and should not contain i18n string. -std::string GetMenuUIHTMLSourceFromResource(int res); - } // namespace chromeos #endif // CHROME_BROWSER_CHROMEOS_DOM_UI_MENU_UI_H_ diff --git a/chrome/browser/chromeos/views/menu_locator.cc b/chrome/browser/chromeos/views/menu_locator.cc index bf995e7..f92a250 100644 --- a/chrome/browser/chromeos/views/menu_locator.cc +++ b/chrome/browser/chromeos/views/menu_locator.cc @@ -16,7 +16,7 @@ namespace { using views::Widget; // Menu's corner radious. -const int kMenuCornerRadius = 4; +const int kMenuCornerRadius = 3; const int kSubmenuOverlapPx = 1; gfx::Rect GetBoundsOf(const views::Widget* widget) { @@ -61,12 +61,10 @@ class DropDownMenuLocator : public chromeos::MenuLocator { gfx::Rect screen_rect = GetScreenRectAt(origin_.x(), origin_.y()); int x = origin_.x() - size.width(); int y = origin_.y(); - if (x + size.width() > screen_rect.right()) { + if (x + size.width() > screen_rect.right()) x = screen_rect.right() - size.width(); - } - if (y + size.height() > screen_rect.bottom()) { + if (y + size.height() > screen_rect.bottom()) y = screen_rect.bottom() - size.height(); - } return gfx::Rect(x, y, size.width(), size.height()); } @@ -115,15 +113,17 @@ class ContextMenuLocator : public chromeos::MenuLocator { gfx::Rect ComputeBounds(const gfx::Size& size) { gfx::Rect screen_rect = GetScreenRectAt(origin_.x(), origin_.y()); + int height = size.height(); + if (height > screen_rect.height()) + height = screen_rect.height(); + int x = origin_.x(); int y = origin_.y(); - if (x + size.width() > screen_rect.right()) { + if (x + size.width() > screen_rect.right()) x = screen_rect.right() - size.width(); - } - if (y + size.height() > screen_rect.bottom()) { - y = screen_rect.bottom() - size.height(); - } - return gfx::Rect(x, y, size.width(), size.height()); + if (y + height > screen_rect.bottom()) + y = screen_rect.bottom() - height; + return gfx::Rect(x, y, size.width(), height); } virtual const SkScalar* GetCorners() const { @@ -189,20 +189,22 @@ class SubMenuLocator : public chromeos::MenuLocator { gfx::Rect ComputeBounds(const gfx::Size& size) { gfx::Rect screen_rect = GetScreenRectAt(parent_rect_.x(), root_y_); + int height = size.height(); + if (height > screen_rect.height()) + height = screen_rect.height(); + SubmenuDirection direction = parent_direction_; - if (direction == DEFAULT) { + if (direction == DEFAULT) direction = RIGHT; // TOOD(oshima): support RTL - } // Adjust Y to fit the screen. int y = root_y_; - if (root_y_ + size.height() > screen_rect.bottom()) { - y = screen_rect.bottom() - size.height(); - } + if (root_y_ + height > screen_rect.bottom()) + y = screen_rect.bottom() - height; // Decide the attachment. int x = direction == RIGHT ? ComputeXToRight(screen_rect, size) : ComputeXToLeft(screen_rect, size); - return gfx::Rect(x, y, size.width(), size.height()); + return gfx::Rect(x, y, size.width(), height); } int ComputeXToRight(const gfx::Rect& screen_rect, const gfx::Size& size) { diff --git a/chrome/browser/chromeos/views/native_menu_domui.cc b/chrome/browser/chromeos/views/native_menu_domui.cc index 42ca581..c5b1f63 100644 --- a/chrome/browser/chromeos/views/native_menu_domui.cc +++ b/chrome/browser/chromeos/views/native_menu_domui.cc @@ -187,8 +187,8 @@ void NativeMenuDOMUI::Rebuild() { } items->Set(index, item); } - model.SetBoolean("has_icon", has_icon); - model.SetBoolean("is_root", menu_widget_->is_root()); + model.SetBoolean("hasIcon", has_icon); + model.SetBoolean("isRoot", menu_widget_->is_root()); std::string json_model; base::JSONWriter::Write(&model, false, &json_model); diff --git a/chrome/browser/resources/menu.css b/chrome/browser/resources/menu.css index 44e584f..63ce0b2 100644 --- a/chrome/browser/resources/menu.css +++ b/chrome/browser/resources/menu.css @@ -1,17 +1,16 @@ body { - margin: 0px; background: -webkit-gradient(linear, left top, left bottom, from(white), to(#EEE)); - margin: 0px 0px 0px 0px; + margin: 0 0 0 0; + -webkit-user-select: none; } -.menu_item { - margin-left: 0px; - margin-right: 0px; - margin-top: 0px; +.menu-item { white-space: nowrap; - padding: 0px 20px 0px 5px; + margin: 0 0 0 0; + padding: 0 20px 0 5px; + background-repeat: no-repeat; } .disabled { @@ -19,45 +18,66 @@ body { } .noicon { - padding-left: 20px; + -webkit-padding-start: 20px; } -.menu_label { +.menu-label { display: inline-block; vertical-align: middle; } -.left_icon { - vertical-align: middle; - margin-right: 10px; -} - -.right_icon { - vertical-align: middle; - right: 10px; +.left-icon { + background-position: 4px center; + text-indent: 12px; } -#menu { - width: 100%; - height: 100%; +.right-icon { + background-position: right center; } .separator { - width: 85%; + background: -webkit-gradient(linear, 0 0, 96% 0, from(rgba(0, 0, 0, .10)), + to(rgba(0, 0, 0, .02))); + border: 0; 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)); + margin: 4px 0; } -.mnemonic_enabled .mnemonic { +.mnemonic-enabled .mnemonic { text-decoration: underline; } .selected { - background: #DCE4FA; + background-color: #DCE5FA; } + +#viewport { + overflow: hidden; + height: 100%; +} + +.scroll-button { + height: 20px; + width: 100%; + line-height: 20px; + text-align: center; + background-repeat: no-repeat; + background-position: center center; +} + +#scroll-up { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAADCAYAAACwAX77AAAAI0lEQVQImWNgQIA4BgaGuQxoII6BgeEvAwPDf2RJZEEYngsA2N8I6FqZBpwAAAAASUVORK5CYII="); +} + +#scroll-down { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAADCAYAAACwAX77AAAAIklEQVQImWNgYGCIY2Bg+MfAwPAfCc9jgAJkSbggsiRcEACghQjtUFgYGwAAAABJRU5ErkJggg=="); +} + +.scroll-button:hover { + background-color: #DCE5FA; +} + +.hidden { + display: none; +} + diff --git a/chrome/browser/resources/menu.html b/chrome/browser/resources/menu.html index 8015bdf..c9216f0 100644 --- a/chrome/browser/resources/menu.html +++ b/chrome/browser/resources/menu.html @@ -1,14 +1,16 @@ <!doctype html> <html> <head> - <meta charset="utf-8"/> - <link rel="stylesheet" href="menu.css"/> - <script src="menu.js"></script> + <meta charset="utf-8"> + <link rel="stylesheet" href="menu.css"> + <script src="menu.js"></script> </head> <body> - <div id="menu"> </div> - <script> - setMenu(new Menu()); - </script> + <div id="scroll-up" class="scroll-button"></div> + <div id="viewport"> </div> + <div id="scroll-down" class="scroll-button"></div> + <script> + setMenu(new Menu()); + </script> </body> </html> diff --git a/chrome/browser/resources/menu.js b/chrome/browser/resources/menu.js index 0dffad4..8fa4c3e 100644 --- a/chrome/browser/resources/menu.js +++ b/chrome/browser/resources/menu.js @@ -6,32 +6,39 @@ var SUBMENU_OPEN_DELAY_MS = 200; // How long to wait to close submenu when mouse left. var SUBMENU_CLOSE_DELAY_MS = 500; +// Scroll repeat interval. +var SCROLL_INTERVAL_MS = 20; +// Scrolling amount in pixel. +var SCROLL_TICK_PX = 4; // Regular expression to match/find mnemonic key. -var MNEMONIC_REGEXP = /&(.)/; +var MNEMONIC_REGEXP = /([^&]*)&(.)(.*)/; /** * Sends 'click' DOMUI message. */ -function sendClick(id) { - chrome.send('click', [ id + '' ]); +function sendClick(index) { + chrome.send('click', [String(index)]); } /** * MenuItem class. + * @param {Menu} menu A {@code Menu} object to which this menu item will be + * added to. + * @param {Object} attrs JSON object that represents this menu items + * properties. This is created from menu model in C code. See + * chromeos/views/native_menu_domui.cc. */ -function MenuItem(menu, id, attrs) { +function MenuItem(menu, 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 + * @param {boolean} hasIcon True if the menu has left icon. */ - init: function(has_icon) { + init: function(hasIcon) { this.div = document.createElement('div'); var attrs = this.attrs; if (attrs.type == 'separator') { @@ -40,101 +47,90 @@ MenuItem.prototype = { attrs.type == 'submenu' || attrs.type == 'check' || attrs.type == 'radio') { - this.initMenuItem(has_icon); + this.initMenuItem_(hasIcon); } else { - this.div.className = 'menu_item disabled'; - this.div.innerHTML = 'unknown'; + this.div.className = 'menu-item disabled'; + this.div.textContent = 'unknown'; } - this.div.classList.add(has_icon ? 'has_icon' : 'noicon'); + this.div.classList.add(hasIcon ? 'has-icon' : 'noicon'); }, /** - * Select this item. - * @public + * Chagnes the selection state of the menu item. + * @param {boolean} b True to set the selection, or false otherwise. */ - select: function() { - this.div.classList.add('selected'); - this.menu_.setSelection(this); - }, - - /** - * Unselect this item. - * @public - */ - unselect: function() { - this.div.classList.remove('selected'); + set select(b) { + if (b) { + this.div.classList.add('selected'); + this.menu_.selectedItem = this; + } else { + this.div.classList.remove('selected'); + } }, /** - * Activat the menu item. - * @public + * Activate the menu item. */ activate: function() { if (this.attrs.type == 'submenu') { this.menu_.openSubmenu(this); } else if (this.attrs.type != 'separator' && this.div.className.indexOf('selected') >= 0) { - sendClick(this.id); + sendClick(this.menu_.getMenuItemIndexOf(this)); } }, /** * 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; + chrome.send('open_submenu', + [String(this.menu_.getMenuItemIndexOf(this)), + String(this.div.getBoundingClientRect().top)]); }, /** * Internal method to initiailze the MenuItem's div element. * @private */ - initMenuItem: function(has_icon) { + initMenuItem_: function(hasIcon) { var attrs = this.attrs; - this.div.className = 'menu_item ' + attrs.type; + this.div.className = 'menu-item ' + attrs.type; this.menu_.addHandlers(this); var mnemonic = MNEMONIC_REGEXP.exec(attrs.label); if (mnemonic) { - var c = mnemonic[1]; + var c = mnemonic[2]; 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 (hasIcon) { + this.div.classList.add('left-icon'); + var url; if (attrs.type == 'radio') { - icon.src = attrs.checked ? - menu_.config_.radioOnUrl : menu_.config_.radioOffUrl; + url = attrs.checked ? + this.menu_.config_.radioOnUrl : + this.menu_.config_.radioOffUrl; } else if (attrs.icon) { - icon.src = attrs.icon; + url = attrs.icon; } else if (attrs.type == 'check' && attrs.checked) { - icon.src = menu_.config_.checkUrl; - } else { - icon.style.width = '12px'; + url = this.menu_.config_.checkUrl; + } + if (url) { + this.div.style.backgroundImage = "url(" + url + ")"; } - this.div.appendChild(icon); } var label = document.createElement('div'); - label.className = 'menu_label'; - label.innerHTML = text; + label.className = 'menu-label'; + + if (!mnemonic) { + label.textContent = attrs.label; + } else { + label.appendChild(document.createTextNode(mnemonic[1])); + label.appendChild(document.createElement('span')); + label.appendChild(document.createTextNode(mnemonic[3])); + label.childNodes[1].className = 'mnemonic'; + label.childNodes[1].textContent = mnemonic[2]; + } if (attrs.font) { label.style.font = attrs.font; @@ -142,10 +138,11 @@ MenuItem.prototype = { 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); + // This overrides left-icon's position, but it's OK as submenu + // shoudln't have left-icon. + this.div.classList.add('right-icon'); + this.div.style.backgroundImage = + "url(" + this.menu_.config_.arrowUrl + ")"; } }, }; @@ -154,202 +151,271 @@ MenuItem.prototype = { * 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 */ + // List of menu items this.items_ = []; - /* map from mnemonic character to item to activate */ + // Map from mnemonic character to item to activate this.mnemonics_ = {}; - /* true if this menu is root */ - this.is_root_ = false; } Menu.prototype = { /** + * Configuration object. + * @type {Object} + */ + config_ : null, + /** + * Currently selected menu item. + * @type {MenuItem} + */ + current_ : null, + /** + * Timers for opening/closing submenu. + * @type {number} + */ + openSubmenuTimer_ : 0, + closeSubmenuTimer_ : 0, + /** + * Auto scroll timer. + * @type {number} + */ + scrollTimer_ : 0, + /** + * Pointer to a submenu currently shown, if any. + * @type {MenuItem} + */ + submenuShown_ : null, + /** + * True if this menu is root. + * @type {boolean} + */ + isRoot_ : false, + /** + * Scrollable Viewport. + * @type {HTMLElement} + */ + viewpotr_ : null, + /** + * Total hight of scroll buttons. Used to adjust the height of + * viewport in order to show scroll bottons without scrollbar. + * @type {number} + */ + buttonHeight_ : 0, + + /** * Initialize the menu. - * @public */ init: function(config) { this.config_ = config; - document.getElementById('menu').onmouseout = this.onMouseout_.bind(this); + this.viewport_ = document.getElementById('viewport'); + this.viewport_.addEventListener('mouseout', this.onMouseout_.bind(this)); + document.addEventListener('keydown', this.onKeydown_.bind(this)); - /* disable text select */ - document.onselectstart = function() { return false; } + document.addEventListener('keypress', this.onKeypress_.bind(this)); + window.addEventListener('resize', this.onResize_.bind(this)); + window.addEventListener('mousewheel', this.onMouseWheel_.bind(this)); + + // Setup scroll events. + var up = document.getElementById('scroll-up'); + var down = document.getElementById('scroll-down'); + up.addEventListener('mouseout', this.stopScroll_.bind(this)); + down.addEventListener('mouseout', this.stopScroll_.bind(this)); + var menu = this; + up.addEventListener('mouseover', + function() { + menu.autoScroll_(-SCROLL_TICK_PX); + }); + down.addEventListener('mouseover', + function() { + menu.autoScroll_(SCROLL_TICK_PX); + }); + + this.buttonHeight_ = + up.getBoundingClientRect().height + + down.getBoundingClientRect().height; + }, + + /** + * Returns the index of the {@code item}. + */ + getMenuItemIndexOf: function(item) { + return this.items_.indexOf(item); }, /** * 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); + createMenuItem: function(attrs) { + return new MenuItem(this, attrs); }, /** * Update and display the new model. - * @public */ updateModel: function(model) { - this.is_root = model.is_root; + this.isRoot = model.isRoot; this.current_ = null; this.items_ = []; this.mnemonics_ = {}; + this.viewport_.innerHTML = ''; // remove menu items - var menu = document.getElementById('menu'); - menu.innerHTML = ''; // remove menu items - - var id = 0; - - for (i in model.items) { + for (var i = 0; i < model.items.length; i++) { var attrs = model.items[i]; - var item = this.createMenuItem(id++, attrs); - this.items_[item.id] = item; - this.last_id_ = item.id; + var item = this.createMenuItem(attrs); + this.items_.push(item); if (!item.attrs.visible) { continue; } - - item.init(model.has_icon); - menu.appendChild(item.div); + item.init(model.hasIcon); + this.viewport_.appendChild(item.div); } + this.onResize_(); }, /** * Highlights the currently selected item, or * select the 1st selectable item if none is selected. - * @public */ showSelection: function() { if (this.current_) { - this.current_.select(); + this.current_.select = true; } else { - this.findNextEnabled_(1).select(); + this.findNextEnabled_(1).select = true; } }, /** * Registers mnemonic key. - * @param {c} a mnemonic key to activate item. - * @param {item} an item to be activated when {c} is pressed. - * @public + * @param {string} c A mnemonic key to activate item. + * @param {MenuItem} item An item to be activated when {@code c} is pressed. */ 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); - }); + menu.onMouseover_(event, item); + }); if (item.attrs.enabled) { item.div.addEventListener('mouseup', function(event) { - menu.onClick_(event, item); - }); + menu.onClick_(event, item); + }); } else { item.div.classList.add('disabled'); } }, /** - * Set the selected item. This also start or cancel - * @public + * Set the selected item. This controls timers to open/close submenus. + * 1) If the selected menu is submenu, and that submenu is not yet opeend, + * start timer to open. This will not cancel close timer, so + * if there is a submenu opened, it will be closed before new submenu is + * open. + * 2) If the selected menu is submenu, and that submenu is already opened, + * cancel both open/close timer. + * 3) If the selected menu is not submenu, cancel all timers and start + * timer to close submenu. + * This prevents from opening/closing menus while you're actively + * navigating menus. To open submenu, you need to wait a bit, or click + * submenu. + * + * @param {MenuItem} item The selected item. */ - setSelection: function(item) { - if (this.current_ == item) - return; - - if (this.current_ != null) - this.current_.unselect(); - - this.current_ = item; + set selectedItem(item) { + if (this.current_ != item) { + if (this.current_ != null) + this.current_.select = false; + this.current_ = item; + this.makeSelectedItemVisible_(); + } 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); + if (this.submenuShown_ != item) { + this.openSubmenuTimer_ = + setTimeout( + function() { + menu.openSubmenu(item); + }, + SUBMENU_OPEN_DELAY_MS); } else { this.cancelSubmenuTimer_(); } - } else if (this.submenu_shown_) { + } else if (this.submenuShown_) { this.cancelSubmenuTimer_(); - this.close_submenu_timer_ = - setTimeout(function() { menu.closeSubmenu_(item); }, - SUBMENU_CLOSE_DELAY_MS); + this.closeSubmenuTimer_ = + setTimeout( + function() { + menu.closeSubmenu_(item); + }, + SUBMENU_CLOSE_DELAY_MS); } }, /** - * Open submenu {item}. It does nothing if the submenu is + * Open submenu {@code item}. It does nothing if the submenu is * already opened. - * @param {item} the submenu item to open. - * @public + * @param {MenuItem} item the submenu item to open. */ openSubmenu: function(item) { this.cancelSubmenuTimer_(); - if (this.submenu_shown_ != item) { - this.submenu_shown_ = item; + if (this.submenuShown_ != item) { + this.submenuShown_ = item; item.sendOpenSubmenuCommand(); } }, /** - * Handle keyboard navigatio and mnemonic keys. + * Handle keyboard navigation and activation. * @private */ onKeydown_: function(event) { - switch (event.keyCode) { - case 27: /* escape */ - sendClick(-1); // -1 closes the menu. - break; - case 37: /* left */ + switch (event.keyIdentifier) { + case 'Left': this.moveToParent_(); break; - case 39: /* right */ + case '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(); + case 'Up': + this.viewport_.className = 'mnemonic-enabled'; + this.findNextEnabled_(-1).select = true; + break; + case 'Down': + this.viewport_.className = 'mnemonic-enabled'; + this.findNextEnabled_(1).select = true; break; - case 9: /* tab */ - // TBD. + case 'U+0009': // tab break; - case 13: /* return */ - case 32: /* space */ + case 'U+001B': // escape + sendClick(-1); // -1 closes the menu. + break; + case 'Enter': + case 'U+0020': // 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(); } }, + /** + * Handle mnemonic keys. + * @private + */ + onKeypress_: function(event) { + // Handles mnemonic. + var c = String.fromCharCode(event.keyCode); + var item = this.mnemonics_[c.toLowerCase()]; + if (item) + item.activate(); + }, + // Mouse Event handlers onClick_: function(event, item) { item.activate(); @@ -359,25 +425,42 @@ Menu.prototype = { this.cancelSubmenuTimer_(); // Ignore false mouseover event at (0,0) which is // emitted when opening submenu. - if (item.attrs.enabled && - (event.x != 0 && event.y != 0)) { - item.select(); + if (item.attrs.enabled && event.clientX != 0 && event.clientY != 0) { + item.select = true; } }, onMouseout_: function(event) { if (this.current_) { - this.current_.unselect(); + this.current_.select = false; + } + }, + + onResize_: function() { + if (this.viewport_.scrollHeight > window.innerHeight) { + this.viewport_.style.height = + (window.innerHeight - this.buttonHeight_) + 'px'; + document.getElementById('scroll-up').classList.remove('hidden'); + document.getElementById('scroll-down').classList.remove('hidden'); + } else { + this.viewport_.style.height = ''; + document.getElementById('scroll-up').classList.add('hidden'); + document.getElementById('scroll-down').classList.add('hidden'); } }, + onMouseWheel_: function(event) { + var delta = event.wheelDelta / 5; + this.viewport_.scrollTop -= delta; + }, + /** * Closes the submenu. * a submenu. * @private */ closeSubmenu_: function(item) { - this.submenu_shown_ = null; + this.submenuShown_ = null; this.cancelSubmenuTimer_(); chrome.send('close_submenu', []); }, @@ -388,9 +471,9 @@ Menu.prototype = { * @private */ moveToParent_: function() { - if (!this.is_root) { + if (!this.isRoot) { if (this.current_) { - this.current_.unselect(); + this.current_.select = false; } chrome.send('move_to_parent', []); } @@ -403,7 +486,7 @@ Menu.prototype = { */ moveToSubmenu_: function () { var current = this.current_; - if(current && current.attrs.type == 'submenu') { + if (current && current.attrs.type == 'submenu') { this.openSubmenu(current); chrome.send('move_to_submenu', []); } @@ -418,24 +501,20 @@ Menu.prototype = { * @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; + var len = this.items_.length; + var index; + if (this.current_) { + index = this.getMenuItemIndexOf(this.current_); + } else { + index = incr > 0 ? -1 : len; + } + for (var i = 0; i < len; i++) { + index = (index + incr + len) % len; + var item = this.items_[index]; + if (item.attrs.enabled && item.attrs.type != 'separator') + return item; + } + return null; }, /** @@ -443,15 +522,52 @@ Menu.prototype = { * @private */ cancelSubmenuTimer_: function() { - if (this.open_submenu_timer_) { - clearTimeout(this.open_submenu_timer_); - this.open_submenu_timer_ = 0; + if (this.openSubmenuTimer_) { + clearTimeout(this.openSubmenuTimer_); + this.openSubmenuTimer_ = 0; + } + if (this.closeSubmenuTimer_) { + clearTimeout(this.closeSubmenuTimer_); + this.closeSubmenuTimer_ = 0; + } + }, + + /** + * Starts auto scroll. + * @param {number} tick the number of pixels to scroll. + * @private + */ + autoScroll_: function(tick) { + var previous = this.viewport_.scrollTop; + this.viewport_.scrollTop += tick; + if (this.viewport_.scrollTop != previous) { + var menu = this; + this.scrollTimer_ = setTimeout( + function() { + menu.autoScroll_(tick); + }, + SCROLL_INTERVAL_MS); } - if (this.close_submenu_timer_) { - clearTimeout(this.close_submenu_timer_); - this.close_submenu_timer_ = 0; + }, + + /** + * Stops auto scroll. + * @private + */ + stopScroll_: function () { + if (this.scrollTimer_) { + clearTimeout(this.scrollTimer_); + this.scrollTimer_ = 0; } }, + + /** + * Scrolls the viewport to make the selected item visible. + * @private + */ + makeSelectedItemVisible_: function(){ + this.current_.div.scrollIntoViewIfNeeded(); + }, }; /** |