diff options
author | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-14 22:32:23 +0000 |
---|---|---|
committer | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-14 22:32:23 +0000 |
commit | 4cc5e954e5a7b0c60f88b5461fd953d377ffe5f9 (patch) | |
tree | 5a2e69c5caacc2251599c3691ed662e072a28995 | |
parent | fdabdc748152405ebdd2f7910ee9d5aebe26588c (diff) | |
download | chromium_src-4cc5e954e5a7b0c60f88b5461fd953d377ffe5f9.zip chromium_src-4cc5e954e5a7b0c60f88b5461fd953d377ffe5f9.tar.gz chromium_src-4cc5e954e5a7b0c60f88b5461fd953d377ffe5f9.tar.bz2 |
Bookmark manager: Middle click, Ctrl click, Shift+Control click should open bookmark link in a background tab etc..
BUG=40359
TEST=Middle click on a bookmark link. It should open in a background tab
Review URL: http://codereview.chromium.org/1553026
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@44560 0039d316-1c4b-4281-b951-d872f2087c98
8 files changed, 658 insertions, 118 deletions
diff --git a/chrome/browser/resources/bookmark_manager/css/bmm.css b/chrome/browser/resources/bookmark_manager/css/bmm.css index e9e75e5d7..4a9e48d 100644 --- a/chrome/browser/resources/bookmark_manager/css/bmm.css +++ b/chrome/browser/resources/bookmark_manager/css/bmm.css @@ -35,9 +35,6 @@ list > * > * { list > * > .label { -webkit-transition: all .15s; cursor: pointer; -} - -list .label { color: black; } diff --git a/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js index a683208..a81ca23 100644 --- a/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js +++ b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js @@ -152,11 +152,10 @@ cr.define('bmm', function() { handleClick_: function(e) { var self = this; - function dispatch(url, kind) { - var event = self.ownerDocument.createEvent('Event'); - event.initEvent('urlClicked', true, false); + function dispatch(url) { + var event = new cr.Event('urlClicked', true, false); event.url = url; - event.kind = kind; + event.originalEvent = e; self.dispatchEvent(event); } @@ -164,8 +163,7 @@ cr.define('bmm', function() { // Handle clicks on the links to URLs. if (el.href) { - dispatch(el.href, - e.shiftKey ? 'window' : e.button == 1 ? 'tab' : 'self'); + dispatch(el.href); // Handle middle click to open bookmark in a new tab. } else if (e.button == 1) { @@ -174,7 +172,7 @@ cr.define('bmm', function() { } var node = el.bookmarkNode; if (!bmm.isFolder(node)) - dispatch(node.url, 'tab'); + dispatch(node.url); } }, @@ -460,6 +458,7 @@ cr.define('bmm', function() { var urlEl = el.childNodes[1]; labelEl.href = urlEl.textContent = bookmarkNode.url; } else { + labelEl.href = '#' + bookmarkNode.id; el.className = 'folder'; } } @@ -470,25 +469,6 @@ cr.define('bmm', function() { return div; })(); - - /** - * Workaround for http://crbug.com/40902 - * @param {!HTMLElement} list The element to fix the width for. - */ - function fixListWidth(list) { - // The width of the list is wrong after its content has changed. Fortunately - // the reported offsetWidth is correct so we can detect the incorrect width. - if (list.offsetWidth != list.parentNode.clientWidth - list.offsetLeft) { - // Set the width to the correct size. This causes the relayout. - list.style.width = list.parentNode.clientWidth - list.offsetLeft + 'px'; - // Remove the temporary style.width in a timeout. Once the timer fires the - // size should not change since we already fixed the width. - window.setTimeout(function() { - list.style.width = ''; - }, 0); - } - } - return { createListItem: createListItem, BookmarkList: BookmarkList, diff --git a/chrome/browser/resources/bookmark_manager/js/cr/linkcontroller.js b/chrome/browser/resources/bookmark_manager/js/cr/linkcontroller.js new file mode 100644 index 0000000..e6241de --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/linkcontroller.js @@ -0,0 +1,168 @@ +// 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. + +/** + * @fileoverview This file provides a class that can be used to open URLs based + * on user interactions. It ensures a consistent behavior when it comes to + * holding down Ctrl and Shift while clicking or activating the a link. + * + * This depends on the {@code chrome.windows} and {@code chrome.tabs} + * extensions API. + */ + +cr.define('cr', function() { + + /** + * The kind of link open we want to perform. + * @enum {number} + */ + const LinkKind = { + FOREGROUND_TAB: 0, + BACKGROUND_TAB: 1, + WINDOW: 2, + SELF: 3, + INCOGNITO: 4 + }; + + /** + * This class is used to handle opening of links based on user actions. The + * following actions are currently implemented: + * + * * Press Ctrl and click a link. Or click a link with your middle mouse + * button (or mousewheel). Or press Enter while holding Ctrl. + * Opens the link in a new tab in the background . + * * Press Ctrl+Shift and click a link. Or press Shift and click a link with + * your middle mouse button (or mousewheel). Or press Enter while holding + * Ctrl+Shift. + * Opens the link in a new tab and switches to the newly opened tab. + * * Press Shift and click a link. Or press Enter while holding Shift. + * Opens the link in a new window. + * + * On Mac, uses Command instead of Ctrl. + * For keyboard support you need to use keydown. + * + * @param {!LocalStrings} localStrings The local strings object which is used + * to localize the warning prompt in case the user tries to open a lot of + * links. + * @constructor + */ + function LinkController(localStrings) { + this.localStrings_ = localStrings; + } + + LinkController.prototype = { + /** + * The number of links that can be opened before showing a warning confirm + * message. + */ + warningLimit: 15, + + /** + * The DOM window that we want to open links into in case we are opening + * links in the same window. + * @type {!Window} + */ + window: window, + + /** + * This method is used for showing the warning confirm message when the + * user is trying to open a lot of links. + * @param {number} The number of URLs to open. + * @return {string} The message to show the user. + */ + getWarningMessage: function(count) { + return this.localStrings_.getStringF('should_open_all', count); + }, + + /** + * Open an URL from a mouse or keyboard event. + * @param {string} url The URL to open. + * @param {!Event} e The event triggering the opening of the URL. + */ + openUrlFromEvent: function(url, e) { + // We only support keydown Enter and non right click events. + if (e.type == 'keydown') { + if(e.keyIdentifier != 'Enter') + return; + } else if (e.type != 'click' || e.button == 2) { + return; + } + + var kind; + var ctrl = cr.isMac && e.metaKey || !cr.isMac && e.ctrlKey; + + if (e.button == 1 || ctrl) // middle, ctrl or keyboard + kind = e.shiftKey ? LinkKind.FOREGROUND_TAB : LinkKind.BACKGROUND_TAB; + else // left or keyboard + kind = e.shiftKey ? LinkKind.WINDOW : LinkKind.SELF; + + this.openUrls([url], kind); + }, + + + /** + * Opens a URL in a new tab, window or incognito window. + * @param {string} url The URL to open. + * @param {LinkKind} kind The kind of open we want to do. + */ + openUrl: function (url, kind) { + this.openUrls([url], kind); + }, + + /** + * Opens URLs in new tab, window or incognito mode. + * @param {!Array.<string>} urls The URLs to open. + * @param {LinkKind} kind The kind of open we want to do. + */ + openUrls: function (urls, kind) { + if (urls.length < 1) + return; + + if (urls.length > this.warningLimit) { + if (!this.window.confirm(this.getWarningMessage(urls.length))) + return; + } + + // Fix '#124' URLs since opening those in a new window does not work. We + // prepend the base URL when we encounter those. + var base = this.window.location.href.split('#')[0]; + urls = urls.map(function(url) { + return url[0] == '#' ? base + url : url; + }); + + var incognito = kind == LinkKind.INCOGNITO; + if (kind == LinkKind.WINDOW || incognito) { + chrome.windows.create({ + url: urls[0], + incognito: incognito + }, function(window) { + urls.forEach(function(url, i) { + if (i > 0) + chrome.tabs.create({ + url: url, + windowId: window.id, + selected: false + }); + }); + }); + } else if (kind == LinkKind.FOREGROUND_TAB || + kind == LinkKind.BACKGROUND_TAB) { + urls.forEach(function(url, i) { + chrome.tabs.create({ + url: url, + selected: kind == LinkKind.FOREGROUND_TAB && !i + }); + }); + } else { + this.window.location.href = urls[0]; + } + } + }; + + // Export + return { + LinkController: LinkController, + LinkKind: LinkKind + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/linkcontroller_test.html b/chrome/browser/resources/bookmark_manager/js/cr/linkcontroller_test.html new file mode 100644 index 0000000..eb7ebd5 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/linkcontroller_test.html @@ -0,0 +1,353 @@ +<!DOCTYPE html> +<html> +<head> +<!-- TODO(arv): Check in Closue unit tests and make this run as part of the + tests --> +<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script> +<script src="../cr.js"></script> +<script src="linkcontroller.js"></script> +<script> + +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.mockmatchers'); + +</script> +</head> +<body> +<script> + +var mockControl, propertyReplacer, mockWindow; +var chrome = chrome || {}; +chrome.tabs = chrome.tabs || {}; +chrome.windows = chrome.windows || {}; + +var ObjectEqualsMatcher = goog.testing.mockmatchers.ObjectEquals; +var SaveArgumentMatcher = goog.testing.mockmatchers.SaveArgument; + +var MSG = 'MSG'; +var localStrings = { + getStringF: function(msg, number) { + assertEquals('should_open_all', msg); + return MSG + number; + } +}; + +var URL1 = 'http://chromium.org/'; +var URL2 = '#hash'; +var WINDOW_ID = 'WINDOW_ID'; + +function setUp() { + mockControl = new goog.testing.MockControl; + chrome.tabs.create = mockControl.createFunctionMock(); + chrome.windows.create = mockControl.createFunctionMock(); + + propertyReplacer = new goog.testing.PropertyReplacer; + + mockWindow = { + confirm: mockControl.createFunctionMock(), + location: { + get href() { + return 'http://www.google.com/'; + }, + set href(url) { + assertEquals(URL1, url); + } + } + }; +} + +function tearDown() { + mockControl.$tearDown(); + propertyReplacer.reset(); +} + +function testGetWarningMessage() { + var lc = new cr.LinkController(localStrings); + var msg = lc.getWarningMessage(10); + assertEquals(MSG + 10, msg); +} + +function openUrlFromEventHelper(event, isMac, expectedKind) { + var lc = new cr.LinkController(localStrings); + + lc.openUrls = mockControl.createFunctionMock(); + lc.openUrls([URL1], expectedKind); + + propertyReplacer.set(cr, 'isMac', isMac); + + mockControl.$replayAll(); + + lc.openUrlFromEvent(URL1, event); + + mockControl.$verifyAll(); +} + +/////////////////////////////////////////////////////////////////////////////// + +function testOpenUrlFromEventForegroundTab() { + var e = { + type: 'click', + button: 0, + shiftKey: true, + ctrlKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.FOREGROUND_TAB); +} + +function testOpenUrlFromEventForegroundTabMac() { + var e = { + type: 'click', + button: 0, + shiftKey: true, + metaKey: true + }; + openUrlFromEventHelper(e, true, cr.LinkKind.FOREGROUND_TAB); +} + +function testOpenUrlFromEventForegroundTabEnter() { + var e = { + type: 'keydown', + keyIdentifier: 'Enter', + shiftKey: true, + ctrlKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.FOREGROUND_TAB); +} + +function testOpenUrlFromEventForegroundTabEnterMac() { + var e = { + type: 'keydown', + keyIdentifier: 'Enter', + shiftKey: true, + metaKey: true + }; + openUrlFromEventHelper(e, true, cr.LinkKind.FOREGROUND_TAB); +} + +function testOpenUrlFromEventForegroundTabMiddleClick() { + var e = { + type: 'click', + button: 1, + shiftKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.FOREGROUND_TAB); +} + +/////////////////////////////////////////////////////////////////////////////// + +function testOpenUrlFromEventBackgroundTab() { + var e = { + type: 'click', + button: 0, + ctrlKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.BACKGROUND_TAB); +} + +function testOpenUrlFromEventBackgroundTabMac() { + var e = { + type: 'click', + button: 0, + metaKey: true + }; + openUrlFromEventHelper(e, true, cr.LinkKind.BACKGROUND_TAB); +} + +function testOpenUrlFromEventBackgroundTabEnter() { + var e = { + type: 'keydown', + keyIdentifier: 'Enter', + ctrlKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.BACKGROUND_TAB); +} + +function testOpenUrlFromEventBackgroundTabEnterMac() { + var e = { + type: 'keydown', + keyIdentifier: 'Enter', + metaKey: true + }; + openUrlFromEventHelper(e, true, cr.LinkKind.BACKGROUND_TAB); +} + +function testOpenUrlFromEventBackgroundTabMiddleClick() { + var e = { + type: 'click', + button: 1 + }; + openUrlFromEventHelper(e, false, cr.LinkKind.BACKGROUND_TAB); +} + +/////////////////////////////////////////////////////////////////////////////// + +function testOpenUrlFromEventWindow() { + var e = { + type: 'click', + button: 0, + shiftKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.WINDOW); +} + +function testOpenUrlFromEventWindowEnter() { + var e = { + type: 'keydown', + keyIdentifier: 'Enter', + shiftKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.WINDOW); +} + +/////////////////////////////////////////////////////////////////////////////// + +function testOpenUrlFromEventSelf() { + var e = { + type: 'click', + button: 0 + }; + openUrlFromEventHelper(e, false, cr.LinkKind.SELF); +} + +function testOpenUrlFromEventSelfEnter() { + var e = { + type: 'keydown', + keyIdentifier: 'Enter' + }; + openUrlFromEventHelper(e, false, cr.LinkKind.SELF); +} + +/////////////////////////////////////////////////////////////////////////////// + +function testOpenUrl() { + var lc = new cr.LinkController(localStrings); + lc.openUrls = mockControl.createFunctionMock(); + + lc.openUrls(new ObjectEqualsMatcher([URL1]), cr.LinkKind.SELF); + mockControl.$replayAll(); + + lc.openUrl(URL1, cr.LinkKind.SELF); + + mockControl.$verifyAll(); +} + +/////////////////////////////// OpenUrls ////////////////////////////////////// + +function testOpenUrlsTooFew() { + var lc = new cr.LinkController(localStrings); + + mockControl.$replayAll(); + + lc.openUrls([], cr.LinkKind.SELF); + + mockControl.$verifyAll(); +} + + +function testOpenUrlsTooMany() { + var lc = new cr.LinkController(localStrings); + lc.warningLimit = 9; + + var urls = new Array(lc.warningLimit + 1); + lc.window = mockWindow; + + lc.window.confirm('MSG10').$returns(false); + mockControl.$replayAll(); + + lc.openUrls(urls, cr.LinkKind.SELF); + + mockControl.$verifyAll(); +} + +function testOpenUrlsSelf() { + var lc = new cr.LinkController(localStrings); + lc.window = mockWindow; + + mockControl.$replayAll(); + + lc.openUrls([URL1], cr.LinkKind.SELF); + + mockControl.$verifyAll(); +} + +function testOpenUrlsForegroundTab() { + var lc = new cr.LinkController(localStrings); + lc.window = mockWindow; + + chrome.tabs.create(new ObjectEqualsMatcher({url: URL1, selected: true})); + chrome.tabs.create(new ObjectEqualsMatcher({ + url: 'http://www.google.com/#hash', + selected: false + })); + + mockControl.$replayAll(); + + lc.openUrls([URL1, URL2], cr.LinkKind.FOREGROUND_TAB); + + mockControl.$verifyAll(); +} + +function testOpenUrlsBackgroundTab() { + var lc = new cr.LinkController(localStrings); + lc.window = mockWindow; + + chrome.tabs.create(new ObjectEqualsMatcher({url: URL1, selected: false})); + chrome.tabs.create(new ObjectEqualsMatcher({ + url: 'http://www.google.com/#hash', + selected: false + })); + + mockControl.$replayAll(); + + lc.openUrls([URL1, URL2], cr.LinkKind.BACKGROUND_TAB); + + mockControl.$verifyAll(); +} + +function testOpenUrlsWindow() { + var lc = new cr.LinkController(localStrings); + lc.window = mockWindow; + + var callbackMatcher = new SaveArgumentMatcher; + chrome.windows.create(new ObjectEqualsMatcher({url: URL1, incognito: false}), + callbackMatcher); + chrome.tabs.create(new ObjectEqualsMatcher({ + url: 'http://www.google.com/#hash', + windowId: WINDOW_ID, + selected: false + })); + + mockControl.$replayAll(); + + lc.openUrls([URL1, URL2], cr.LinkKind.WINDOW); + callbackMatcher.arg({id: WINDOW_ID}); + + mockControl.$verifyAll(); +} + +function testOpenUrlsIncognito() { + var lc = new cr.LinkController(localStrings); + lc.window = mockWindow; + + var callbackMatcher = new SaveArgumentMatcher; + chrome.windows.create(new ObjectEqualsMatcher({url: URL1, incognito: true}), + callbackMatcher); + chrome.tabs.create(new ObjectEqualsMatcher({ + url: 'http://www.google.com/#hash', + windowId: WINDOW_ID, + selected: false + })); + + mockControl.$replayAll(); + + lc.openUrls([URL1, URL2], cr.LinkKind.INCOGNITO); + callbackMatcher.arg({id: WINDOW_ID}); + + mockControl.$verifyAll(); +} + +</script> +</body> +</html> diff --git a/chrome/browser/resources/bookmark_manager/js/cr/promise.js b/chrome/browser/resources/bookmark_manager/js/cr/promise.js index 3e8944f..a9d233a 100644 --- a/chrome/browser/resources/bookmark_manager/js/cr/promise.js +++ b/chrome/browser/resources/bookmark_manager/js/cr/promise.js @@ -15,16 +15,20 @@ cr.define('cr', function() { /** * Creates a future promise. - * @param {Function=} opt_callback Callback. + * @param {*=} opt_value The value to set the promise to. If set completes + * the promise immediately. * @constructor */ - function Promise(opt_callback) { + function Promise(opt_value) { /** * An array of the callbacks. * @type {!Array.<!Function>} * @private */ - this.callbacks_ = opt_callback ? [opt_callback] : []; + this.callbacks_ = []; + + if (arguments.length > 0) + this.value = opt_value; } Promise.prototype = { @@ -133,10 +137,11 @@ cr.define('cr', function() { /** * Creates a new future promise that is fulfilled when any of the promises are - * fulfilled. + * fulfilled. The value of the returned promise will be the value of the first + * fulfilled promise. * @param {...!Promise} var_args The promises used to build up the new * promise. - * @return {!Promise} The new promise that will be fulfilled when any of th + * @return {!Promise} The new promise that will be fulfilled when any of the * passed in promises are fulfilled. */ Promise.any = function(var_args) { @@ -151,6 +156,41 @@ cr.define('cr', function() { }; /** + * Creates a new future promise that is fulfilled when all of the promises are + * fulfilled. The value of the returned promise is an array of the values of + * the promises passed in. + * @param {...!Promise} var_args The promises used to build up the new + * promise. + * @return {!Promise} The promise that wraps all the promises in the array. + */ + Promise.all = function(var_args) { + var p = new Promise; + var args = Array.prototype.slice.call(arguments); + var count = args.length; + if (!count) { + p.value = []; + return p; + } + + function f(v) { + count--; + if (!count) { + p.value = args.map(function(argP) { + return argP.value; + }); + } + } + + // Do not use count here since count may be decremented in the call to + // addListener if the promise is already done. + for (var i = 0; i < args.length; i++) { + args[i].addListener(f); + } + + return p; + }; + + /** * Wraps an event in a future promise. * @param {!EventTarget} target The object that dispatches the event. * @param {string} type The type of the event. diff --git a/chrome/browser/resources/bookmark_manager/js/cr/promise_test.html b/chrome/browser/resources/bookmark_manager/js/cr/promise_test.html index 19e3d1a..5c9ae1d 100644 --- a/chrome/browser/resources/bookmark_manager/js/cr/promise_test.html +++ b/chrome/browser/resources/bookmark_manager/js/cr/promise_test.html @@ -107,9 +107,9 @@ function testCallbacks4() { calls2++; assertEquals(V, v); } - var p = new Promise(f1); + var p = new Promise(V); + p.addListener(f1); p.addListener(f2); - p.value = V; assertEquals(1, calls1); assertEquals(1, calls2); } @@ -195,6 +195,32 @@ function testAny() { assertEquals(2, any.value); } +function testAll() { + var p1 = new Promise; + var p2 = new Promise; + var p3 = new Promise; + + var pAll = Promise.all(p1, p2, p3); + p1.value = 1; + p2.value = 2; + p3.value = 3; + assertArrayEquals([1, 2, 3], pAll.value); +} + +function testAllEmpty() { + var pAll = Promise.all(); + assertArrayEquals([], pAll.value); +} + +function testAllAlreadyDone() { + var p1 = new Promise(1); + var p2 = new Promise(2); + var p3 = new Promise(3); + + var pAll = Promise.all(p1, p2, p3); + assertArrayEquals([1, 2, 3], pAll.value); +} + function testEvent() { var p = Promise.event(document.body, 'foo'); var e = new cr.Event('foo'); diff --git a/chrome/browser/resources/bookmark_manager/main.html b/chrome/browser/resources/bookmark_manager/main.html index 9a12c77..ad947b0 100644 --- a/chrome/browser/resources/bookmark_manager/main.html +++ b/chrome/browser/resources/bookmark_manager/main.html @@ -21,6 +21,7 @@ found in the LICENSE file. <script src="js/cr.js"></script> <script src="js/cr/event.js"></script> <script src="js/cr/eventtarget.js"></script> +<script src="js/cr/linkcontroller.js"></script> <script src="js/cr/promise.js"></script> <script src="js/cr/ui.js"></script> <script src="js/cr/ui/listselectionmodel.js"></script> @@ -86,6 +87,7 @@ const BookmarkList = bmm.BookmarkList; const BookmarkTree = bmm.BookmarkTree; const ListItem = cr.ui.ListItem; const TreeItem = cr.ui.TreeItem; +const LinkKind = cr.LinkKind; /** * The id of the bookmark root. @@ -236,16 +238,16 @@ window.onhashchange = function(e) { } }; -// Activate is handled by the activate-command. +// Activate is handled by the open-in-same-window-command. list.addEventListener('dblclick', function(e) { if (e.button == 0) - $('activate-command').execute(); + $('open-in-same-window-command').execute(); }); // The list dispatches an event when the user clicks on the URL or the Show in // folder part. list.addEventListener('urlClicked', function(e) { - openUrls([e.url], e.kind); + getLinkController().openUrlFromEvent(e.url, e.originalEvent); }); /** @@ -923,12 +925,10 @@ dnd.init(); <!-- open * are handled in canExecute handler --> <command id="open-in-new-tab-command"></command> +<command id="open-in-background-tab-command"></command> <command id="open-in-new-window-command"></command> <command id="open-incognito-window-command"></command> - -<!-- Activate command is used by enter and dblclick and it eaither navigates to - a folder or execute one of the open commands. --> -<command id="activate-command"></command> +<command id="open-in-same-window-command"></command> <!-- TODO(arv): I think the commands might be better created in code? --> @@ -977,6 +977,7 @@ const Command = cr.ui.Command; const CommandBinding = cr.ui.CommandBinding; const Menu = cr.ui.Menu; const MenuButton = cr.ui.MenuButton; +const Promise = cr.Promise; cr.ui.decorate('menu', Menu); cr.ui.decorate('button[menu]', MenuButton); @@ -1146,12 +1147,13 @@ list.addEventListener('canExecute', function(e) { break; case 'open-in-new-tab-command': + case 'open-in-background-tab-command': case 'open-in-new-window-command': case 'open-incognito-window-command': updateOpenCommands(e, command); break; - case 'activate-command': + case 'open-in-same-window-command': e.canExecute = hasSelected(); break; } @@ -1205,6 +1207,7 @@ tree.addEventListener('canExecute', function(e) { break; case 'open-in-new-tab-command': + case 'open-in-background-tab-command': case 'open-in-new-window-command': case 'open-incognito-window-command': updateOpenCommands(e, command); @@ -1221,7 +1224,7 @@ function updateCommandsBasedOnSelection(e) { // Paste only needs to updated when the tree selection changes. var commandNames = ['copy', 'cut', 'delete', 'rename-folder', 'edit', 'add-new-bookmark', 'new-folder', 'open-in-new-tab', - 'open-in-new-window', 'open-incognito-window', 'activate']; + 'open-in-new-window', 'open-incognito-window', 'open-in-same-window']; if (e.target == tree) { commandNames.push('paste', 'show-in-folder', 'sort'); @@ -1313,49 +1316,15 @@ function showInFolder() { navigateTo(parentId); } +var linkController; + /** - * Opens URLs in new tab, window or incognito mode. - * @param {!Array.<string>} urls The URLs to open. - * @param {string} kind The kind is either 'tab', 'window', or 'incognito'. + * @return {!cr.LinkController} The link controller used to open links based on + * user clicks and keyboard actions. */ -function openUrls(urls, kind) { - if (urls.length < 1) - return; - - if (urls.length > 15) { - if (!confirm(localStrings.getStringF('should_open_all', urls.length))) - return; - } - - // Fix '#124' URLs since open those in a new window does not work. We prepend - // the base URL when we encounter those. - var base = window.location.href.split('#')[0]; - urls = urls.map(function(url) { - return url[0] == '#' ? base + url : url; - }); - - var incognito = kind == 'incognito'; - if (kind == 'window' || incognito) { - chrome.windows.create({ - url: urls[0], - incognito: incognito - }, function(window) { - urls.forEach(function(url, i) { - if (i > 0) - chrome.tabs.create({ - url: url, - windowId: window.id, - selected: false - }); - }); - }); - } else if (kind == 'tab') { - urls.forEach(function(url, i) { - chrome.tabs.create({url: url, selected: !i}); - }); - } else { - window.location.href = urls[0]; - } +function getLinkController() { + return linkController || + (linkController = new cr.LinkController(localStrings)); } /** @@ -1386,28 +1355,14 @@ function getSelectedBookmarkIds() { /** * Opens the selected bookmarks. + * @param {LinkKind} kind The kind of link we want to open. */ function openBookmarks(kind) { // If we have selected any folders we need to find all items recursively. - // We can do several async calls to getChildren but instead we do a single - // call to getTree and only add the subtrees of the selected items. + // We use multiple async calls to getSubtree instead of getting the whole + // tree since we would like to minimize the amount of data sent. var urls = []; - var idMap = {}; - - // Traverses the tree until it finds a node tree that should be added. Then - // we switch over to use addNodes. We could merge these two functions into - // one but that would make the code less readable. - function traverseNodes(node) { - // This is not using the iterator since it uses breadth first search. - if (node.id in idMap) { - addNodes(node); - } else if (node.children) { - for (var i = 0; i < node.children.length; i++) { - traverseNodes(node.children[i]); - } - } - } // Adds the node and all the descendants function addNodes(node) { @@ -1421,14 +1376,23 @@ function openBookmarks(kind) { var nodes = getSelectedBookmarkNodes(); - // Create a map for simpler lookup later. - nodes.forEach(function(node) { - idMap[node.id] = true; + // Get a future promise for every selected item. + var promises = nodes.map(function(node) { + if (bmm.isFolder(node)) + return bmm.loadSubtree(node.id); + // Not a folder so we already have all the data we need. + return new Promise(node.url); }); - var p = bmm.loadTree(); - p.addListener(function(node) { - traverseNodes(node); - openUrls(urls, kind); + + var p = Promise.all.apply(null, promises); + p.addListener(function(values) { + values.forEach(function(v) { + if (typeof v == 'string') + urls.push(v); + else + addNodes(v); + }); + getLinkController().openUrls(urls, kind); }); } @@ -1441,7 +1405,7 @@ function openItem() { if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0])) { navigateTo(bookmarkNodes[0].id); } else { - openBookmarks('tab'); + openBookmarks(LinkKind.FOREGROUND_TAB); } } @@ -1506,13 +1470,16 @@ function handleCommand(e) { showInFolder(); break; case 'open-in-new-tab-command': - openBookmarks('tab'); + openBookmarks(LinkKind.FOREGROUND_TAB); + break; + case 'open-in-background-tab-command': + openBookmarks(LinkKind.BACKGROUND_TAB); break; case 'open-in-new-window-command': - openBookmarks('window'); + openBookmarks(LinkKind.WINDOW); break; case 'open-incognito-window-command': - openBookmarks('incognito'); + openBookmarks(LinkKind.INCOGNITO); break; case 'delete-command': deleteBookmarks(); @@ -1539,7 +1506,7 @@ function handleCommand(e) { case 'add-new-bookmark-command': addPage(); break; - case 'activate-command': + case 'open-in-same-window-command': openItem(); break; } @@ -1547,11 +1514,19 @@ function handleCommand(e) { // Delete on all platforms. On Mac we also allow Meta+Backspace. $('delete-command').shortcut = 'U+007F' + - (cr.isMac ? ' U+0008 U+0008-meta' : ''); + (cr.isMac ? ' U+0008 Meta-U+0008' : ''); // Enter on all platforms. Mac we also support space and Command-Down !?! -$('activate-command').shortcut = 'Enter' + - (cr.isMac ? ' U+0020 Down-meta' : ''); +$('open-in-same-window-command').shortcut = 'Enter' + + (cr.isMac ? ' U+0020 Meta-Down' : ''); + +// +$('open-in-new-window-command').shortcut = 'Shift-Enter'; +$('open-in-background-tab-command').shortcut = cr.isMac ? 'Meta-Enter' : + 'Ctrl-Enter'; +$('open-in-new-tab-command').shortcut = cr.isMac ? 'Shift-Meta-Enter' : + 'Shift-Ctrl-Enter'; + if (!cr.isMac) { $('rename-folder-command').shortcut = 'F2'; $('edit-command').shortcut = 'F2'; diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index 4601b8b..b317623 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -3161,6 +3161,7 @@ 'files': [ 'browser/resources/bookmark_manager/js/cr/event.js', 'browser/resources/bookmark_manager/js/cr/eventtarget.js', + 'browser/resources/bookmark_manager/js/cr/linkcontroller.js', 'browser/resources/bookmark_manager/js/cr/promise.js', 'browser/resources/bookmark_manager/js/cr/ui.js', ] |