Merge fx-team to m-c.

This commit is contained in:
Ryan VanderMeulen 2013-11-22 15:23:22 -05:00
commit dae949d346
53 changed files with 1316 additions and 1147 deletions

View File

@ -1030,7 +1030,7 @@ SocialToolbar = {
if (!toolbarButton) {
toolbarButton = document.createElement("toolbarbutton");
toolbarButton.setAttribute("type", "badged");
toolbarButton.classList.add("toolbarbutton-1");
toolbarButton.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional");
toolbarButton.setAttribute("id", toolbarButtonId);
toolbarButton.setAttribute("notificationFrameId", notificationFrameId);
toolbarButton.addEventListener("mousedown", function (event) {
@ -1479,7 +1479,7 @@ SocialStatus = {
return null;
let palette = document.getElementById("navigator-toolbox").palette;
let button = document.createElement("toolbarbutton");
button.setAttribute("class", "toolbarbutton-1 social-status-button");
button.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional social-status-button");
button.setAttribute("type", "badged");
button.setAttribute("removable", "true");
button.setAttribute("image", provider.iconURL);
@ -1763,7 +1763,7 @@ SocialMarks = {
let palette = document.getElementById("navigator-toolbox").palette;
let button = document.createElement("toolbarbutton");
button.setAttribute("type", "socialmark");
button.setAttribute("class", "toolbarbutton-1 social-mark-button");
button.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional social-mark-button");
button.style.listStyleImage = "url(" + provider.iconURL + ")";
button.setAttribute("origin", provider.origin);
button.setAttribute("id", this._toolbarHelper.idFromOrgin(provider.origin));

View File

@ -860,6 +860,84 @@ chatbox:-moz-full-screen-ancestor > .chat-titlebar {
-moz-image-region: auto;
}
/* Customize mode */
#tab-view-deck {
transition-property: padding;
transition-duration: 150ms;
transition-timing-function: ease-out;
}
#tab-view-deck[fastcustomizeanimation] {
transition-duration: 1ms;
transition-timing-function: linear;
}
#PanelUI-contents > .panel-customization-placeholder > .panel-customization-placeholder-child {
list-style-image: none;
}
#customization-panelHolder {
overflow-y: hidden;
}
#customization-panelWrapper,
#customization-panelWrapper > .panel-arrowcontent {
-moz-box-flex: 1;
}
#customization-panelWrapper > .panel-arrowcontent {
padding: 0 !important;
}
#customization-panelHolder > #PanelUI-mainView {
display: flex;
flex-direction: column;
/* Hack alert - by manually setting the preferred height to 0, we convince
#PanelUI-mainView to shrink when the window gets smaller in customization
mode. Not sure why that is - might have to do with our intermingling of
XUL flex, and CSS3 Flexbox. */
height: 0;
}
#customization-panelHolder > #PanelUI-mainView > #PanelUI-contents-scroller {
display: flex;
flex: auto;
flex-direction: column;
}
#customization-panel-container {
overflow-y: auto;
}
toolbarpaletteitem[dragover] {
border-left-color: transparent;
border-right-color: transparent;
}
#customization-palette:not([hidden]) {
display: block;
overflow: auto;
min-height: 3em;
}
toolbarpaletteitem[place="palette"] {
width: 110px;
height: 94px;
overflow: hidden;
display: inline-block;
}
toolbarpaletteitem[place="palette"][hidden] {
display: none;
}
#customization-palette .toolbarpaletteitem-box {
-moz-box-pack: center;
-moz-box-flex: 1;
width: 110px;
max-width: 110px;
}
/* UI Tour */
@keyframes uitour-wobble {

View File

@ -1917,6 +1917,7 @@ let CustomizableUIInternal = {
let currentPlacements = gPlacements.get(areaId);
// We're excluding all of the placement IDs for items that do not exist,
// and items that have removable="false",
// because we don't want to consider them when determining if we're
// in the default state. This way, if an add-on introduces a widget
// and is then uninstalled, the leftover placement doesn't cause us to
@ -1924,20 +1925,24 @@ let CustomizableUIInternal = {
let buildAreaNodes = gBuildAreas.get(areaId);
if (buildAreaNodes && buildAreaNodes.size) {
let container = [...buildAreaNodes][0];
let removableOrDefault = (itemNodeOrItem) => {
let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem;
let isRemovable = this.isWidgetRemovable(itemNodeOrItem);
let isInDefault = defaultPlacements.indexOf(item) != -1;
return isRemovable || isInDefault;
};
// Toolbars have a currentSet property which also deals correctly with overflown
// widgets (if any) - use that instead:
if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
currentPlacements = container.currentSet.split(',');
currentPlacements = currentPlacements.filter(removableOrDefault);
} else {
// Clone the array so we don't modify the actual placements...
currentPlacements = [...currentPlacements];
// Loop backwards through the placements so we can easily remove items:
let itemIndex = currentPlacements.length;
while (itemIndex--) {
if (!container.querySelector(idToSelector(currentPlacements[itemIndex]))) {
currentPlacements.splice(itemIndex, 1);
}
}
currentPlacements = currentPlacements.filter((item) => {
let itemNode = container.querySelector(idToSelector(item));
return itemNode && removableOrDefault(itemNode || item);
});
}
}
LOG("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") +

View File

@ -657,6 +657,9 @@ CustomizeMode.prototype = {
reset: function() {
this.resetting = true;
// Disable the reset button temporarily while resetting:
let btn = this.document.getElementById("customization-reset-button");
btn.disabled = true;
return Task.spawn(function() {
this._removePanelCustomizationPlaceholders();
yield this.depopulatePalette();

View File

@ -37,6 +37,7 @@ skip-if = true
skip-if = os == "mac"
[browser_938980_navbar_collapsed.js]
[browser_938995_indefaultstate_nonremovable.js]
[browser_940946_removable_from_navbar_customizemode.js]
[browser_941083_invalidate_wrapper_cache_createWidget.js]
[browser_panel_toggle.js]

View File

@ -0,0 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const kWidgetId = "test-non-removable-widget";
let gTests = [
{
desc: "Adding non-removable items to a toolbar or the panel shouldn't change inDefaultState",
run: function() {
let navbar = document.getElementById("nav-bar");
ok(CustomizableUI.inDefaultState, "Should start in default state");
CustomizableUI.createWidget({id: kWidgetId, removable: false, label: "Test"});
CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR);
ok(CustomizableUI.inDefaultState, "Should still be in default state after navbar addition");
CustomizableUI.destroyWidget(kWidgetId);
CustomizableUI.createWidget({id: kWidgetId, removable: false, label: "Test"});
CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_PANEL);
ok(CustomizableUI.inDefaultState, "Should still be in default state after panel addition");
CustomizableUI.destroyWidget(kWidgetId);
ok(CustomizableUI.inDefaultState, "Should be in default state after destroying both widgets");
},
teardown: null
},
];
function test() {
waitForExplicitFinish();
runTests(gTests);
}

View File

@ -100,7 +100,6 @@ exports.testBasic = function(options) {
'Saluton', 'Sawubona'
],
unassigned: [ ],
tooltipState: 'true:importantFieldFlag',
args: {
command: { name: 'tsslow' },
hello: {
@ -123,7 +122,6 @@ exports.testBasic = function(options) {
status: 'ERROR',
predictions: [ 'Shalom', 'Saluton', 'Sawubona', 'Namasté' ],
unassigned: [ ],
tooltipState: 'true:importantFieldFlag',
args: {
command: { name: 'tsslow' },
hello: {
@ -147,7 +145,6 @@ exports.testBasic = function(options) {
status: 'VALID',
predictions: [ 'Shalom' ],
unassigned: [ ],
tooltipState: 'true:importantFieldFlag',
args: {
command: { name: 'tsslow' },
hello: {

View File

@ -290,8 +290,6 @@ exports.testNodes = function(options) {
cursor: 16,
current: 'nodes',
status: 'ERROR',
outputState: 'false:default',
tooltipState: 'true:isError',
args: {
command: { name: 'tse' },
node: {
@ -327,8 +325,6 @@ exports.testNodes = function(options) {
cursor: 17,
current: 'nodes2',
status: 'ERROR',
outputState: 'false:default',
tooltipState: 'false:default',
args: {
command: { name: 'tse' },
node: {

View File

@ -124,7 +124,7 @@ BrowserDebuggerProcess.prototype = {
}
// Create a new chrome debugging profile.
this._dbgProfile = profileService.createProfile(null, null, profileName);
this._dbgProfile = profileService.createProfile(null, profileName);
profileService.flush();
dumpn("Finished creating the chrome debugger user profile.");

View File

@ -31,60 +31,44 @@ var APZCObserver = {
os.addObserver(this, "apzc-handle-pan-begin", false);
os.addObserver(this, "apzc-handle-pan-end", false);
// Fired by ContentAreaObserver
window.addEventListener("SizeChanged", this, true);
Elements.tabList.addEventListener("TabSelect", this, true);
Elements.tabList.addEventListener("TabOpen", this, true);
Elements.tabList.addEventListener("TabClose", this, true);
},
shutdown: function shutdown() {
if (!this._enabled) {
return;
}
let os = Services.obs;
os.removeObserver(this, "apzc-handle-pan-begin");
os.removeObserver(this, "apzc-handle-pan-end");
window.removeEventListener("SizeChanged", this, true);
Elements.tabList.removeEventListener("TabSelect", this, true);
Elements.tabList.removeEventListener("TabOpen", this, true);
Elements.tabList.removeEventListener("TabClose", this, true);
},
handleEvent: function APZC_handleEvent(aEvent) {
switch (aEvent.type) {
case "SizeChanged":
case 'TabSelect':
this._resetDisplayPort();
break;
case 'pageshow':
if (aEvent.target != Browser.selectedBrowser.contentDocument) {
break;
}
// intentional fall through
case 'TabSelect': {
// Start off with something reasonable. The apzc will handle these
// calculations once scrolling starts.
let doc = Browser.selectedBrowser.contentDocument.documentElement;
// While running tests, sometimes this can be null. If we don't have a
// root document, there's no point in setting a scrollable display port.
if (!doc) {
break;
}
let win = Browser.selectedBrowser.contentWindow;
let factor = 0.2;
let portX = 0;
let portY = 0;
let portWidth = ContentAreaObserver.width;
let portHeight = ContentAreaObserver.height;
if (portWidth < doc.scrollWidth) {
portWidth += ContentAreaObserver.width * factor;
if (portWidth > doc.scrollWidth) {
portWidth = doc.scrollWidth;
}
}
if (portHeight < doc.scrollHeight) {
portHeight += ContentAreaObserver.height * factor;
if (portHeight > doc.scrollHeight) {
portHeight = doc.scrollHeight;
}
}
if (win.scrollX > 0) {
portX -= ContentAreaObserver.width * factor;
}
if (win.scrollY > 0) {
portY -= ContentAreaObserver.height * factor;
}
let cwu = Browser.selectedBrowser.contentWindow
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
cwu.setDisplayPortForElement(portX, portY,
portWidth, portHeight,
Browser.selectedBrowser.contentDocument.documentElement);
this._resetDisplayPort();
break;
}
case 'TabOpen': {
let browser = aEvent.originalTarget.linkedBrowser;
browser.addEventListener("pageshow", this, true);
@ -101,19 +85,6 @@ var APZCObserver = {
}
},
shutdown: function shutdown() {
if (!this._enabled) {
return;
}
Elements.tabList.removeEventListener("TabSelect", this, true);
Elements.tabList.removeEventListener("TabOpen", this, true);
Elements.tabList.removeEventListener("TabClose", this, true);
let os = Services.obs;
os.removeObserver(this, "apzc-handle-pan-begin");
os.removeObserver(this, "apzc-handle-pan-end");
},
observe: function ao_observe(aSubject, aTopic, aData) {
if (aTopic == "apzc-handle-pan-begin") {
// When we're panning, hide the main scrollbars by setting imprecise
@ -140,5 +111,47 @@ var APZCObserver = {
break;
}
}
},
_resetDisplayPort: function () {
// Start off with something reasonable. The apzc will handle these
// calculations once scrolling starts.
let doc = Browser.selectedBrowser.contentDocument.documentElement;
// While running tests, sometimes this can be null. If we don't have a
// root document, there's no point in setting a scrollable display port.
if (!doc) {
return;
}
let win = Browser.selectedBrowser.contentWindow;
let factor = 0.2;
let portX = 0;
let portY = 0;
let portWidth = ContentAreaObserver.width;
let portHeight = ContentAreaObserver.height;
if (portWidth < doc.scrollWidth) {
portWidth += ContentAreaObserver.width * factor;
if (portWidth > doc.scrollWidth) {
portWidth = doc.scrollWidth;
}
}
if (portHeight < doc.scrollHeight) {
portHeight += ContentAreaObserver.height * factor;
if (portHeight > doc.scrollHeight) {
portHeight = doc.scrollHeight;
}
}
if (win.scrollX > 0) {
portX -= ContentAreaObserver.width * factor;
}
if (win.scrollY > 0) {
portY -= ContentAreaObserver.height * factor;
}
let cwu = Browser.selectedBrowser.contentWindow
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
cwu.setDisplayPortForElement(portX, portY,
portWidth, portHeight,
Browser.selectedBrowser.contentDocument.documentElement);
}
};

View File

@ -66,6 +66,10 @@
this.classList.remove("flyoutpanel-slide-in");
this.setAttribute("isSlidingOut", true);
this.removeAttribute("isSlidIn");
let event = document.createEvent("Events");
event.initEvent("MozFlyoutPanelHiding", true, false);
this.dispatchEvent(event);
]]>
</body>
</method>

View File

@ -454,10 +454,14 @@ var BrowserUI = {
openLinkInNewTab: function (aURI, aBringFront, aOwner) {
ContextUI.peekTabs(aBringFront ? kForegroundTabAnimationDelay
: kBackgroundTabAnimationDelay);
let tab = Browser.addTab(aURI, aBringFront, aOwner, {
referrerURI: aOwner.browser.documentURI,
charset: aOwner.browser.characterSet,
});
let params = null;
if (aOwner) {
params = {
referrerURI: aOwner.browser.documentURI,
charset: aOwner.browser.characterSet,
};
}
let tab = Browser.addTab(aURI, aBringFront, aOwner, params);
Elements.tabList.strip.ensureElementIsVisible(tab.chromeTab);
return tab;
},

View File

@ -39,6 +39,7 @@ let AboutFlyoutPanel = {
}
window.addEventListener('MozFlyoutPanelShowing', this, false);
window.addEventListener('MozFlyoutPanelHiding', this, false);
#if MOZ_UPDATE_CHANNEL != release
let defaults = Services.prefs.getDefaultBranch("");
@ -59,9 +60,13 @@ let AboutFlyoutPanel = {
switch (aEvent.type) {
case 'MozFlyoutPanelShowing':
#ifdef MOZ_UPDATER
onUnload();
this.appUpdater = new appUpdater();
gAppUpdater = this.appUpdater;
#endif
break;
case 'MozFlyoutPanelHiding':
#ifdef MOZ_UPDATER
onUnload();
#endif
break;
}
@ -83,7 +88,7 @@ function onUnload(aEvent) {
// Safe to call even when there isn't a download in progress.
gAppUpdater.removeDownloadListener();
gAppUpdater = null;
AboutFlyout.appUpdater = null;
AboutFlyoutPanel.appUpdater = null;
}
function appUpdater()

View File

@ -19,9 +19,8 @@
.devtools-toolbar {
-moz-appearance: none;
padding: 4px 3px;
color: hsl(210,30%,85%);
background-color: #343c45;
border-bottom: 1px solid #060a0d;
border-bottom-width: 1px;
border-bottom-style: solid;
}
.devtools-menulist,

View File

@ -13,7 +13,6 @@
.devtools-monospace {
font-family: Menlo, monospace;
font-size: 108%;
}
/* Toolbar and Toolbar items */
@ -21,9 +20,8 @@
.devtools-toolbar {
-moz-appearance: none;
padding: 4px 3px;
color: hsl(210,30%,85%);
background-color: #343c45;
border-bottom-width: 1px;
border-bottom-style: solid;
}
.devtools-menulist,

View File

@ -3,18 +3,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Customization mode */
#tab-view-deck {
transition-property: padding;
transition-duration: 150ms;
transition-timing-function: ease-out;
}
#tab-view-deck[fastcustomizeanimation] {
transition-duration: 1ms;
transition-timing-function: linear;
}
#nav-bar[customize-entered] > #nav-bar-customization-target {
margin: 1px 3px;
}
@ -34,35 +22,6 @@
outline-offset: -5px;
}
#PanelUI-contents > .panel-customization-placeholder > .panel-customization-placeholder-child {
list-style-image: none;
}
#customization-panelHolder {
overflow-y: hidden;
}
#customization-panelWrapper,
#customization-panelWrapper > .panel-arrowcontent {
-moz-box-flex: 1;
}
#customization-panelHolder > #PanelUI-mainView {
display: flex;
flex-direction: column;
/* Hack alert - by manually setting the preferred height to 0, we convince
#PanelUI-mainView to shrink when the window gets smaller in customization
mode. Not sure why that is - might have to do with our intermingling of
XUL flex, and CSS3 Flexbox. */
height: 0;
}
#customization-panelHolder > #PanelUI-mainView > #PanelUI-contents-scroller {
display: flex;
flex: auto;
flex-direction: column;
}
#main-window[customize-entered] .customization-target {
min-width: 100px;
padding-left: 10px;
@ -96,11 +55,6 @@
background-repeat: no-repeat, no-repeat, repeat, repeat, no-repeat;
background-size: auto 12px, 12px 100%, auto, auto, auto;
background-attachment: scroll, scroll, fixed, fixed, scroll;
overflow-y: auto;
}
#customization-panelWrapper > .panel-arrowcontent {
padding: 0 !important;
}
toolbarpaletteitem {
@ -146,41 +100,15 @@ toolbarpaletteitem[place="toolbar"] {
margin-left: 0;
}
toolbarpaletteitem[dragover] {
border-left-color: transparent;
border-right-color: transparent;
}
#customization-palette:not([hidden]) {
display: block;
overflow: auto;
min-height: 3em;
margin-bottom: 25px;
}
toolbarpaletteitem[place="palette"] {
width: 110px;
height: 94px;
overflow: hidden;
display: inline-block;
}
toolbarpaletteitem[place="palette"][hidden] {
display: none;
}
#customization-palette > #wrapper-edit-controls,
#customization-palette > #wrapper-zoom-controls {
width: 225px;
}
#customization-palette .toolbarpaletteitem-box {
-moz-box-pack: center;
-moz-box-flex: 1;
width: 110px;
max-width: 110px;
}
#wrapper-edit-controls[place="palette"] > .toolbarpaletteitem-box,
#wrapper-zoom-controls[place="palette"] > .toolbarpaletteitem-box {
width: 225px;

View File

@ -135,9 +135,8 @@
.theme-toolbar,
.devtools-toolbar { /* General toolbar styling */
color: hsl(210,30%,85%);
background-image: url(background-noise-toolbar.png), linear-gradient(#3e4750, #3e4750);
background-color: #343c45;
border-color: #060a0d;
box-shadow: 0 1px 0 hsla(204,45%,98%,.05) inset, -1px 0 0 hsla(204,45%,98%,.05) inset, 0 -1px 0 hsla(204,45%,98%,.05) inset;
}
.theme-fg-contrast { /* To be used for text on theme-bg-contrast */

View File

@ -138,9 +138,8 @@
.theme-toolbar,
.devtools-toolbar { /* General toolbar styling */
color: hsl(210,30%,85%);
background-image: url(background-noise-toolbar.png), linear-gradient(#3e4750, #3e4750);
background-color: #343c45;
border-color: #060a0d;
box-shadow: 0 1px 0 hsla(204,45%,98%,.05) inset, -1px 0 0 hsla(204,45%,98%,.05) inset, 0 -1px 0 hsla(204,45%,98%,.05) inset;
}
.ruleview-colorswatch,

View File

@ -8,7 +8,7 @@
%define glassActiveBorderColor rgb(37, 44, 51)
%define glassInactiveBorderColor rgb(102, 102, 102)
%define nested-buttons #zoom-out-button, #zoom-in-button, #cut-button, #copy-button, #paste-button
%define nestedButtons #zoom-out-button, #zoom-in-button, #cut-button, #copy-button, #paste-button
%include downloads/indicator-aero.css
@ -227,10 +227,10 @@
#TabsToolbar > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme),
#toolbar-menubar > toolbarpaletteitem > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme),
#toolbar-menubar > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme),
#toolbar-menubar > toolbarpaletteitem > toolbaritem > :-moz-any(@nested-buttons@):not(:-moz-lwtheme),
#toolbar-menubar > toolbaritem > :-moz-any(@nested-buttons@):not(:-moz-lwtheme),
#TabsToolbar > toolbarpaletteitem > toolbaritem > :-moz-any(@nested-buttons@):not(:-moz-lwtheme),
#TabsToolbar > toolbaritem > :-moz-any(@nested-buttons@):not(:-moz-lwtheme),
#toolbar-menubar > toolbarpaletteitem > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme),
#toolbar-menubar > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme),
#TabsToolbar > toolbarpaletteitem > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme),
#TabsToolbar > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme),
#toolbar-menubar > toolbarpaletteitem > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme),
#toolbar-menubar > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme),
#TabsToolbar > toolbarpaletteitem > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme),

View File

@ -18,9 +18,8 @@
.devtools-toolbar {
-moz-appearance: none;
padding: 4px 3px;
color: hsl(210,30%,85%);
background-color: #343c45;
border-bottom: 1px solid #060a0d;
border-bottom-width: 1px;
border-bottom-style: solid;
}
.devtools-menulist,

View File

@ -6,6 +6,7 @@
package org.mozilla.gecko;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.favicons.Favicons;
@ -667,7 +668,7 @@ abstract public class BrowserApp extends GeckoApp
}
mHomePagerContainer.setPadding(0, 0, 0, 0);
if (mBrowserToolbar != null) {
mBrowserToolbar.scrollTo(0, 0);
ViewHelper.setTranslationY(mBrowserToolbar, 0);
}
}
@ -928,7 +929,8 @@ abstract public class BrowserApp extends GeckoApp
final int marginTop = Math.round(aMetrics.marginTop);
ThreadUtils.postToUiThread(new Runnable() {
public void run() {
toolbarLayout.scrollTo(0, toolbarLayout.getHeight() - marginTop);
ViewHelper.setTranslationY(toolbarLayout, marginTop - toolbarLayout.getHeight());
if (mDoorHangerPopup.isShowing()) {
mDoorHangerPopup.updatePopup();
}

View File

@ -48,6 +48,9 @@ public class TopSitesGridItemView extends RelativeLayout {
// Pinned state.
private boolean mIsPinned = false;
// Dirty state.
private boolean mIsDirty = false;
// Empty state.
private boolean mIsEmpty = true;
private int mLoadId = Favicons.NOT_LOADING;
@ -146,6 +149,10 @@ public class TopSitesGridItemView extends RelativeLayout {
displayThumbnail(R.drawable.top_site_add);
}
public void markAsDirty() {
mIsDirty = true;
}
/**
* Updates the title, URL, and pinned state of this view.
*
@ -184,6 +191,12 @@ public class TopSitesGridItemView extends RelativeLayout {
changed = true;
}
// The dirty state forces the state update to return true
// so that the adapter loads favicons once the thumbnails
// are loaded in TopSitesPage/TopSitesGridAdapter.
changed = (changed || mIsDirty);
mIsDirty = false;
return changed;
}

View File

@ -558,6 +558,17 @@ public class TopSitesPage extends HomeFragment {
*/
public void updateThumbnails(Map<String, Bitmap> thumbnails) {
mThumbnails = thumbnails;
final int count = mGrid.getChildCount();
for (int i = 0; i < count; i++) {
TopSitesGridItemView gridItem = (TopSitesGridItemView) mGrid.getChildAt(i);
// All the views have already got their initial state at this point.
// This will force each view to load favicons for the missing
// thumbnails if necessary.
gridItem.markAsDirty();
}
notifyDataSetChanged();
}
@ -578,7 +589,11 @@ public class TopSitesPage extends HomeFragment {
// If there is no url, then show "add bookmark".
if (TextUtils.isEmpty(url)) {
view.blankOut();
// Wait until thumbnails are loaded before showing anything.
if (mThumbnails != null) {
view.blankOut();
}
return;
}
@ -589,8 +604,9 @@ public class TopSitesPage extends HomeFragment {
// fetches.
final boolean updated = view.updateState(title, url, pinned, thumbnail);
// If we sent in a thumbnail, we're done now.
if (thumbnail != null) {
// If thumbnails are still being loaded, don't try to load favicons
// just yet. If we sent in a thumbnail, we're done now.
if (mThumbnails == null || thumbnail != null) {
return;
}

View File

@ -289,12 +289,13 @@ gbjar.sources += [
'toolbar/AutocompleteHandler.java',
'toolbar/BackButton.java',
'toolbar/BrowserToolbar.java',
'toolbar/BrowserToolbarBackground.java',
'toolbar/CanvasDelegate.java',
'toolbar/ForwardButton.java',
'toolbar/PageActionLayout.java',
'toolbar/ShapedButton.java',
'toolbar/TabCounter.java',
'toolbar/ToolbarEditLayout.java',
'toolbar/ToolbarEditText.java',
'TouchEventInterceptor.java',
'updater/UpdateService.java',
'updater/UpdateServiceHelper.java',
@ -899,6 +900,7 @@ ANDROID_RESFILES += [
'resources/layout/tabs_panel_header.xml',
'resources/layout/tabs_panel_indicator.xml',
'resources/layout/text_selection_handles.xml',
'resources/layout/toolbar_edit_layout.xml',
'resources/layout/top_sites_grid_item_view.xml',
'resources/layout/two_line_page_row.xml',
'resources/layout/validation_message.xml',

View File

@ -6,11 +6,6 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto">
<org.mozilla.gecko.toolbar.BrowserToolbarBackground android:id="@+id/url_bar_bg"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/url_bar_bg"/>
<org.mozilla.gecko.toolbar.ShapedButton android:id="@+id/tabs"
style="@style/UrlBar.ImageButton"
android:layout_width="84dip"
@ -65,43 +60,14 @@
android:contentDescription="@string/back"
android:background="@drawable/url_bar_nav_button"/>
<LinearLayout android:id="@+id/url_edit_container"
<org.mozilla.gecko.toolbar.ToolbarEditLayout android:id="@+id/edit_layout"
style="@style/UrlBar.Button"
android:paddingLeft="12dp"
android:paddingRight="4dp"
android:visibility="gone"
android:orientation="horizontal"
android:layout_toRightOf="@id/back"
android:layout_toLeftOf="@id/menu_items">
<view class="org.mozilla.gecko.CustomEditText"
android:id="@+id/url_edit_text"
style="@style/UrlBar.Button"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1.0"
android:hint="@string/url_bar_default_text"
android:textColor="@color/url_bar_title"
android:textColorHint="@color/url_bar_title_hint"
android:textColorHighlight="@color/url_bar_text_highlight"
android:textSelectHandle="@drawable/handle_middle"
android:textSelectHandleLeft="@drawable/handle_start"
android:textSelectHandleRight="@drawable/handle_end"
android:textCursorDrawable="@null"
android:inputType="textUri|textNoSuggestions"
android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen"
android:selectAllOnFocus="true"
android:singleLine="true"
android:gravity="center_vertical|left"
gecko:autoUpdateTheme="false"/>
<ImageButton android:id="@+id/go"
style="@style/UrlBar.ImageButton.Icon"
android:src="@drawable/ic_url_bar_go"
android:contentDescription="@string/go"
android:visibility="gone"/>
</LinearLayout>
android:layout_toLeftOf="@id/menu_items"/>
<LinearLayout android:id="@+id/url_display_container"
style="@style/UrlBar.Button.Container"

View File

@ -12,11 +12,6 @@
<ImageButton android:id="@+id/forward"
style="@style/UrlBar.ImageButton.Unused"/>
<org.mozilla.gecko.toolbar.BrowserToolbarBackground android:id="@+id/url_bar_bg"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/url_bar_bg"/>
<ImageView android:id="@+id/url_bar_entry"
style="@style/UrlBar.Button"
android:layout_marginLeft="4dp"
@ -42,6 +37,7 @@
android:duplicateParentState="true"
android:clickable="false"
android:focusable="false"
android:visibility="invisible"
android:src="@drawable/url_bar_right_edge"
android:scaleType="fitXY"/>
@ -90,42 +86,12 @@
android:layout_marginTop="12dip"
android:layout_alignRight="@id/tabs"/>
<LinearLayout android:id="@+id/url_edit_container"
<org.mozilla.gecko.toolbar.ToolbarEditLayout android:id="@+id/edit_layout"
style="@style/UrlBar.Button"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:paddingLeft="8dp"
android:visibility="gone"
android:orientation="horizontal">
<org.mozilla.gecko.CustomEditText
android:id="@+id/url_edit_text"
style="@style/UrlBar.Button"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1.0"
android:hint="@string/url_bar_default_text"
android:textColor="@color/url_bar_title"
android:textColorHint="@color/url_bar_title_hint"
android:textColorHighlight="@color/url_bar_text_highlight"
android:textSelectHandle="@drawable/handle_middle"
android:textSelectHandleLeft="@drawable/handle_start"
android:textSelectHandleRight="@drawable/handle_end"
android:textCursorDrawable="@null"
android:inputType="textUri|textNoSuggestions"
android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen"
android:selectAllOnFocus="true"
android:singleLine="true"
android:gravity="center_vertical|left"
gecko:autoUpdateTheme="false"/>
<ImageButton android:id="@+id/go"
style="@style/UrlBar.ImageButton.Icon"
android:src="@drawable/ic_url_bar_go"
android:contentDescription="@string/go"
android:visibility="gone"/>
</LinearLayout>
android:visibility="gone"/>
<LinearLayout android:id="@+id/url_display_container"
style="@style/UrlBar.Button"

View File

@ -73,7 +73,8 @@
android:layout_width="fill_parent"
android:layout_height="@dimen/browser_toolbar_height"
android:clickable="true"
android:focusable="true"/>
android:focusable="true"
android:background="@drawable/url_bar_bg"/>
</view>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto">
<org.mozilla.gecko.toolbar.ToolbarEditText
android:id="@+id/url_edit_text"
style="@style/UrlBar.Button"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1.0"
android:hint="@string/url_bar_default_text"
android:textColor="@color/url_bar_title"
android:textColorHint="@color/url_bar_title_hint"
android:textColorHighlight="@color/url_bar_text_highlight"
android:textSelectHandle="@drawable/handle_middle"
android:textSelectHandleLeft="@drawable/handle_start"
android:textSelectHandleRight="@drawable/handle_end"
android:textCursorDrawable="@null"
android:inputType="textUri|textNoSuggestions"
android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen"
android:selectAllOnFocus="true"
android:singleLine="true"
android:gravity="center_vertical|left"
gecko:autoUpdateTheme="false"/>
<ImageButton android:id="@+id/go"
style="@style/UrlBar.ImageButton.Icon"
android:src="@drawable/ic_url_bar_go"
android:contentDescription="@string/go"
android:visibility="gone"/>
</merge>

View File

@ -7,8 +7,6 @@ package org.mozilla.gecko.toolbar;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.CustomEditText;
import org.mozilla.gecko.InputMethods;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoProfile;
@ -18,18 +16,14 @@ import org.mozilla.gecko.SiteIdentityPopup;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
import org.mozilla.gecko.gfx.LayerView;
import org.mozilla.gecko.util.GamepadUtils;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.MenuPopup;
import org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.util.Clipboard;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UiAsyncTask;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.widget.GeckoImageButton;
@ -42,22 +36,15 @@ import org.json.JSONObject;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Build;
import android.os.SystemClock;
import android.text.style.ForegroundColorSpan;
import android.text.Editable;
import android.text.InputType;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextMenu;
@ -68,33 +55,24 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.Window;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.AlphaAnimation;
import android.view.animation.Interpolator;
import android.view.animation.TranslateAnimation;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.RelativeLayout;
import android.widget.RelativeLayout.LayoutParams;
import android.widget.ViewSwitcher;
import java.util.Arrays;
import java.util.List;
public class BrowserToolbar extends GeckoRelativeLayout
implements TextWatcher,
AutocompleteHandler,
Tabs.OnTabsChangedListener,
implements Tabs.OnTabsChangedListener,
GeckoMenu.ActionItemBarPresenter,
Animation.AnimationListener,
GeckoEventListener {
@ -127,11 +105,9 @@ public class BrowserToolbar extends GeckoRelativeLayout
}
private View mUrlDisplayContainer;
private View mUrlEditContainer;
private CustomEditText mUrlEditText;
private ToolbarEditLayout mUrlEditLayout;
private View mUrlBarEntry;
private ImageView mUrlBarRightEdge;
private BrowserToolbarBackground mUrlBarBackground;
private GeckoTextView mTitle;
private int mTitlePadding;
private boolean mSiteSecurityVisible;
@ -142,7 +118,6 @@ public class BrowserToolbar extends GeckoRelativeLayout
private ImageButton mFavicon;
private ImageButton mStop;
private ImageButton mSiteSecurity;
private ImageButton mGo;
private PageActionLayout mPageActionLayout;
private Animation mProgressSpinner;
private TabCounter mTabsCounter;
@ -165,12 +140,6 @@ public class BrowserToolbar extends GeckoRelativeLayout
private boolean mShowSiteSecurity;
private boolean mSpinnerVisible;
private boolean mDelayRestartInput;
// The previous autocomplete result returned to us
private String mAutoCompleteResult = "";
// The user typed part of the autocomplete result
private String mAutoCompletePrefix = null;
private boolean mIsEditing;
private boolean mAnimatingEntry;
@ -196,6 +165,8 @@ public class BrowserToolbar extends GeckoRelativeLayout
private final ForegroundColorSpan mDomainColor;
private final ForegroundColorSpan mPrivateDomainColor;
private final LightweightTheme mTheme;
private boolean mShowUrl;
private boolean mTrimURLs;
@ -207,6 +178,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
public BrowserToolbar(Context context, AttributeSet attrs) {
super(context, attrs);
mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
// BrowserToolbar is attached to BrowserApp only.
mActivity = (BrowserApp) context;
@ -217,7 +189,6 @@ public class BrowserToolbar extends GeckoRelativeLayout
Tabs.registerOnTabsChangedListener(this);
mSwitchingTabs = true;
mIsEditing = false;
mAnimatingEntry = false;
mShowUrl = false;
mTrimURLs = true;
@ -283,14 +254,11 @@ public class BrowserToolbar extends GeckoRelativeLayout
mAnimatingEntry = false;
mUrlBarBackground = (BrowserToolbarBackground) findViewById(R.id.url_bar_bg);
mUrlBarViewOffset = res.getDimensionPixelSize(R.dimen.url_bar_offset_left);
mDefaultForwardMargin = res.getDimensionPixelSize(R.dimen.forward_default_offset);
mUrlDisplayContainer = findViewById(R.id.url_display_container);
mUrlBarEntry = findViewById(R.id.url_bar_entry);
mUrlEditContainer = findViewById(R.id.url_edit_container);
mUrlEditText = (CustomEditText) findViewById(R.id.url_edit_text);
mUrlEditLayout = (ToolbarEditLayout) findViewById(R.id.edit_layout);
// This will clip the right edge's image at 60% of its width
mUrlBarRightEdge = (ImageView) findViewById(R.id.url_bar_right_edge);
@ -339,6 +307,8 @@ public class BrowserToolbar extends GeckoRelativeLayout
mFocusOrder = Arrays.asList(this, mSiteSecurity, mPageActionLayout, mStop,
mTabs, mMenu);
}
setIsEditing(false);
}
@Override
@ -398,94 +368,10 @@ public class BrowserToolbar extends GeckoRelativeLayout
}
});
mUrlEditText.addTextChangedListener(this);
mUrlEditText.setOnKeyPreImeListener(new CustomEditText.OnKeyPreImeListener() {
@Override
public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) {
// We only want to process one event per tap
if (event.getAction() != KeyEvent.ACTION_DOWN)
return false;
if (keyCode == KeyEvent.KEYCODE_ENTER) {
// If the edit text has a composition string, don't submit the text yet.
// ENTER is needed to commit the composition string.
Editable content = mUrlEditText.getText();
if (!hasCompositionString(content)) {
if (mCommitListener != null) {
mCommitListener.onCommit();
}
return true;
}
}
if (keyCode == KeyEvent.KEYCODE_BACK) {
// Drop the virtual keyboard.
clearFocus();
return true;
}
return false;
}
});
mUrlEditText.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER || GamepadUtils.isActionKey(event)) {
if (event.getAction() != KeyEvent.ACTION_DOWN)
return true;
if (mCommitListener != null) {
mCommitListener.onCommit();
}
return true;
} else if (GamepadUtils.isBackKey(event)) {
if (mDismissListener != null) {
mDismissListener.onDismiss();
}
return true;
}
return false;
}
});
mUrlEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
mUrlEditLayout.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (v == null) {
return;
}
setSelected(hasFocus);
if (hasFocus) {
return;
}
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
try {
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
} catch (NullPointerException e) {
Log.e(LOGTAG, "InputMethodManagerService, why are you throwing"
+ " a NullPointerException? See bug 782096", e);
}
}
});
mUrlEditText.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (Build.VERSION.SDK_INT >= 11) {
CustomEditText text = (CustomEditText) v;
if (text.getSelectionStart() == text.getSelectionEnd())
return false;
return false;
}
return false;
}
});
@ -553,16 +439,6 @@ public class BrowserToolbar extends GeckoRelativeLayout
}
});
mGo = (ImageButton) findViewById(R.id.go);
mGo.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
if (mCommitListener != null) {
mCommitListener.onCommit();
}
}
});
float slideWidth = getResources().getDimension(R.dimen.browser_toolbar_lock_width);
LinearLayout.LayoutParams siteSecParams = (LinearLayout.LayoutParams) mSiteSecurity.getLayoutParams();
@ -621,24 +497,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
return false;
} else if (isEditing()) {
final int prevSelStart = mUrlEditText.getSelectionStart();
final int prevSelEnd = mUrlEditText.getSelectionEnd();
// Manually dispatch the key event to the edit text. If selection changed as
// a result of the key event, then give focus back to mUrlEditText
mUrlEditText.dispatchKeyEvent(event);
final int curSelStart = mUrlEditText.getSelectionStart();
final int curSelEnd = mUrlEditText.getSelectionEnd();
if (prevSelStart != curSelStart || prevSelEnd != curSelEnd) {
mUrlEditText.requestFocusFromTouch();
// Restore the selection, which gets lost due to the focus switch
mUrlEditText.setSelection(curSelStart, curSelEnd);
}
return true;
return mUrlEditLayout.onKey(keyCode, event);
}
return false;
@ -648,7 +507,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
public boolean onTouchEvent(MotionEvent event) {
// If the motion event has occured below the toolbar (due to the scroll
// offset), let it pass through to the page.
if (event != null && event.getY() > getHeight() - getScrollY()) {
if (event != null && event.getY() > getHeight() + ViewHelper.getTranslationY(this)) {
return false;
}
@ -743,88 +602,8 @@ public class BrowserToolbar extends GeckoRelativeLayout
}
}
// Return early if we're backspacing through the string, or
// have no autocomplete results
@Override
public void onAutocomplete(final String result) {
if (!isEditing()) {
return;
}
final String text = mUrlEditText.getText().toString();
if (result == null) {
mAutoCompleteResult = "";
return;
}
if (!result.startsWith(text) || text.equals(result)) {
return;
}
mAutoCompleteResult = result;
mUrlEditText.getText().append(result.substring(text.length()));
mUrlEditText.setSelection(text.length(), result.length());
}
@Override
public void afterTextChanged(final Editable s) {
if (!isEditing()) {
return;
}
final String text = s.toString();
boolean useHandler = false;
boolean reuseAutocomplete = false;
if (!hasCompositionString(s) && !StringUtils.isSearchQuery(text, false)) {
useHandler = true;
// If you're hitting backspace (the string is getting smaller
// or is unchanged), don't autocomplete.
if (mAutoCompletePrefix != null && (mAutoCompletePrefix.length() >= text.length())) {
useHandler = false;
} else if (mAutoCompleteResult != null && mAutoCompleteResult.startsWith(text)) {
// If this text already matches our autocomplete text, autocomplete likely
// won't change. Just reuse the old autocomplete value.
useHandler = false;
reuseAutocomplete = true;
}
}
// If this is the autocomplete text being set, don't run the filter.
if (TextUtils.isEmpty(mAutoCompleteResult) || !mAutoCompleteResult.equals(text)) {
if (isEditing() && mFilterListener != null) {
mFilterListener.onFilter(text, useHandler ? this : null);
}
mAutoCompletePrefix = text;
if (reuseAutocomplete) {
onAutocomplete(mAutoCompleteResult);
}
}
// If the edit text has a composition string, don't call updateGoButton().
// That method resets IME and composition state will be broken.
if (!hasCompositionString(s) ||
InputMethods.isGestureKeyboard(mUrlEditText.getContext())) {
updateGoButton(text);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// do nothing
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
// do nothing
}
public boolean isVisible() {
return getScrollY() == 0;
return ViewHelper.getTranslationY(this) == 0;
}
public void setNextFocusDownId(int nextId) {
@ -875,19 +654,6 @@ public class BrowserToolbar extends GeckoRelativeLayout
return getWidth() - mTabs.getLeft();
}
private static boolean hasCompositionString(Editable content) {
Object[] spans = content.getSpans(0, content.length(), Object.class);
if (spans != null) {
for (Object span : spans) {
if ((content.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
// Found composition string.
return true;
}
}
}
return false;
}
private boolean canDoBack(Tab tab) {
return (tab.canDoBack() && !mIsEditing);
}
@ -1082,11 +848,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
return;
}
mUrlEditText.setText(suggestion);
mUrlEditText.setSelection(mUrlEditText.getText().length());
mUrlEditText.requestFocus();
showSoftInput();
mUrlEditLayout.onEditSuggestion(suggestion);
}
public void setTitle(CharSequence title) {
@ -1105,7 +867,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
String url = tab.getURL();
if (!isEditing()) {
mUrlEditText.setText(url);
mUrlEditLayout.setText(url);
}
// Setting a null title will ensure we just see the "Enter Search or Address" placeholder text.
@ -1221,14 +983,17 @@ public class BrowserToolbar extends GeckoRelativeLayout
public void setOnCommitListener(OnCommitListener listener) {
mCommitListener = listener;
mUrlEditLayout.setOnCommitListener(listener);
}
public void setOnDismissListener(OnDismissListener listener) {
mDismissListener = listener;
mUrlEditLayout.setOnDismissListener(listener);
}
public void setOnFilterListener(OnFilterListener listener) {
mFilterListener = listener;
mUrlEditLayout.setOnFilterListener(listener);
}
public void setOnStartEditingListener(OnStartEditingListener listener) {
@ -1239,41 +1004,33 @@ public class BrowserToolbar extends GeckoRelativeLayout
mStopEditingListener = listener;
}
private void showSoftInput() {
InputMethodManager imm =
(InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mUrlEditText, InputMethodManager.SHOW_IMPLICIT);
private void showUrlEditLayout() {
setUrlEditLayoutVisibility(true, null);
}
private void showUrlEditContainer() {
setUrlEditContainerVisibility(true, null);
private void showUrlEditLayout(PropertyAnimator animator) {
setUrlEditLayoutVisibility(true, animator);
}
private void showUrlEditContainer(PropertyAnimator animator) {
setUrlEditContainerVisibility(true, animator);
private void hideUrlEditLayout() {
setUrlEditLayoutVisibility(false, null);
}
private void hideUrlEditContainer() {
setUrlEditContainerVisibility(false, null);
private void hideUrlEditLayout(PropertyAnimator animator) {
setUrlEditLayoutVisibility(false, animator);
}
private void hideUrlEditContainer(PropertyAnimator animator) {
setUrlEditContainerVisibility(false, animator);
}
private void setUrlEditLayoutVisibility(final boolean showEditLayout, PropertyAnimator animator) {
final View viewToShow = (showEditLayout ? mUrlEditLayout : mUrlDisplayContainer);
final View viewToHide = (showEditLayout ? mUrlDisplayContainer : mUrlEditLayout);
private void setUrlEditContainerVisibility(final boolean showEditContainer, PropertyAnimator animator) {
final View viewToShow = (showEditContainer ? mUrlEditContainer : mUrlDisplayContainer);
final View viewToHide = (showEditContainer ? mUrlDisplayContainer : mUrlEditContainer);
if (showEditLayout) {
mUrlEditLayout.prepareShowAnimation(animator);
}
if (animator == null) {
viewToHide.setVisibility(View.GONE);
viewToShow.setVisibility(View.VISIBLE);
if (showEditContainer) {
mUrlEditText.requestFocus();
showSoftInput();
}
return;
}
@ -1286,26 +1043,16 @@ public class BrowserToolbar extends GeckoRelativeLayout
PropertyAnimator.Property.ALPHA,
0.0f);
animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
animator.addPropertyAnimationListener(new PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
viewToShow.setVisibility(View.VISIBLE);
if (showEditContainer) {
ViewHelper.setAlpha(mGo, 0.0f);
mUrlEditText.requestFocus();
}
}
@Override
public void onPropertyAnimationEnd() {
viewToHide.setVisibility(View.GONE);
ViewHelper.setAlpha(viewToHide, 1.0f);
if (showEditContainer) {
ViewHelper.setAlpha(mGo, 1.0f);
showSoftInput();
}
}
});
}
@ -1345,6 +1092,11 @@ public class BrowserToolbar extends GeckoRelativeLayout
}
}
public void setIsEditing(boolean isEditing) {
mIsEditing = isEditing;
mUrlEditLayout.setEnabled(isEditing);
}
/**
* Returns whether or not the URL bar is in editing mode (url bar is expanded, hiding the new
* tab button). Note that selection state is independent of editing mode.
@ -1358,21 +1110,25 @@ public class BrowserToolbar extends GeckoRelativeLayout
return;
}
mUrlEditText.setText(url != null ? url : "");
mIsEditing = true;
mUrlEditLayout.setText(url != null ? url : "");
setIsEditing(true);
updateChildrenForEditing();
if (mStartEditingListener != null) {
mStartEditingListener.onStartEditing();
}
if (mUrlBarRightEdge != null) {
mUrlBarRightEdge.setVisibility(View.VISIBLE);
}
final int entryTranslation = getUrlBarEntryTranslation();
final int curveTranslation = getUrlBarCurveTranslation();
// This animation doesn't make much sense in a sidebar UI
if (HardwareUtils.isTablet() || Build.VERSION.SDK_INT < 11) {
showUrlEditContainer();
showUrlEditLayout();
if (!HardwareUtils.isTablet()) {
if (mUrlBarRightEdge != null) {
@ -1430,7 +1186,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
curveTranslation);
}
showUrlEditContainer(animator);
showUrlEditLayout(animator);
animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
@Override
@ -1469,11 +1225,11 @@ public class BrowserToolbar extends GeckoRelativeLayout
}
private String stopEditing() {
final String url = mUrlEditText.getText().toString();
final String url = mUrlEditLayout.getText();
if (!isEditing()) {
return url;
}
mIsEditing = false;
setIsEditing(false);
updateChildrenForEditing();
@ -1482,7 +1238,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
}
if (HardwareUtils.isTablet() || Build.VERSION.SDK_INT < 11) {
hideUrlEditContainer();
hideUrlEditLayout();
if (!HardwareUtils.isTablet()) {
updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount());
@ -1535,7 +1291,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
0);
}
hideUrlEditContainer(contentAnimator);
hideUrlEditLayout(contentAnimator);
contentAnimator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
@Override
@ -1544,6 +1300,10 @@ public class BrowserToolbar extends GeckoRelativeLayout
@Override
public void onPropertyAnimationEnd() {
if (mUrlBarRightEdge != null) {
mUrlBarRightEdge.setVisibility(View.INVISIBLE);
}
PropertyAnimator buttonsAnimator = new PropertyAnimator(300);
// Fade toolbar buttons (page actions, stop) after the entry
@ -1571,73 +1331,6 @@ public class BrowserToolbar extends GeckoRelativeLayout
return url;
}
private void updateGoButton(String text) {
if (text.length() == 0) {
mGo.setVisibility(View.GONE);
return;
}
mGo.setVisibility(View.VISIBLE);
if (InputMethods.shouldDisableUrlBarUpdate(mUrlEditText.getContext())) {
return;
}
int imageResource = R.drawable.ic_url_bar_go;
String contentDescription = mActivity.getString(R.string.go);
int imeAction = EditorInfo.IME_ACTION_GO;
int actionBits = mUrlEditText.getImeOptions() & EditorInfo.IME_MASK_ACTION;
if (StringUtils.isSearchQuery(text, actionBits == EditorInfo.IME_ACTION_SEARCH)) {
imageResource = R.drawable.ic_url_bar_search;
contentDescription = mActivity.getString(R.string.search);
imeAction = EditorInfo.IME_ACTION_SEARCH;
}
InputMethodManager imm = InputMethods.getInputMethodManager(mUrlEditText.getContext());
if (imm == null) {
return;
}
boolean restartInput = false;
if (actionBits != imeAction) {
int optionBits = mUrlEditText.getImeOptions() & ~EditorInfo.IME_MASK_ACTION;
mUrlEditText.setImeOptions(optionBits | imeAction);
mDelayRestartInput = (imeAction == EditorInfo.IME_ACTION_GO) &&
(InputMethods.shouldDelayUrlBarUpdate(mUrlEditText.getContext()));
if (!mDelayRestartInput) {
restartInput = true;
}
} else if (mDelayRestartInput) {
// Only call delayed restartInput when actionBits == imeAction
// so if there are two restarts in a row, the first restarts will
// be discarded and the second restart will be properly delayed
mDelayRestartInput = false;
restartInput = true;
}
if (restartInput) {
updateKeyboardInputType();
imm.restartInput(mUrlEditText);
mGo.setImageResource(imageResource);
mGo.setContentDescription(contentDescription);
}
}
private void updateKeyboardInputType() {
// If the user enters a space, then we know they are entering search terms, not a URL.
// We can then switch to text mode so,
// 1) the IME auto-inserts spaces between words
// 2) the IME doesn't reset input keyboard to Latin keyboard.
String text = mUrlEditText.getText().toString();
int currentInputType = mUrlEditText.getInputType();
int newInputType = StringUtils.isSearchQuery(text, false)
? (currentInputType & ~InputType.TYPE_TEXT_VARIATION_URI) // Text mode
: (currentInputType | InputType.TYPE_TEXT_VARIATION_URI); // URL mode
if (newInputType != currentInputType) {
mUrlEditText.setRawInputType(newInputType);
}
}
public void setButtonEnabled(ImageButton button, boolean enabled) {
final Drawable drawable = button.getDrawable();
if (drawable != null) {
@ -1679,7 +1372,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
layoutParams.leftMargin = 0;
// Do the same on the URL edit container
layoutParams = (ViewGroup.MarginLayoutParams)mUrlEditContainer.getLayoutParams();
layoutParams = (ViewGroup.MarginLayoutParams) mUrlEditLayout.getLayoutParams();
layoutParams.leftMargin = 0;
requestLayout();
@ -1696,7 +1389,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
(ViewGroup.MarginLayoutParams)mUrlDisplayContainer.getLayoutParams();
layoutParams.leftMargin = mUrlBarViewOffset;
layoutParams = (ViewGroup.MarginLayoutParams)mUrlEditContainer.getLayoutParams();
layoutParams = (ViewGroup.MarginLayoutParams) mUrlEditLayout.getLayoutParams();
layoutParams.leftMargin = mUrlBarViewOffset;
ViewHelper.setTranslationX(mTitle, 0);
@ -1791,13 +1484,12 @@ public class BrowserToolbar extends GeckoRelativeLayout
updateForwardButton(canDoForward(tab));
final boolean isPrivate = tab.isPrivate();
mUrlBarBackground.setPrivateMode(isPrivate);
setPrivateMode(isPrivate);
mTabs.setPrivateMode(isPrivate);
mTitle.setPrivateMode(isPrivate);
mMenu.setPrivateMode(isPrivate);
mMenuIcon.setPrivateMode(isPrivate);
mUrlEditText.setPrivateMode(isPrivate);
mUrlEditLayout.setPrivateMode(isPrivate);
if (mBack instanceof BackButton)
((BackButton) mBack).setPrivateMode(isPrivate);
@ -1879,4 +1571,22 @@ public class BrowserToolbar extends GeckoRelativeLayout
}
}
}
@Override
public void onLightweightThemeChanged() {
Drawable drawable = mTheme.getDrawable(this);
if (drawable == null)
return;
StateListDrawable stateList = new StateListDrawable();
stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.background_private));
stateList.addState(EMPTY_STATE_SET, drawable);
setBackgroundDrawable(stateList);
}
@Override
public void onLightweightThemeReset() {
setBackgroundResource(R.drawable.url_bar_bg);
}
}

View File

@ -1,42 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.toolbar;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.LightweightTheme;
import org.mozilla.gecko.R;
import org.mozilla.gecko.widget.GeckoLinearLayout;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.util.AttributeSet;
public class BrowserToolbarBackground extends GeckoLinearLayout {
private final LightweightTheme mTheme;
public BrowserToolbarBackground(Context context, AttributeSet attrs) {
super(context, attrs);
mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
}
@Override
public void onLightweightThemeChanged() {
final Drawable drawable = mTheme.getDrawable(this);
if (drawable == null)
return;
final StateListDrawable stateList = new StateListDrawable();
stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.background_private));
stateList.addState(EMPTY_STATE_SET, drawable);
setBackgroundDrawable(stateList);
}
@Override
public void onLightweightThemeReset() {
setBackgroundResource(R.drawable.url_bar_bg);
}
}

View File

@ -0,0 +1,193 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.toolbar;
import org.mozilla.gecko.R;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
import org.mozilla.gecko.toolbar.ToolbarEditText.OnTextTypeChangeListener;
import org.mozilla.gecko.toolbar.ToolbarEditText.TextType;
import org.mozilla.gecko.widget.GeckoLinearLayout;
import android.content.Context;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnFocusChangeListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageButton;
public class ToolbarEditLayout extends GeckoLinearLayout {
private final ToolbarEditText mEditText;
private final ImageButton mGo;
private OnCommitListener mCommitListener;
private OnFocusChangeListener mFocusChangeListener;
public ToolbarEditLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(HORIZONTAL);
LayoutInflater.from(context).inflate(R.layout.toolbar_edit_layout, this);
mGo = (ImageButton) findViewById(R.id.go);
mEditText = (ToolbarEditText) findViewById(R.id.url_edit_text);
}
@Override
public void onAttachedToWindow() {
mGo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mCommitListener != null) {
mCommitListener.onCommit();
}
}
});
mEditText.setOnTextTypeChangeListener(new OnTextTypeChangeListener() {
@Override
public void onTextTypeChange(ToolbarEditText editText, TextType textType) {
updateGoButton(textType);
}
});
mEditText.setOnFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (mFocusChangeListener != null) {
mFocusChangeListener.onFocusChange(ToolbarEditLayout.this, hasFocus);
}
}
});
}
@Override
public void setOnFocusChangeListener(OnFocusChangeListener listener) {
mFocusChangeListener = listener;
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
mGo.setEnabled(enabled);
mEditText.setEnabled(enabled);
}
@Override
public void setPrivateMode(boolean isPrivate) {
super.setPrivateMode(isPrivate);
mEditText.setPrivateMode(isPrivate);
}
private void updateGoButton(TextType textType) {
if (textType == TextType.EMPTY) {
mGo.setVisibility(View.GONE);
return;
}
mGo.setVisibility(View.VISIBLE);
final int imageResource;
final String contentDescription;
if (textType == TextType.SEARCH_QUERY) {
imageResource = R.drawable.ic_url_bar_search;
contentDescription = getContext().getString(R.string.search);
} else {
imageResource = R.drawable.ic_url_bar_go;
contentDescription = getContext().getString(R.string.go);
}
mGo.setImageResource(imageResource);
mGo.setContentDescription(contentDescription);
}
private void showSoftInput() {
InputMethodManager imm =
(InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT);
}
void prepareShowAnimation(PropertyAnimator animator) {
if (animator == null) {
mEditText.requestFocus();
showSoftInput();
return;
}
animator.addPropertyAnimationListener(new PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
ViewHelper.setAlpha(mGo, 0.0f);
mEditText.requestFocus();
}
@Override
public void onPropertyAnimationEnd() {
ViewHelper.setAlpha(mGo, 1.0f);
showSoftInput();
}
});
}
void setOnCommitListener(OnCommitListener listener) {
mCommitListener = listener;
mEditText.setOnCommitListener(listener);
}
void setOnDismissListener(OnDismissListener listener) {
mEditText.setOnDismissListener(listener);
}
void setOnFilterListener(OnFilterListener listener) {
mEditText.setOnFilterListener(listener);
}
boolean onKey(int keyCode, KeyEvent event) {
final int prevSelStart = mEditText.getSelectionStart();
final int prevSelEnd = mEditText.getSelectionEnd();
// Manually dispatch the key event to the edit text. If selection changed as
// a result of the key event, then give focus back to mEditText
mEditText.dispatchKeyEvent(event);
final int curSelStart = mEditText.getSelectionStart();
final int curSelEnd = mEditText.getSelectionEnd();
if (prevSelStart != curSelStart || prevSelEnd != curSelEnd) {
mEditText.requestFocusFromTouch();
// Restore the selection, which gets lost due to the focus switch
mEditText.setSelection(curSelStart, curSelEnd);
}
return true;
}
void onEditSuggestion(String suggestion) {
mEditText.setText(suggestion);
mEditText.setSelection(mEditText.getText().length());
mEditText.requestFocus();
showSoftInput();
}
void setText(String text) {
mEditText.setText(text);
}
String getText() {
return mEditText.getText().toString();
}
}

View File

@ -0,0 +1,349 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.toolbar;
import org.mozilla.gecko.R;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
import org.mozilla.gecko.CustomEditText;
import org.mozilla.gecko.CustomEditText.OnKeyPreImeListener;
import org.mozilla.gecko.InputMethods;
import org.mozilla.gecko.util.GamepadUtils;
import org.mozilla.gecko.util.StringUtils;
import android.content.Context;
import android.graphics.Rect;
import android.text.Editable;
import android.text.InputType;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
public class ToolbarEditText extends CustomEditText
implements AutocompleteHandler {
private static final String LOGTAG = "GeckoToolbarEditText";
enum TextType {
EMPTY,
SEARCH_QUERY,
URL
}
interface OnTextTypeChangeListener {
public void onTextTypeChange(ToolbarEditText editText, TextType textType);
}
private final Context mContext;
private TextType mTextType;
private OnCommitListener mCommitListener;
private OnDismissListener mDismissListener;
private OnFilterListener mFilterListener;
private OnTextTypeChangeListener mTextTypeListener;
// The previous autocomplete result returned to us
private String mAutoCompleteResult = "";
// The user typed part of the autocomplete result
private String mAutoCompletePrefix = null;
private boolean mDelayRestartInput;
public ToolbarEditText(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mTextType = TextType.EMPTY;
}
void setOnCommitListener(OnCommitListener listener) {
mCommitListener = listener;
}
void setOnDismissListener(OnDismissListener listener) {
mDismissListener = listener;
}
void setOnFilterListener(OnFilterListener listener) {
mFilterListener = listener;
}
void setOnTextTypeChangeListener(OnTextTypeChangeListener listener) {
mTextTypeListener = listener;
}
@Override
public void onAttachedToWindow() {
setOnKeyListener(new KeyListener());
setOnKeyPreImeListener(new KeyPreImeListener());
addTextChangedListener(new TextChangeListener());
}
@Override
public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
if (gainFocus) {
return;
}
InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
try {
imm.hideSoftInputFromWindow(getWindowToken(), 0);
} catch (NullPointerException e) {
Log.e(LOGTAG, "InputMethodManagerService, why are you throwing"
+ " a NullPointerException? See bug 782096", e);
}
}
// Return early if we're backspacing through the string, or
// have no autocomplete results
@Override
public final void onAutocomplete(final String result) {
if (!isEnabled()) {
return;
}
final String text = getText().toString();
if (result == null) {
mAutoCompleteResult = "";
return;
}
if (!result.startsWith(text) || text.equals(result)) {
return;
}
mAutoCompleteResult = result;
getText().append(result.substring(text.length()));
setSelection(text.length(), result.length());
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
updateTextTypeFromText(getText().toString());
}
private void updateKeyboardInputType() {
// If the user enters a space, then we know they are entering
// search terms, not a URL. We can then switch to text mode so,
// 1) the IME auto-inserts spaces between words
// 2) the IME doesn't reset input keyboard to Latin keyboard.
final String text = getText().toString();
final int currentInputType = getInputType();
final int newInputType = StringUtils.isSearchQuery(text, false)
? (currentInputType & ~InputType.TYPE_TEXT_VARIATION_URI) // Text mode
: (currentInputType | InputType.TYPE_TEXT_VARIATION_URI); // URL mode
if (newInputType != currentInputType) {
setRawInputType(newInputType);
}
}
private static boolean hasCompositionString(Editable content) {
Object[] spans = content.getSpans(0, content.length(), Object.class);
if (spans != null) {
for (Object span : spans) {
if ((content.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
// Found composition string.
return true;
}
}
}
return false;
}
private void setTextType(TextType textType) {
mTextType = textType;
if (mTextTypeListener != null) {
mTextTypeListener.onTextTypeChange(this, mTextType);
}
}
private void updateTextTypeFromText(String text) {
if (text.length() == 0) {
setTextType(TextType.EMPTY);
return;
}
if (InputMethods.shouldDisableUrlBarUpdate(mContext)) {
return;
}
final int actionBits = getImeOptions() & EditorInfo.IME_MASK_ACTION;
final int imeAction;
if (StringUtils.isSearchQuery(text, actionBits == EditorInfo.IME_ACTION_SEARCH)) {
imeAction = EditorInfo.IME_ACTION_SEARCH;
} else {
imeAction = EditorInfo.IME_ACTION_GO;
}
InputMethodManager imm = InputMethods.getInputMethodManager(mContext);
if (imm == null) {
return;
}
boolean restartInput = false;
if (actionBits != imeAction) {
int optionBits = getImeOptions() & ~EditorInfo.IME_MASK_ACTION;
setImeOptions(optionBits | imeAction);
mDelayRestartInput = (imeAction == EditorInfo.IME_ACTION_GO) &&
(InputMethods.shouldDelayUrlBarUpdate(mContext));
if (!mDelayRestartInput) {
restartInput = true;
}
} else if (mDelayRestartInput) {
// Only call delayed restartInput when actionBits == imeAction
// so if there are two restarts in a row, the first restarts will
// be discarded and the second restart will be properly delayed
mDelayRestartInput = false;
restartInput = true;
}
if (restartInput) {
updateKeyboardInputType();
imm.restartInput(ToolbarEditText.this);
}
setTextType(imeAction == EditorInfo.IME_ACTION_GO ?
TextType.URL : TextType.SEARCH_QUERY);
}
private class TextChangeListener implements TextWatcher {
@Override
public void afterTextChanged(final Editable s) {
if (!isEnabled()) {
return;
}
final String text = s.toString();
boolean useHandler = false;
boolean reuseAutocomplete = false;
if (!hasCompositionString(s) && !StringUtils.isSearchQuery(text, false)) {
useHandler = true;
// If you're hitting backspace (the string is getting smaller
// or is unchanged), don't autocomplete.
if (mAutoCompletePrefix != null && (mAutoCompletePrefix.length() >= text.length())) {
useHandler = false;
} else if (mAutoCompleteResult != null && mAutoCompleteResult.startsWith(text)) {
// If this text already matches our autocomplete text, autocomplete likely
// won't change. Just reuse the old autocomplete value.
useHandler = false;
reuseAutocomplete = true;
}
}
// If this is the autocomplete text being set, don't run the filter.
if (TextUtils.isEmpty(mAutoCompleteResult) || !mAutoCompleteResult.equals(text)) {
if (mFilterListener != null) {
mFilterListener.onFilter(text, useHandler ? ToolbarEditText.this : null);
}
mAutoCompletePrefix = text;
if (reuseAutocomplete) {
onAutocomplete(mAutoCompleteResult);
}
}
// If the edit text has a composition string, don't call updateGoButton().
// That method resets IME and composition state will be broken.
if (!hasCompositionString(s) || InputMethods.isGestureKeyboard(mContext)) {
updateTextTypeFromText(text);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// do nothing
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
// do nothing
}
}
private class KeyPreImeListener implements OnKeyPreImeListener {
@Override
public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) {
// We only want to process one event per tap
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return false;
}
if (keyCode == KeyEvent.KEYCODE_ENTER) {
// If the edit text has a composition string, don't submit the text yet.
// ENTER is needed to commit the composition string.
final Editable content = getText();
if (!hasCompositionString(content)) {
if (mCommitListener != null) {
mCommitListener.onCommit();
}
return true;
}
}
if (keyCode == KeyEvent.KEYCODE_BACK) {
// Drop the virtual keyboard.
clearFocus();
return true;
}
return false;
}
}
private class KeyListener implements View.OnKeyListener {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER || GamepadUtils.isActionKey(event)) {
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return true;
}
if (mCommitListener != null) {
mCommitListener.onCommit();
}
return true;
} else if (GamepadUtils.isBackKey(event)) {
if (mDismissListener != null) {
mDismissListener.onDismiss();
}
return true;
}
return false;
}
}
}

View File

@ -4,6 +4,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
%{C++
#include "mozilla/DebugOnly.h"
%}
[ptr] native octetPtr(uint8_t);
@ -78,10 +81,7 @@ interface mozIStorageValueArray : nsISupports {
inline int32_t AsInt32(uint32_t idx) {
int32_t v = 0;
#ifdef DEBUG
nsresult rv =
#endif
GetInt32(idx, &v);
mozilla::DebugOnly<nsresult> rv = GetInt32(idx, &v);
NS_ABORT_IF_FALSE(NS_SUCCEEDED(rv) || IsNull(idx),
"Getting value failed, wrong column index?");
return v;
@ -89,10 +89,7 @@ interface mozIStorageValueArray : nsISupports {
inline int64_t AsInt64(uint32_t idx) {
int64_t v = 0;
#ifdef DEBUG
nsresult rv =
#endif
GetInt64(idx, &v);
mozilla::DebugOnly<nsresult> rv = GetInt64(idx, &v);
NS_ABORT_IF_FALSE(NS_SUCCEEDED(rv) || IsNull(idx),
"Getting value failed, wrong column index?");
return v;
@ -100,10 +97,7 @@ interface mozIStorageValueArray : nsISupports {
inline double AsDouble(uint32_t idx) {
double v = 0.0;
#ifdef DEBUG
nsresult rv =
#endif
GetDouble(idx, &v);
mozilla::DebugOnly<nsresult> rv = GetDouble(idx, &v);
NS_ABORT_IF_FALSE(NS_SUCCEEDED(rv) || IsNull(idx),
"Getting value failed, wrong column index?");
return v;
@ -112,10 +106,7 @@ interface mozIStorageValueArray : nsISupports {
inline const char* AsSharedUTF8String(uint32_t idx, uint32_t *len) {
const char *str = nullptr;
*len = 0;
#ifdef DEBUG
nsresult rv =
#endif
GetSharedUTF8String(idx, len, &str);
mozilla::DebugOnly<nsresult> rv = GetSharedUTF8String(idx, len, &str);
NS_ABORT_IF_FALSE(NS_SUCCEEDED(rv) || IsNull(idx),
"Getting value failed, wrong column index?");
return str;
@ -124,10 +115,7 @@ interface mozIStorageValueArray : nsISupports {
inline const PRUnichar* AsSharedWString(uint32_t idx, uint32_t *len) {
const PRUnichar *str = nullptr;
*len = 0;
#ifdef DEBUG
nsresult rv =
#endif
GetSharedString(idx, len, &str);
mozilla::DebugOnly<nsresult> rv = GetSharedString(idx, len, &str);
NS_ABORT_IF_FALSE(NS_SUCCEEDED(rv) || IsNull(idx),
"Getting value failed, wrong column index?");
return str;
@ -136,10 +124,7 @@ interface mozIStorageValueArray : nsISupports {
inline const uint8_t* AsSharedBlob(uint32_t idx, uint32_t *len) {
const uint8_t *blob = nullptr;
*len = 0;
#ifdef DEBUG
nsresult rv =
#endif
GetSharedBlob(idx, len, &blob);
mozilla::DebugOnly<nsresult> rv = GetSharedBlob(idx, len, &blob);
NS_ABORT_IF_FALSE(NS_SUCCEEDED(rv) || IsNull(idx),
"Getting value failed, wrong column index?");
return blob;
@ -147,10 +132,7 @@ interface mozIStorageValueArray : nsISupports {
inline bool IsNull(uint32_t idx) {
bool b = false;
#ifdef DEBUG
nsresult rv =
#endif
GetIsNull(idx, &b);
mozilla::DebugOnly<nsresult> rv = GetIsNull(idx, &b);
NS_ABORT_IF_FALSE(NS_SUCCEEDED(rv),
"Getting value failed, wrong column index?");
return b;

View File

@ -9,10 +9,18 @@ support-files =
thumbnails_crash_content_helper.js
thumbnails_update.sjs
[browser_thumbnails_background.js]
# Too many intermittent failures (bug 931889)
skip-if = os == "linux"
[browser_thumbnails_background_crash.js]
[browser_thumbnails_bg_basic.js]
[browser_thumbnails_bg_queueing.js]
[browser_thumbnails_bg_timeout.js]
[browser_thumbnails_bg_redirect.js]
[browser_thumbnails_bg_destroy_browser.js]
[browser_thumbnails_bg_no_cookies_sent.js]
[browser_thumbnails_bg_no_cookies_stored.js]
[browser_thumbnails_bg_no_auth_prompt.js]
[browser_thumbnails_bg_no_alert.js]
[browser_thumbnails_bg_no_duplicates.js]
[browser_thumbnails_bg_captureIfMissing.js]
[browser_thumbnails_bug726727.js]
[browser_thumbnails_bug727765.js]
[browser_thumbnails_bug818225.js]

View File

@ -1,417 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_PAGE_URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_background.sjs";
const imports = {};
Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", imports);
Cu.import("resource://gre/modules/PageThumbs.jsm", imports);
Cu.import("resource://gre/modules/Task.jsm", imports);
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", imports);
registerCleanupFunction(function () {
imports.BackgroundPageThumbs._destroy();
});
function test() {
requestLongerTimeout(2);
waitForExplicitFinish();
spawnNextTest();
}
function spawnNextTest() {
if (!tests.length) {
finish();
return;
}
let nextTest = tests.shift();
info("starting sub-test " + nextTest.name);
imports.Task.spawn(nextTest).then(spawnNextTest, function onError(err) {
ok(false, err);
spawnNextTest();
});
}
let tests = [
function basic() {
let url = "http://www.example.com/";
let file = fileForURL(url);
ok(!file.exists(), "Thumbnail should not be cached yet.");
let capturedURL = yield capture(url);
is(capturedURL, url, "Captured URL should be URL passed to capture");
ok(file.exists(), "Thumbnail should be cached after capture: " + file.path);
file.remove(false);
},
function queueing() {
let deferred = imports.Promise.defer();
let urls = [
"http://www.example.com/0",
"http://www.example.com/1",
// an item that will timeout to ensure timeouts work and we resume.
testPageURL({ wait: 2002 }),
"http://www.example.com/2",
];
let files = urls.map(fileForURL);
files.forEach(f => ok(!f.exists(), "Thumbnail should not be cached yet."));
urls.forEach(function (url) {
let isTimeoutTest = url.indexOf("wait") >= 0;
imports.BackgroundPageThumbs.capture(url, {
timeout: isTimeoutTest ? 100 : 30000,
onDone: function onDone(capturedURL) {
ok(urls.length > 0, "onDone called, so URLs should still remain");
is(capturedURL, urls.shift(),
"Captured URL should be currently expected URL (i.e., " +
"capture() callbacks should be called in the correct order)");
let file = files.shift();
if (isTimeoutTest) {
ok(!file.exists(),
"Thumbnail shouldn't exist for timed out capture: " + file.path);
} else {
ok(file.exists(),
"Thumbnail should be cached after capture: " + file.path);
}
if (!urls.length)
deferred.resolve();
},
});
});
yield deferred.promise;
},
function timeout() {
let deferred = imports.Promise.defer();
let url = testPageURL({ wait: 30000 });
let file = fileForURL(url);
ok(!file.exists(), "Thumbnail should not be cached already.");
let numCalls = 0;
imports.BackgroundPageThumbs.capture(url, {
timeout: 0,
onDone: function onDone(capturedURL) {
is(capturedURL, url, "Captured URL should be URL passed to capture");
is(numCalls++, 0, "onDone should be called only once");
ok(!file.exists(),
"Capture timed out so thumbnail should not be cached: " + file.path);
deferred.resolve();
},
});
yield deferred.promise;
},
function redirect() {
let finalURL = "http://example.com/redirected";
let originalURL = testPageURL({ redirect: finalURL });
let originalFile = fileForURL(originalURL);
ok(!originalFile.exists(),
"Thumbnail file for original URL should not exist yet.");
let finalFile = fileForURL(finalURL);
ok(!finalFile.exists(),
"Thumbnail file for final URL should not exist yet.");
let capturedURL = yield capture(originalURL);
is(capturedURL, originalURL,
"Captured URL should be URL passed to capture");
ok(originalFile.exists(),
"Thumbnail for original URL should be cached: " + originalFile.path);
ok(finalFile.exists(),
"Thumbnail for final URL should be cached: " + finalFile.path);
originalFile.remove(false);
finalFile.remove(false);
},
function destroyBrowserTimeout() {
let url1 = "http://example.com/1";
let file1 = fileForURL(url1);
ok(!file1.exists(), "First file should not exist yet.");
let url2 = "http://example.com/2";
let file2 = fileForURL(url2);
ok(!file2.exists(), "Second file should not exist yet.");
let defaultTimeout = imports.BackgroundPageThumbs._destroyBrowserTimeout;
imports.BackgroundPageThumbs._destroyBrowserTimeout = 1000;
yield capture(url1);
ok(file1.exists(), "First file should exist after capture.");
file1.remove(false);
yield wait(2000);
is(imports.BackgroundPageThumbs._thumbBrowser, undefined,
"Thumb browser should be destroyed after timeout.");
imports.BackgroundPageThumbs._destroyBrowserTimeout = defaultTimeout;
yield capture(url2);
ok(file2.exists(), "Second file should exist after capture.");
file2.remove(false);
isnot(imports.BackgroundPageThumbs._thumbBrowser, undefined,
"Thumb browser should exist immediately after capture.");
},
function privateBrowsingActive() {
let url = "http://example.com/";
let file = fileForURL(url);
ok(!file.exists(), "Thumbnail file should not already exist.");
let win = yield openPrivateWindow();
let capturedURL = yield capture(url);
is(capturedURL, url, "Captured URL should be URL passed to capture.");
ok(file.exists(),
"Thumbnail file should be created even when a private window is open.");
file.remove(false);
win.close();
},
function openPrivateWindowDuringCapture() {
let url = "http://example.com/";
let file = fileForURL(url);
ok(!file.exists(), "Thumbnail file should not already exist.");
let deferred = imports.Promise.defer();
let waitCount = 0;
function maybeFinish() {
if (++waitCount == 2)
deferred.resolve();
}
imports.BackgroundPageThumbs.capture(url, {
onDone: function (capturedURL) {
is(capturedURL, url, "Captured URL should be URL passed to capture.");
ok(file.exists(),
"Thumbnail file should be created even though a private window " +
"was opened during the capture.");
file.remove(false);
maybeFinish();
},
});
// Opening the private window at this point relies on a couple of
// implementation details: (1) The capture will start immediately and
// synchronously (since at this point in the test, the service is
// initialized and its queue is empty), and (2) when it starts the capture
// registers with the window watcher.
openPrivateWindow().then(function (win) {
win.close();
maybeFinish();
});
yield deferred.promise;
},
function noCookiesSent() {
// Visit the test page in the browser and tell it to set a cookie.
let url = testPageURL({ setGreenCookie: true });
let tab = gBrowser.loadOneTab(url, { inBackground: false });
let browser = tab.linkedBrowser;
yield onPageLoad(browser);
// The root element of the page shouldn't be green yet.
let greenStr = "rgb(0, 255, 0)";
isnot(browser.contentDocument.documentElement.style.backgroundColor,
greenStr,
"The page shouldn't be green yet.");
// Cookie should be set now. Reload the page to verify. Its root element
// will be green if the cookie's set.
browser.reload();
yield onPageLoad(browser);
is(browser.contentDocument.documentElement.style.backgroundColor,
greenStr,
"The page should be green now.");
// Capture the page. Get the image data of the capture and verify it's not
// green. (Checking only the first pixel suffices.)
yield capture(url);
let file = fileForURL(url);
ok(file.exists(), "Thumbnail file should exist after capture.");
let deferred = imports.Promise.defer();
retrieveImageDataForURL(url, function ([r, g, b]) {
isnot([r, g, b].toString(), [0, 255, 0].toString(),
"The captured page should not be green.");
gBrowser.removeTab(tab);
file.remove(false);
deferred.resolve();
});
yield deferred.promise;
},
// check that if a page captured in the background attempts to set a cookie,
// that cookie is not saved for subsequent requests.
function noCookiesStored() {
let url = testPageURL({ setRedCookie: true });
let file = fileForURL(url);
ok(!file.exists(), "Thumbnail file should not exist before capture.");
yield capture(url);
ok(file.exists(), "Thumbnail file should exist after capture.");
file.remove(false);
// now load it up in a browser - it should *not* be red, otherwise the
// cookie above was saved.
let tab = gBrowser.loadOneTab(url, { inBackground: false });
let browser = tab.linkedBrowser;
yield onPageLoad(browser);
// The root element of the page shouldn't be red.
let redStr = "rgb(255, 0, 0)";
isnot(browser.contentDocument.documentElement.style.backgroundColor,
redStr,
"The page shouldn't be red.");
gBrowser.removeTab(tab);
},
// the following tests attempt to display modal dialogs. The test just
// relies on the fact that if the dialog was displayed the test will hang
// and timeout. IOW - the tests would pass if the dialogs appear and are
// manually closed by the user - so don't do that :) (obviously there is
// noone available to do that when run via tbpl etc, so this should be safe,
// and it's tricky to use the window-watcher to check a window *does not*
// appear - how long should the watcher be active before assuming it's not
// going to appear?)
function noAuthPrompt() {
let url = "http://mochi.test:8888/browser/browser/base/content/test/general/authenticate.sjs?user=anyone";
let file = fileForURL(url);
ok(!file.exists(), "Thumbnail file should not already exist.");
let capturedURL = yield capture(url);
is(capturedURL, url, "Captured URL should be URL passed to capture.");
ok(file.exists(),
"Thumbnail file should exist even though it requires auth.");
file.remove(false);
},
function noAlert() {
let url = "data:text/html,<script>try { alert('yo!'); } catch (e) {}</script>";
let file = fileForURL(url);
ok(!file.exists(), "Thumbnail file should not already exist.");
let capturedURL = yield capture(url);
is(capturedURL, url, "Captured URL should be URL passed to capture.");
ok(file.exists(),
"Thumbnail file should exist even though it alerted.");
file.remove(false);
},
function noDuplicates() {
let deferred = imports.Promise.defer();
let url = "http://example.com/1";
let file = fileForURL(url);
ok(!file.exists(), "Thumbnail file should not already exist.");
let numCallbacks = 0;
let doneCallback = function(doneUrl) {
is(doneUrl, url, "called back with correct url");
numCallbacks += 1;
// We will delete the file after the first callback, then check it
// still doesn't exist on the second callback, which should give us
// confidence that we didn't end up with 2 different captures happening
// for the same url...
if (numCallbacks == 1) {
ok(file.exists(), "Thumbnail file should now exist.");
file.remove(false);
return;
}
if (numCallbacks == 2) {
ok(!file.exists(), "Thumbnail file should still be deleted.");
// and that's all we expect, so we are done...
deferred.resolve();
return;
}
ok(false, "only expecting 2 callbacks");
}
imports.BackgroundPageThumbs.capture(url, {onDone: doneCallback});
imports.BackgroundPageThumbs.capture(url, {onDone: doneCallback});
yield deferred.promise;
},
function capIfMissing() {
let url = "http://example.com/";
let file = fileForURL(url);
ok(!file.exists(), "Thumbnail file should not already exist.");
let capturedURL = yield captureIfMissing(url);
is(capturedURL, url, "Captured URL should be URL passed to capture");
ok(file.exists(), "Thumbnail should be cached after capture: " + file.path);
let past = Date.now() - 1000000000;
let pastFudge = past + 30000;
file.lastModifiedTime = past;
ok(file.lastModifiedTime < pastFudge, "Last modified time should stick!");
capturedURL = yield captureIfMissing(url);
is(capturedURL, url, "Captured URL should be URL passed to second capture");
ok(file.exists(), "Thumbnail should remain cached after second capture: " +
file.path);
ok(file.lastModifiedTime < pastFudge,
"File should not have been overwritten");
file.remove(false);
},
];
function capture(url, options) {
return captureWithMethod("capture", url, options);
}
function captureIfMissing(url, options) {
return captureWithMethod("captureIfMissing", url, options);
}
function captureWithMethod(methodName, url, options={}) {
let deferred = imports.Promise.defer();
options.onDone = function onDone(capturedURL) {
deferred.resolve(capturedURL);
};
imports.BackgroundPageThumbs[methodName](url, options);
return deferred.promise;
}
function testPageURL(opts) {
return TEST_PAGE_URL + "?" + encodeURIComponent(JSON.stringify(opts || {}));
}
function fileForURL(url) {
let path = imports.PageThumbsStorage.getFilePathForURL(url);
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
file.initWithPath(path);
return file;
}
function wait(ms) {
let deferred = imports.Promise.defer();
setTimeout(function onTimeout() {
deferred.resolve();
}, ms);
return deferred.promise;
}
function openPrivateWindow() {
let deferred = imports.Promise.defer();
// from OpenBrowserWindow in browser.js
let win = window.openDialog("chrome://browser/content/", "_blank",
"chrome,all,dialog=no,private",
"about:privatebrowsing");
win.addEventListener("load", function load(event) {
if (event.target == win.document) {
win.removeEventListener("load", load);
deferred.resolve(win);
}
});
return deferred.promise;
}
function onPageLoad(browser) {
let deferred = imports.Promise.defer();
browser.addEventListener("load", function load(event) {
if (event.target == browser.contentWindow.document) {
browser.removeEventListener("load", load, true);
deferred.resolve();
}
}, true);
return deferred.promise;
}

View File

@ -0,0 +1,13 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function runTests() {
let url = "http://www.example.com/";
ok(!thumbnailExists(url), "Thumbnail should not be cached yet.");
let capturedURL = yield bgCapture(url);
is(capturedURL, url, "Captured URL should be URL passed to capture");
ok(thumbnailExists(url), "Thumbnail should be cached after capture");
removeThumbnail(url);
}

View File

@ -0,0 +1,24 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function runTests() {
let url = "http://example.com/";
let file = thumbnailFile(url);
ok(!file.exists(), "Thumbnail file should not already exist.");
let capturedURL = yield bgCaptureIfMissing(url);
is(capturedURL, url, "Captured URL should be URL passed to capture");
ok(file.exists(url), "Thumbnail should be cached after capture");
let past = Date.now() - 1000000000;
let pastFudge = past + 30000;
file.lastModifiedTime = past;
ok(file.lastModifiedTime < pastFudge, "Last modified time should stick!");
capturedURL = yield bgCaptureIfMissing(url);
is(capturedURL, url, "Captured URL should be URL passed to second capture");
ok(file.exists(), "Thumbnail should remain cached after second capture");
ok(file.lastModifiedTime < pastFudge,
"File should not have been overwritten");
file.remove(false);
}

View File

@ -0,0 +1,29 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function runTests() {
let url1 = "http://example.com/1";
ok(!thumbnailExists(url1), "First file should not exist yet.");
let url2 = "http://example.com/2";
ok(!thumbnailExists(url2), "Second file should not exist yet.");
let defaultTimeout = BackgroundPageThumbs._destroyBrowserTimeout;
BackgroundPageThumbs._destroyBrowserTimeout = 1000;
yield bgCapture(url1);
ok(thumbnailExists(url1), "First file should exist after capture.");
removeThumbnail(url1);
yield wait(2000);
is(BackgroundPageThumbs._thumbBrowser, undefined,
"Thumb browser should be destroyed after timeout.");
BackgroundPageThumbs._destroyBrowserTimeout = defaultTimeout;
yield bgCapture(url2);
ok(thumbnailExists(url2), "Second file should exist after capture.");
removeThumbnail(url2);
isnot(BackgroundPageThumbs._thumbBrowser, undefined,
"Thumb browser should exist immediately after capture.");
}

View File

@ -0,0 +1,13 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function runTests() {
let url = "data:text/html,<script>try { alert('yo!'); } catch (e) {}</script>";
ok(!thumbnailExists(url), "Thumbnail file should not already exist.");
let capturedURL = yield bgCapture(url);
is(capturedURL, url, "Captured URL should be URL passed to capture.");
ok(thumbnailExists(url),
"Thumbnail file should exist even though it alerted.");
removeThumbnail(url);
}

View File

@ -0,0 +1,21 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// the following tests attempt to display modal dialogs. The test just
// relies on the fact that if the dialog was displayed the test will hang
// and timeout. IOW - the tests would pass if the dialogs appear and are
// manually closed by the user - so don't do that :) (obviously there is
// noone available to do that when run via tbpl etc, so this should be safe,
// and it's tricky to use the window-watcher to check a window *does not*
// appear - how long should the watcher be active before assuming it's not
// going to appear?)
function runTests() {
let url = "http://mochi.test:8888/browser/browser/base/content/test/general/authenticate.sjs?user=anyone";
ok(!thumbnailExists(url), "Thumbnail file should not already exist.");
let capturedURL = yield bgCapture(url);
is(capturedURL, url, "Captured URL should be URL passed to capture.");
ok(thumbnailExists(url),
"Thumbnail file should exist even though it requires auth.");
removeThumbnail(url);
}

View File

@ -0,0 +1,38 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function runTests() {
// Visit the test page in the browser and tell it to set a cookie.
let url = bgTestPageURL({ setGreenCookie: true });
let tab = gBrowser.loadOneTab(url, { inBackground: false });
let browser = tab.linkedBrowser;
yield whenLoaded(browser);
// The root element of the page shouldn't be green yet.
let greenStr = "rgb(0, 255, 0)";
isnot(browser.contentDocument.documentElement.style.backgroundColor,
greenStr,
"The page shouldn't be green yet.");
// Cookie should be set now. Reload the page to verify. Its root element
// will be green if the cookie's set.
browser.reload();
yield whenLoaded(browser);
is(browser.contentDocument.documentElement.style.backgroundColor,
greenStr,
"The page should be green now.");
// Capture the page. Get the image data of the capture and verify it's not
// green. (Checking only the first pixel suffices.)
yield bgCapture(url);
ok(thumbnailExists(url), "Thumbnail file should exist after capture.");
retrieveImageDataForURL(url, function ([r, g, b]) {
isnot([r, g, b].toString(), [0, 255, 0].toString(),
"The captured page should not be green.");
gBrowser.removeTab(tab);
removeThumbnail(url);
next();
});
yield true;
}

View File

@ -0,0 +1,24 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// check that if a page captured in the background attempts to set a cookie,
// that cookie is not saved for subsequent requests.
function runTests() {
let url = bgTestPageURL({ setRedCookie: true });
ok(!thumbnailExists(url), "Thumbnail file should not exist before capture.");
yield bgCapture(url);
ok(thumbnailExists(url), "Thumbnail file should exist after capture.");
removeThumbnail(url);
// now load it up in a browser - it should *not* be red, otherwise the
// cookie above was saved.
let tab = gBrowser.loadOneTab(url, { inBackground: false });
let browser = tab.linkedBrowser;
yield whenLoaded(browser);
// The root element of the page shouldn't be red.
let redStr = "rgb(255, 0, 0)";
isnot(browser.contentDocument.documentElement.style.backgroundColor,
redStr,
"The page shouldn't be red.");
gBrowser.removeTab(tab);
}

View File

@ -0,0 +1,31 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function runTests() {
let url = "http://example.com/1";
ok(!thumbnailExists(url), "Thumbnail file should not already exist.");
let numCallbacks = 0;
let doneCallback = function(doneUrl) {
is(doneUrl, url, "called back with correct url");
numCallbacks += 1;
// We will delete the file after the first callback, then check it
// still doesn't exist on the second callback, which should give us
// confidence that we didn't end up with 2 different captures happening
// for the same url...
if (numCallbacks == 1) {
ok(thumbnailExists(url), "Thumbnail file should now exist.");
removeThumbnail(url);
return;
}
if (numCallbacks == 2) {
ok(!thumbnailExists(url), "Thumbnail file should still be deleted.");
// and that's all we expect, so we are done...
next();
return;
}
ok(false, "only expecting 2 callbacks");
}
BackgroundPageThumbs.capture(url, {onDone: doneCallback});
BackgroundPageThumbs.capture(url, {onDone: doneCallback});
yield true;
}

View File

@ -0,0 +1,37 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function runTests() {
let urls = [
"http://www.example.com/0",
"http://www.example.com/1",
// an item that will timeout to ensure timeouts work and we resume.
bgTestPageURL({ wait: 2002 }),
"http://www.example.com/2",
];
urls.forEach(url => {
ok(!thumbnailExists(url), "Thumbnail should not exist yet: " + url);
let isTimeoutTest = url.indexOf("wait") >= 0;
BackgroundPageThumbs.capture(url, {
timeout: isTimeoutTest ? 100 : 30000,
onDone: function onDone(capturedURL) {
ok(urls.length > 0, "onDone called, so URLs should still remain");
is(capturedURL, urls.shift(),
"Captured URL should be currently expected URL (i.e., " +
"capture() callbacks should be called in the correct order)");
if (isTimeoutTest) {
ok(!thumbnailExists(capturedURL),
"Thumbnail shouldn't exist for timed out capture");
} else {
ok(thumbnailExists(capturedURL),
"Thumbnail should be cached after capture");
removeThumbnail(url);
}
if (!urls.length)
// Test done.
next();
},
});
});
yield true;
}

View File

@ -0,0 +1,23 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function runTests() {
let finalURL = "http://example.com/redirected";
let originalURL = bgTestPageURL({ redirect: finalURL });
ok(!thumbnailExists(originalURL),
"Thumbnail file for original URL should not exist yet.");
ok(!thumbnailExists(finalURL),
"Thumbnail file for final URL should not exist yet.");
let capturedURL = yield bgCapture(originalURL);
is(capturedURL, originalURL,
"Captured URL should be URL passed to capture");
ok(thumbnailExists(originalURL),
"Thumbnail for original URL should be cached");
ok(thumbnailExists(finalURL),
"Thumbnail for final URL should be cached");
removeThumbnail(originalURL);
removeThumbnail(finalURL);
}

View File

@ -0,0 +1,19 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function runTests() {
let url = bgTestPageURL({ wait: 30000 });
ok(!thumbnailExists(url), "Thumbnail should not be cached already.");
let numCalls = 0;
BackgroundPageThumbs.capture(url, {
timeout: 0,
onDone: function onDone(capturedURL) {
is(capturedURL, url, "Captured URL should be URL passed to capture");
is(numCalls++, 0, "onDone should be called only once");
ok(!thumbnailExists(url),
"Capture timed out so thumbnail should not be cached");
next();
},
});
yield true;
}

View File

@ -3,10 +3,11 @@
let tmp = {};
Cu.import("resource://gre/modules/PageThumbs.jsm", tmp);
Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", tmp);
Cu.import("resource:///modules/sessionstore/SessionStore.jsm", tmp);
Cu.import("resource://gre/modules/FileUtils.jsm", tmp);
Cu.import("resource://gre/modules/osfile.jsm", tmp);
let {PageThumbs, PageThumbsStorage, SessionStore, FileUtils, OS} = tmp;
let {PageThumbs, BackgroundPageThumbs, PageThumbsStorage, SessionStore, FileUtils, OS} = tmp;
Cu.import("resource://gre/modules/PlacesUtils.jsm");
@ -48,10 +49,12 @@ let TestRunner = {
/**
* Runs the next available test or finishes if there's no test left.
* @param aValue This value will be passed to the yielder via the runner's
* iterator.
*/
next: function () {
next: function (aValue) {
try {
let value = TestRunner._iter.next();
let value = TestRunner._iter.send(aValue);
if (value && typeof value.then == "function") {
value.then(result => {
next(result);
@ -67,9 +70,11 @@ let TestRunner = {
/**
* Continues the current test execution.
* @param aValue This value will be passed to the yielder via the runner's
* iterator.
*/
function next() {
TestRunner.next();
function next(aValue) {
TestRunner.next(aValue);
}
/**
@ -164,15 +169,32 @@ function retrieveImageDataForURL(aURL, aCallback) {
});
}
/**
* Returns the file of the thumbnail with the given URL.
* @param aURL The URL of the thumbnail.
*/
function thumbnailFile(aURL) {
return new FileUtils.File(PageThumbsStorage.getFilePathForURL(aURL));
}
/**
* Checks if a thumbnail for the given URL exists.
* @param aURL The url associated to the thumbnail.
*/
function thumbnailExists(aURL) {
let file = new FileUtils.File(PageThumbsStorage.getFilePathForURL(aURL));
let file = thumbnailFile(aURL);
return file.exists() && file.fileSize;
}
/**
* Removes the thumbnail for the given URL.
* @param aURL The URL associated with the thumbnail.
*/
function removeThumbnail(aURL) {
let file = thumbnailFile(aURL);
file.remove(false);
}
/**
* Asynchronously adds visits to a page, invoking a callback function when done.
*
@ -263,6 +285,10 @@ function whenFileRemoved(aFile, aCallback) {
executeSoon(callback || next);
}
function wait(aMillis) {
setTimeout(next, aMillis);
}
/**
* Makes sure that a given list of URLs is not implicitly expired.
*
@ -276,3 +302,21 @@ function dontExpireThumbnailURLs(aURLs) {
PageThumbs.removeExpirationFilter(dontExpireURLs);
});
}
function bgCapture(aURL, aOptions) {
bgCaptureWithMethod("capture", aURL, aOptions);
}
function bgCaptureIfMissing(aURL, aOptions) {
bgCaptureWithMethod("captureIfMissing", aURL, aOptions);
}
function bgCaptureWithMethod(aMethodName, aURL, aOptions = {}) {
aOptions.onDone = next;
BackgroundPageThumbs[aMethodName](aURL, aOptions);
}
function bgTestPageURL(aOpts = {}) {
let TEST_PAGE_URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_background.sjs";
return TEST_PAGE_URL + "?" + encodeURIComponent(JSON.stringify(aOpts));
}

View File

@ -195,7 +195,7 @@ function onFinish()
// Create profile named profileName in profileRoot.
try {
profile = gProfileService.createProfile(gProfileRoot, null, profileName);
profile = gProfileService.createProfile(gProfileRoot, profileName);
}
catch (e) {
var profileCreationFailed =

View File

@ -10,7 +10,7 @@ interface nsIFile;
interface nsIToolkitProfile;
interface nsIProfileLock;
[scriptable, uuid(b619f83d-8317-473c-b342-67905993fdc7)]
[scriptable, uuid(b81c33a6-1ce8-4695-856b-02b7f15cc114)]
interface nsIToolkitProfileService : nsISupports
{
attribute boolean startWithLastProfile;
@ -37,19 +37,17 @@ interface nsIToolkitProfileService : nsISupports
/**
* Create a new profile.
*
* The profile temporary directory will be chosen based on where the
* profile directory is located.
*
* @param aRootDir
* The profile directory. May be null, in which case a suitable
* default will be chosen based on the profile name.
* @param aTempDir
* The profile temporary directory. May be null, in which case a
* suitable default will be chosen based either on the profile name
* if aRootDir is null or aRootDir itself.
* @param aName
* The profile name.
*/
nsIToolkitProfile createProfile(in nsIFile aRootDir,
in nsIFile aTempDir,
in AUTF8String aName);
/**

View File

@ -127,7 +127,6 @@ private:
nsresult CreateTimesInternal(nsIFile *profileDir);
nsresult CreateProfileInternal(nsIFile* aRootDir,
nsIFile* aLocalDir,
const nsACString& aName,
const nsACString* aProfileName,
const nsACString* aAppName,
@ -656,7 +655,7 @@ nsToolkitProfileService::CreateDefaultProfileForApp(const nsACString& aProfileNa
NS_ENSURE_FALSE(exists, NS_ERROR_ALREADY_INITIALIZED);
nsIFile* profileDefaultsDir = aProfileDefaultsDir;
rv = CreateProfileInternal(nullptr, nullptr,
rv = CreateProfileInternal(nullptr,
NS_LITERAL_CSTRING("default"),
&aProfileName, &aAppName, &aVendorName,
&profileDefaultsDir, true, aResult);
@ -698,17 +697,15 @@ nsToolkitProfileService::CreateDefaultProfileForApp(const nsACString& aProfileNa
NS_IMETHODIMP
nsToolkitProfileService::CreateProfile(nsIFile* aRootDir,
nsIFile* aLocalDir,
const nsACString& aName,
nsIToolkitProfile** aResult)
{
return CreateProfileInternal(aRootDir, aLocalDir, aName,
return CreateProfileInternal(aRootDir, aName,
nullptr, nullptr, nullptr, nullptr, false, aResult);
}
nsresult
nsToolkitProfileService::CreateProfileInternal(nsIFile* aRootDir,
nsIFile* aLocalDir,
const nsACString& aName,
const nsACString* aProfileName,
const nsACString* aAppName,
@ -745,26 +742,22 @@ nsToolkitProfileService::CreateProfileInternal(nsIFile* aRootDir,
}
}
nsCOMPtr<nsIFile> localDir (aLocalDir);
nsCOMPtr<nsIFile> localDir;
if (!localDir) {
if (aRootDir) {
localDir = aRootDir;
}
else {
rv = gDirServiceProvider->GetUserProfilesLocalDir(getter_AddRefs(localDir),
aProfileName,
aAppName,
aVendorName);
NS_ENSURE_SUCCESS(rv, rv);
bool isRelative;
rv = mAppData->Contains(rootDir, true, &isRelative);
if (NS_SUCCEEDED(rv) && isRelative) {
nsAutoCString path;
rv = rootDir->GetRelativeDescriptor(mAppData, path);
NS_ENSURE_SUCCESS(rv, rv);
// use same salting
if (NS_IsNativeUTF8()) {
localDir->AppendNative(dirName);
} else {
localDir->Append(NS_ConvertUTF8toUTF16(dirName));
}
}
rv = NS_NewNativeLocalFile(EmptyCString(), true,
getter_AddRefs(localDir));
NS_ENSURE_SUCCESS(rv, rv);
rv = localDir->SetRelativeDescriptor(mTempData, path);
} else {
localDir = rootDir;
}
bool exists;

View File

@ -29,15 +29,23 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=543854
const ASCIIName = "myprofile";
const UnicodeName = "\u09A0\u09BE\u0995\u09C1\u09B0"; // A Bengali name
var gDirService;
var gIOService;
var gProfileService;
var gDefaultLocalProfileParent;
gDirService = Cc["@mozilla.org/file/directory_service;1"].
getService(Ci.nsIProperties);
gIOService = Cc["@mozilla.org/network/io-service;1"].
getService(Ci.nsIIOService);
gProfileService = Cc["@mozilla.org/toolkit/profile-service;1"].
getService(Ci.nsIToolkitProfileService);
gDefaultLocalProfileParent = gDirService.get("DefProfLRt", Ci.nsIFile);
createProfile(ASCIIName);
createProfile(UnicodeName);
SimpleTest.finish();
@ -76,7 +84,7 @@ function createProfile(profileName) {
// Filesystem precision is lower than Date precision.
let lowerBound = Date.now() - 1000;
let profile = gProfileService.createProfile(null, null, profileName);
let profile = gProfileService.createProfile(null, profileName);
// check that the directory was created
isnot(profile, null, "Profile " + profileName + " created");
@ -107,6 +115,16 @@ function createProfile(profileName) {
// Check against real clock time.
checkBounds(lowerBound, created, upperBound);
// Clean up the profile before local profile test.
profile.remove(true);
// Create with non-null aRootDir
let profile = gProfileService.createProfile(profileDir, profileName);
let localProfileDir = profile.localDir;
ok(gDefaultLocalProfileParent.contains(localProfileDir, false),
"Local profile dir created in DefProfLRt");
// Clean up the profile.
profile.remove(true);
}

View File

@ -40,7 +40,6 @@ CreateResetProfile(nsIToolkitProfileService* aProfileSvc, nsIToolkitProfile* *aN
nsAutoCString newProfileName("default-");
newProfileName.Append(nsPrintfCString("%lld", PR_Now() / 1000));
nsresult rv = aProfileSvc->CreateProfile(nullptr, // choose a default dir for us
nullptr, // choose a default dir for us
newProfileName,
getter_AddRefs(newProfile));
if (NS_FAILED(rv)) return rv;

View File

@ -2185,12 +2185,12 @@ SelectProfile(nsIProfileLock* *aResult, nsIToolkitProfileService* aProfileSvc, n
return rv;
}
// As with -profile, assume that the given path will be used for both the
// main profile directory and the temp profile directory.
rv = aProfileSvc->CreateProfile(lf, lf, nsDependentCSubstring(arg, delim),
// As with -profile, assume that the given path will be used for the
// main profile directory.
rv = aProfileSvc->CreateProfile(lf, nsDependentCSubstring(arg, delim),
getter_AddRefs(profile));
} else {
rv = aProfileSvc->CreateProfile(nullptr, nullptr, nsDependentCString(arg),
rv = aProfileSvc->CreateProfile(nullptr, nsDependentCString(arg),
getter_AddRefs(profile));
}
// Some pathological arguments can make it this far
@ -2275,7 +2275,6 @@ SelectProfile(nsIProfileLock* *aResult, nsIToolkitProfileService* aProfileSvc, n
// create a default profile
nsCOMPtr<nsIToolkitProfile> profile;
nsresult rv = aProfileSvc->CreateProfile(nullptr, // choose a default dir for us
nullptr, // choose a default dir for us
NS_LITERAL_CSTRING("default"),
getter_AddRefs(profile));
if (NS_SUCCEEDED(rv)) {