Merge mozilla-central to mozilla-inbound
@ -33,7 +33,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.0.0'
|
||||
classpath 'com.android.tools.build:gradle:2.1.0'
|
||||
classpath('com.stanfy.spoon:spoon-gradle-plugin:1.0.4') {
|
||||
// Without these, we get errors linting.
|
||||
exclude module: 'guava'
|
||||
|
@ -44,46 +44,74 @@
|
||||
label="&inspectorHTMLEdit.label;"
|
||||
accesskey="&inspectorHTMLEdit.accesskey;"
|
||||
oncommand="inspector.editHTML()"/>
|
||||
<menuitem id="node-menu-copyinner"
|
||||
label="&inspectorHTMLCopyInner.label;"
|
||||
accesskey="&inspectorHTMLCopyInner.accesskey;"
|
||||
oncommand="inspector.copyInnerHTML()"/>
|
||||
<menuitem id="node-menu-copyouter"
|
||||
label="&inspectorHTMLCopyOuter.label;"
|
||||
accesskey="&inspectorHTMLCopyOuter.accesskey;"
|
||||
oncommand="inspector.copyOuterHTML()"/>
|
||||
<menuitem id="node-menu-copyuniqueselector"
|
||||
label="&inspectorCopyUniqueSelector.label;"
|
||||
accesskey="&inspectorCopyUniqueSelector.accesskey;"
|
||||
oncommand="inspector.copyUniqueSelector()"/>
|
||||
<menuitem id="node-menu-copyimagedatauri"
|
||||
label="&inspectorCopyImageDataUri.label;"
|
||||
oncommand="inspector.copyImageDataUri()"/>
|
||||
<menuitem id="node-menu-showdomproperties"
|
||||
label="&inspectorShowDOMProperties.label;"
|
||||
oncommand="inspector.showDOMProperties()"/>
|
||||
<menuitem id="node-menu-useinconsole"
|
||||
label="&inspectorUseInConsole.label;"
|
||||
oncommand="inspector.useInConsole()"/>
|
||||
<menuitem id="node-menu-expand"
|
||||
label="&inspectorExpandNode.label;"
|
||||
oncommand="inspector.expandNode()"/>
|
||||
<menuitem id="node-menu-collapse"
|
||||
label="&inspectorCollapseNode.label;"
|
||||
oncommand="inspector.collapseNode()"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="node-menu-pasteinnerhtml"
|
||||
label="&inspectorHTMLPasteInner.label;"
|
||||
accesskey="&inspectorHTMLPasteInner.accesskey;"
|
||||
oncommand="inspector.pasteInnerHTML()"/>
|
||||
<menuitem id="node-menu-pasteouterhtml"
|
||||
label="&inspectorHTMLPasteOuter.label;"
|
||||
accesskey="&inspectorHTMLPasteOuter.accesskey;"
|
||||
oncommand="inspector.pasteOuterHTML()"/>
|
||||
<menu id="node-menu-paste-extra-submenu"
|
||||
label="&inspectorHTMLPasteExtraSubmenu.label;"
|
||||
accesskey="&inspectorHTMLPasteExtraSubmenu.accesskey;">
|
||||
<menuitem id="node-menu-add"
|
||||
label="&inspectorAddNode.label;"
|
||||
accesskey="&inspectorAddNode.accesskey;"
|
||||
oncommand="inspector.addNode()"/>
|
||||
<menuitem id="node-menu-duplicatenode"
|
||||
label="&inspectorDuplicateNode.label;"
|
||||
oncommand="inspector.duplicateNode()"/>
|
||||
<menuitem id="node-menu-delete"
|
||||
label="&inspectorHTMLDelete.label;"
|
||||
accesskey="&inspectorHTMLDelete.accesskey;"
|
||||
oncommand="inspector.deleteNode()"/>
|
||||
<menu label="&inspectorAttributesSubmenu.label;"
|
||||
accesskey="&inspectorAttributesSubmenu.accesskey;">
|
||||
<menupopup>
|
||||
<menuitem id="node-menu-add-attribute"
|
||||
label="&inspectorAddAttribute.label;"
|
||||
accesskey="&inspectorAddAttribute.accesskey;"
|
||||
oncommand="inspector.onAddAttribute()"/>
|
||||
<menuitem id="node-menu-edit-attribute"
|
||||
label="&inspectorEditAttribute.label;"
|
||||
accesskey="&inspectorEditAttribute.accesskey;"
|
||||
oncommand="inspector.onEditAttribute()"/>
|
||||
<menuitem id="node-menu-remove-attribute"
|
||||
label="&inspectorRemoveAttribute.label;"
|
||||
accesskey="&inspectorRemoveAttribute.accesskey;"
|
||||
oncommand="inspector.onRemoveAttribute()"/>
|
||||
</menupopup>
|
||||
</menu>
|
||||
<menuseparator/>
|
||||
<menuitem id="node-menu-pseudo-hover"
|
||||
label=":hover" type="checkbox"
|
||||
oncommand="inspector.togglePseudoClass(':hover')"/>
|
||||
<menuitem id="node-menu-pseudo-active"
|
||||
label=":active" type="checkbox"
|
||||
oncommand="inspector.togglePseudoClass(':active')"/>
|
||||
<menuitem id="node-menu-pseudo-focus"
|
||||
label=":focus" type="checkbox"
|
||||
oncommand="inspector.togglePseudoClass(':focus')"/>
|
||||
<menuseparator/>
|
||||
<menu label="&inspectorCopyHTMLSubmenu.label;">
|
||||
<menupopup>
|
||||
<menuitem id="node-menu-copyinner"
|
||||
label="&inspectorCopyInnerHTML.label;"
|
||||
accesskey="&inspectorCopyInnerHTML.accesskey;"
|
||||
oncommand="inspector.copyInnerHTML()"/>
|
||||
<menuitem id="node-menu-copyouter"
|
||||
label="&inspectorCopyOuterHTML.label;"
|
||||
accesskey="&inspectorCopyOuterHTML.accesskey;"
|
||||
oncommand="inspector.copyOuterHTML()"/>
|
||||
<menuitem id="node-menu-copyuniqueselector"
|
||||
label="&inspectorCopyCSSSelector.label;"
|
||||
accesskey="&inspectorCopyCSSSelector.accesskey;"
|
||||
oncommand="inspector.copyUniqueSelector()"/>
|
||||
<menuitem id="node-menu-copyimagedatauri"
|
||||
label="&inspectorImageDataUri.label;"
|
||||
oncommand="inspector.copyImageDataUri()"/>
|
||||
</menupopup>
|
||||
</menu>
|
||||
<menu label="&inspectorPasteHTMLSubmenu.label;">
|
||||
<menupopup>
|
||||
<menuitem id="node-menu-pasteinnerhtml"
|
||||
label="&inspectorPasteInnerHTML.label;"
|
||||
accesskey="&inspectorPasteInnerHTML.accesskey;"
|
||||
oncommand="inspector.pasteInnerHTML()"/>
|
||||
<menuitem id="node-menu-pasteouterhtml"
|
||||
label="&inspectorPasteOuterHTML.label;"
|
||||
accesskey="&inspectorPasteOuterHTML.accesskey;"
|
||||
oncommand="inspector.pasteOuterHTML()"/>
|
||||
<menuitem id="node-menu-pastebefore"
|
||||
label="&inspectorHTMLPasteBefore.label;"
|
||||
accesskey="&inspectorHTMLPasteBefore.accesskey;"
|
||||
@ -103,6 +131,13 @@
|
||||
</menupopup>
|
||||
</menu>
|
||||
<menuseparator/>
|
||||
<menuitem id="node-menu-expand"
|
||||
label="&inspectorExpandNode.label;"
|
||||
oncommand="inspector.expandNode()"/>
|
||||
<menuitem id="node-menu-collapse"
|
||||
label="&inspectorCollapseNode.label;"
|
||||
oncommand="inspector.collapseNode()"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="node-menu-scrollnodeintoview"
|
||||
label="&inspectorScrollNodeIntoView.label;"
|
||||
accesskey="&inspectorScrollNodeIntoView.accesskey;"
|
||||
@ -110,49 +145,17 @@
|
||||
<menuitem id="node-menu-screenshotnode"
|
||||
label="&inspectorScreenshotNode.label;"
|
||||
oncommand="inspector.screenshotNode()" />
|
||||
<menuitem id="node-menu-add"
|
||||
label="&inspectorAddNode.label;"
|
||||
accesskey="&inspectorAddNode.accesskey;"
|
||||
oncommand="inspector.addNode()"/>
|
||||
<menuitem id="node-menu-duplicatenode"
|
||||
label="&inspectorDuplicateNode.label;"
|
||||
oncommand="inspector.duplicateNode()"/>
|
||||
<menuitem id="node-menu-delete"
|
||||
label="&inspectorHTMLDelete.label;"
|
||||
accesskey="&inspectorHTMLDelete.accesskey;"
|
||||
oncommand="inspector.deleteNode()"/>
|
||||
<menu label="&inspectorAttributeSubmenu.label;"
|
||||
accesskey="&inspectorAttributeSubmenu.accesskey;">
|
||||
<menupopup>
|
||||
<menuitem id="node-menu-add-attribute"
|
||||
label="&inspectorAddAttribute.label;"
|
||||
accesskey="&inspectorAddAttribute.accesskey;"
|
||||
oncommand="inspector.onAddAttribute()"/>
|
||||
<menuitem id="node-menu-edit-attribute"
|
||||
label="&inspectorEditAttribute.label;"
|
||||
accesskey="&inspectorEditAttribute.accesskey;"
|
||||
oncommand="inspector.onEditAttribute()"/>
|
||||
<menuitem id="node-menu-remove-attribute"
|
||||
label="&inspectorRemoveAttribute.label;"
|
||||
accesskey="&inspectorRemoveAttribute.accesskey;"
|
||||
oncommand="inspector.onRemoveAttribute()"/>
|
||||
</menupopup>
|
||||
</menu>
|
||||
<menuitem id="node-menu-useinconsole"
|
||||
label="&inspectorUseInConsole.label;"
|
||||
oncommand="inspector.useInConsole()"/>
|
||||
<menuitem id="node-menu-showdomproperties"
|
||||
label="&inspectorShowDOMProperties.label;"
|
||||
oncommand="inspector.showDOMProperties()"/>
|
||||
<menuseparator id="node-menu-link-separator"/>
|
||||
<menuitem id="node-menu-link-follow"
|
||||
oncommand="inspector.onFollowLink()"/>
|
||||
<menuitem id="node-menu-link-copy"
|
||||
oncommand="inspector.onCopyLink()"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="node-menu-pseudo-hover"
|
||||
label=":hover" type="checkbox"
|
||||
oncommand="inspector.togglePseudoClass(':hover')"/>
|
||||
<menuitem id="node-menu-pseudo-active"
|
||||
label=":active" type="checkbox"
|
||||
oncommand="inspector.togglePseudoClass(':active')"/>
|
||||
<menuitem id="node-menu-pseudo-focus"
|
||||
label=":focus" type="checkbox"
|
||||
oncommand="inspector.togglePseudoClass(':focus')"/>
|
||||
</menupopup>
|
||||
</popupset>
|
||||
|
||||
|
@ -59,6 +59,9 @@ loader.lazyGetter(this, "AutocompletePopup", () => {
|
||||
return require("devtools/client/shared/autocomplete-popup").AutocompletePopup;
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
|
||||
"resource://gre/modules/PluralForm.jsm");
|
||||
|
||||
/**
|
||||
* Vocabulary for the purposes of this file:
|
||||
*
|
||||
@ -1548,11 +1551,12 @@ MarkupView.prototype = {
|
||||
}
|
||||
|
||||
if (!(children.hasFirst && children.hasLast)) {
|
||||
let nodesCount = container.node.numChildren;
|
||||
let showAllString = PluralForm.get(nodesCount,
|
||||
this.strings.GetStringFromName("markupView.more.showAll2"));
|
||||
let data = {
|
||||
showing: this.strings.GetStringFromName("markupView.more.showing"),
|
||||
showAll: this.strings.formatStringFromName(
|
||||
"markupView.more.showAll",
|
||||
[container.node.numChildren.toString()], 1),
|
||||
showAll: showAllString.replace("#1", nodesCount),
|
||||
allButtonClick: () => {
|
||||
container.maxChildren = -1;
|
||||
container.childrenDirty = true;
|
||||
|
@ -53,6 +53,7 @@ support-files =
|
||||
[browser_rules_add-rule_04.js]
|
||||
[browser_rules_add-rule_05.js]
|
||||
[browser_rules_add-rule_pseudo_class.js]
|
||||
[browser_rules_add-rule_iframes.js]
|
||||
[browser_rules_authored.js]
|
||||
[browser_rules_authored_color.js]
|
||||
[browser_rules_authored_override.js]
|
||||
|
@ -0,0 +1,85 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests adding a rule on elements nested in iframes.
|
||||
|
||||
const TEST_URI =
|
||||
`<div>outer</div>
|
||||
<iframe id="frame1" src="data:text/html;charset=utf-8,<div>inner1</div>">
|
||||
</iframe>
|
||||
<iframe id="frame2" src="data:text/html;charset=utf-8,<div>inner2</div>">
|
||||
</iframe>`;
|
||||
|
||||
add_task(function* () {
|
||||
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
|
||||
let {inspector, view} = yield openRuleView();
|
||||
yield selectNode("div", inspector);
|
||||
yield addNewRule(inspector, view);
|
||||
yield testNewRule(view, "div", 1);
|
||||
yield addNewProperty(view, 1, "color", "red");
|
||||
|
||||
let innerFrameDiv1 = yield getNodeFrontInFrame("div", "#frame1", inspector);
|
||||
yield selectNode(innerFrameDiv1, inspector);
|
||||
yield addNewRule(inspector, view);
|
||||
yield testNewRule(view, "div", 1);
|
||||
yield addNewProperty(view, 1, "color", "blue");
|
||||
|
||||
let innerFrameDiv2 = yield getNodeFrontInFrame("div", "#frame2", inspector);
|
||||
yield selectNode(innerFrameDiv2, inspector);
|
||||
yield addNewRule(inspector, view);
|
||||
yield testNewRule(view, "div", 1);
|
||||
yield addNewProperty(view, 1, "color", "green");
|
||||
});
|
||||
|
||||
function* addNewRule(inspector, view) {
|
||||
info("Adding the new rule using the button");
|
||||
view.addRuleButton.click();
|
||||
info("Waiting for rule view to change");
|
||||
let onRuleViewChanged = once(view, "ruleview-changed");
|
||||
yield onRuleViewChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the newly created rule has the expected selector and submit the
|
||||
* selector editor.
|
||||
*/
|
||||
function* testNewRule(view, expected, index) {
|
||||
let idRuleEditor = getRuleViewRuleEditor(view, index);
|
||||
let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
|
||||
is(editor.value, expected,
|
||||
"Selector editor value is as expected: " + expected);
|
||||
|
||||
info("Entering the escape key");
|
||||
EventUtils.synthesizeKey("VK_ESCAPE", {});
|
||||
|
||||
is(idRuleEditor.selectorText.textContent, expected,
|
||||
"Selector text value is as expected: " + expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new property in the rule at the provided index in the rule view.
|
||||
*
|
||||
* @param {RuleView} view
|
||||
* @param {Number} index
|
||||
* The index of the rule in which we should add a new property.
|
||||
* @param {String} name
|
||||
* The name of the new property.
|
||||
* @param {String} value
|
||||
* The value of the new property.
|
||||
*/
|
||||
function* addNewProperty(view, index, name, value) {
|
||||
let idRuleEditor = getRuleViewRuleEditor(view, index);
|
||||
info(`Adding new property "${name}: ${value};"`);
|
||||
|
||||
let onRuleViewChanged = view.once("ruleview-changed");
|
||||
idRuleEditor.addProperty(name, value, "");
|
||||
yield onRuleViewChanged;
|
||||
|
||||
let textProps = idRuleEditor.rule.textProps;
|
||||
let lastProperty = textProps[textProps.length - 1];
|
||||
is(lastProperty.name, name, "Last property has the expected name");
|
||||
is(lastProperty.value, value, "Last property has the expected value");
|
||||
}
|
@ -4,35 +4,35 @@
|
||||
<!ENTITY inspectorHTMLEdit.label "Edit As HTML">
|
||||
<!ENTITY inspectorHTMLEdit.accesskey "E">
|
||||
|
||||
<!-- LOCALIZATION NOTE (inspectorHTMLCopyInner.label): This is the label shown
|
||||
<!-- LOCALIZATION NOTE (inspectorCopyInnerHTML.label): This is the label shown
|
||||
in the inspector contextual-menu for the item that lets users copy the
|
||||
inner HTML of the current node -->
|
||||
<!ENTITY inspectorHTMLCopyInner.label "Copy Inner HTML">
|
||||
<!ENTITY inspectorHTMLCopyInner.accesskey "I">
|
||||
<!ENTITY inspectorCopyInnerHTML.label "Inner HTML">
|
||||
<!ENTITY inspectorCopyInnerHTML.accesskey "I">
|
||||
|
||||
<!-- LOCALIZATION NOTE (inspectorHTMLCopyOuter.label): This is the label shown
|
||||
<!-- LOCALIZATION NOTE (inspectorCopyOuterHTML.label): This is the label shown
|
||||
in the inspector contextual-menu for the item that lets users copy the
|
||||
outer HTML of the current node -->
|
||||
<!ENTITY inspectorHTMLCopyOuter.label "Copy Outer HTML">
|
||||
<!ENTITY inspectorHTMLCopyOuter.accesskey "O">
|
||||
<!ENTITY inspectorCopyOuterHTML.label "Outer HTML">
|
||||
<!ENTITY inspectorCopyOuterHTML.accesskey "O">
|
||||
|
||||
<!-- LOCALIZATION NOTE (inspectorCopyUniqueSelector.label): This is the label
|
||||
<!-- LOCALIZATION NOTE (inspectorCopyCSSSelector.label): This is the label
|
||||
shown in the inspector contextual-menu for the item that lets users copy
|
||||
the CSS Selector of the current node -->
|
||||
<!ENTITY inspectorCopyUniqueSelector.label "Copy Unique Selector">
|
||||
<!ENTITY inspectorCopyUniqueSelector.accesskey "U">
|
||||
<!ENTITY inspectorCopyCSSSelector.label "CSS Selector">
|
||||
<!ENTITY inspectorCopyCSSSelector.accesskey "S">
|
||||
|
||||
<!-- LOCALIZATION NOTE (inspectorHTMLPasteOuter.label): This is the label shown
|
||||
<!-- LOCALIZATION NOTE (inspectorPasteOuterHTML.label): This is the label shown
|
||||
in the inspector contextual-menu for the item that lets users paste outer
|
||||
HTML in the current node -->
|
||||
<!ENTITY inspectorHTMLPasteOuter.label "Paste Outer HTML">
|
||||
<!ENTITY inspectorHTMLPasteOuter.accesskey "P">
|
||||
<!ENTITY inspectorPasteOuterHTML.label "Outer HTML">
|
||||
<!ENTITY inspectorPasteOuterHTML.accesskey "O">
|
||||
|
||||
<!-- LOCALIZATION NOTE (inspectorHTMLPasteInner.label): This is the label shown
|
||||
<!-- LOCALIZATION NOTE (inspectorPasteInnerHTML.label): This is the label shown
|
||||
in the inspector contextual-menu for the item that lets users paste inner
|
||||
HTML in the current node -->
|
||||
<!ENTITY inspectorHTMLPasteInner.label "Paste Inner HTML">
|
||||
<!ENTITY inspectorHTMLPasteInner.accesskey "N">
|
||||
<!ENTITY inspectorPasteInnerHTML.label "Inner HTML">
|
||||
<!ENTITY inspectorPasteInnerHTML.accesskey "I">
|
||||
|
||||
<!-- LOCALIZATION NOTE (inspectorHTMLPasteExtraSubmenu.label): This is the label
|
||||
shown in the inspector contextual-menu for the sub-menu of the other Paste
|
||||
@ -79,14 +79,15 @@
|
||||
current node -->
|
||||
<!ENTITY inspectorHTMLDelete.label "Delete Node">
|
||||
<!ENTITY inspectorHTMLDelete.accesskey "D">
|
||||
<!-- LOCALIZATION NOTE (inspectorAttributeSubmenu.label): This is the label
|
||||
|
||||
<!-- LOCALIZATION NOTE (inspectorAttributesSubmenu.label): This is the label
|
||||
shown in the inspector contextual-menu for the sub-menu of the other
|
||||
attribute items, which allow to:
|
||||
- add new attribute
|
||||
- edit attribute
|
||||
- remove attribute -->
|
||||
<!ENTITY inspectorAttributeSubmenu.label "Attribute">
|
||||
<!ENTITY inspectorAttributeSubmenu.accesskey "A">
|
||||
<!ENTITY inspectorAttributesSubmenu.label "Attributes">
|
||||
<!ENTITY inspectorAttributesSubmenu.accesskey "A">
|
||||
|
||||
<!-- LOCALIZATION NOTE (inspectorAddAttribute.label): This is the label shown in
|
||||
the inspector contextual-menu for the item that lets users add attribute
|
||||
@ -117,12 +118,12 @@
|
||||
shown as the placeholder for the markup view search in the inspector. -->
|
||||
<!ENTITY inspectorSearchHTML.label3 "Search HTML">
|
||||
|
||||
<!-- LOCALIZATION NOTE (inspectorCopyImageDataUri.label): This is the label
|
||||
<!-- LOCALIZATION NOTE (inspectorImageDataUri.label): This is the label
|
||||
shown in the inspector contextual-menu for the item that lets users copy
|
||||
the URL embedding the image data encoded in Base 64 (what we name
|
||||
here Image Data URL). For more information:
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs -->
|
||||
<!ENTITY inspectorCopyImageDataUri.label "Copy Image Data-URL">
|
||||
<!ENTITY inspectorImageDataUri.label "Image Data-URL">
|
||||
|
||||
<!-- LOCALIZATION NOTE (inspectorShowDOMProperties.label): This is the label
|
||||
shown in the inspector contextual-menu for the item that lets users see
|
||||
@ -161,3 +162,23 @@
|
||||
DOM (as children of the currently selected element). -->
|
||||
<!ENTITY inspectorAddNode.label "Create New Node">
|
||||
<!ENTITY inspectorAddNode.accesskey "C">
|
||||
|
||||
<!-- LOCALIZATION NOTE (inspectorCopyHTMLSubmenu.label): This is the label
|
||||
shown in the inspector contextual-menu for the sub-menu of the other
|
||||
copy items, which allow to:
|
||||
- Copy Inner HTML
|
||||
- Copy Outer HTML
|
||||
- Copy Unique selector
|
||||
- Copy Image data URI -->
|
||||
<!ENTITY inspectorCopyHTMLSubmenu.label "Copy">
|
||||
|
||||
<!-- LOCALIZATION NOTE (inspectorPasteHTMLSubmenu.label): This is the label
|
||||
shown in the inspector contextual-menu for the sub-menu of the other
|
||||
paste items, which allow to:
|
||||
- Paste Inner HTML
|
||||
- Paste Outer HTML
|
||||
- Before
|
||||
- After
|
||||
- As First Child
|
||||
- As Last Child -->
|
||||
<!ENTITY inspectorPasteHTMLSubmenu.label "Paste">
|
||||
|
@ -42,12 +42,17 @@ inspector.accesskey=I
|
||||
inspector.panelLabel=Inspector Panel
|
||||
inspector.panelLabel.markupView=Markup View
|
||||
|
||||
# LOCALIZATION NOTE (markupView.more.*)
|
||||
# LOCALIZATION NOTE (markupView.more.showing)
|
||||
# When there are too many nodes to load at once, we will offer to
|
||||
# show all the nodes.
|
||||
# Keyboard shortcut for DOM and Style Inspector will shown inside brackets.
|
||||
markupView.more.showing=Some nodes were hidden.
|
||||
markupView.more.showAll=Show All %S Nodes
|
||||
|
||||
# LOCALIZATION NOTE (markupView.more.showAll2): Semi-colon list of plural forms.
|
||||
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
|
||||
markupView.more.showAll2=Show one more node;Show all #1 nodes
|
||||
|
||||
# LOCALIZATION NOTE (inspector.tooltip2)
|
||||
# Keyboard shortcut for DOM and Style Inspector will be shown inside brackets.
|
||||
inspector.tooltip2=DOM and Style Inspector (%S)
|
||||
|
||||
#LOCALIZATION NOTE: Used in the image preview tooltip when the image could not be loaded
|
||||
|
@ -296,6 +296,9 @@ pref("devtools.webconsole.timestampMessages", false);
|
||||
// to automatically trigger multiline editing (equivalent to shift + enter).
|
||||
pref("devtools.webconsole.autoMultiline", true);
|
||||
|
||||
// Enable the experimental webconsole frontend (work in progress)
|
||||
pref("devtools.webconsole.new-frontend-enabled", false);
|
||||
|
||||
// The number of lines that are displayed in the web console for the Net,
|
||||
// CSS, JS and Web Developer categories. These defaults should be kept in sync
|
||||
// with DEFAULT_LOG_LIMIT in the webconsole frontend.
|
||||
|
@ -75,10 +75,10 @@ module.exports = createClass({
|
||||
}
|
||||
|
||||
let { lastClientX, lastClientY, ignoreX, ignoreY } = this.state;
|
||||
// we are resizing a centered viewport, so dragging a mouse resizes
|
||||
// twice as much - also on opposite side.
|
||||
// the viewport is centered horizontally, so horizontal resize resizes
|
||||
// by twice the distance the mouse was dragged - on left and right side.
|
||||
let deltaX = 2 * (clientX - lastClientX);
|
||||
let deltaY = 2 * (clientY - lastClientY);
|
||||
let deltaY = (clientY - lastClientY);
|
||||
|
||||
if (ignoreX) {
|
||||
deltaX = 0;
|
||||
|
@ -5,34 +5,55 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const TEST_URL = "about:logo";
|
||||
|
||||
function dragElementBy(selector, x, y, win) {
|
||||
function getElRect(selector, win) {
|
||||
let el = win.document.querySelector(selector);
|
||||
let rect = el.getBoundingClientRect();
|
||||
return el.getBoundingClientRect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag an element identified by 'selector' by [x,y] amount. Returns
|
||||
* the rect of the dragged element as it was before drag.
|
||||
*/
|
||||
function dragElementBy(selector, x, y, win) {
|
||||
let rect = getElRect(selector, win);
|
||||
let startPoint = [ rect.left + rect.width / 2, rect.top + rect.height / 2 ];
|
||||
let endPoint = [ startPoint[0] + x, startPoint[1] + y ];
|
||||
|
||||
EventUtils.synthesizeMouseAtPoint(...startPoint, { type: "mousedown" }, win);
|
||||
EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mousemove" }, win);
|
||||
EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mouseup" }, win);
|
||||
|
||||
return rect;
|
||||
}
|
||||
|
||||
function* testViewportResize(ui, selector, moveBy,
|
||||
expectedViewportSize, expectedHandleMove) {
|
||||
let win = ui.toolWindow;
|
||||
|
||||
let resized = waitForViewportResizeTo(ui, ...expectedViewportSize);
|
||||
let startRect = dragElementBy(selector, ...moveBy, win);
|
||||
yield resized;
|
||||
|
||||
let endRect = getElRect(selector, win);
|
||||
is(endRect.left - startRect.left, expectedHandleMove[0],
|
||||
`The x move of ${selector} is as expected`);
|
||||
is(endRect.top - startRect.top, expectedHandleMove[1],
|
||||
`The y move of ${selector} is as expected`);
|
||||
}
|
||||
|
||||
addRDMTask(TEST_URL, function* ({ ui, manager }) {
|
||||
ok(ui, "An instance of the RDM should be attached to the tab.");
|
||||
yield setViewportSize(ui, manager, 300, 300);
|
||||
let win = ui.toolWindow;
|
||||
|
||||
// Do horizontal + vertical resize
|
||||
let resized = waitForViewportResizeTo(ui, 320, 320);
|
||||
dragElementBy(".viewport-resize-handle", 10, 10, win);
|
||||
yield resized;
|
||||
yield testViewportResize(ui, ".viewport-resize-handle",
|
||||
[10, 10], [320, 310], [10, 10]);
|
||||
|
||||
// Do horizontal resize
|
||||
let hResized = waitForViewportResizeTo(ui, 300, 320);
|
||||
dragElementBy(".viewport-horizontal-resize-handle", -10, -10, win);
|
||||
yield hResized;
|
||||
yield testViewportResize(ui, ".viewport-horizontal-resize-handle",
|
||||
[-10, 10], [300, 310], [-10, 0]);
|
||||
|
||||
// Do vertical resize
|
||||
let vResized = waitForViewportResizeTo(ui, 300, 300);
|
||||
dragElementBy(".viewport-vertical-resize-handle", -10, -10, win);
|
||||
yield vResized;
|
||||
yield testViewportResize(ui, ".viewport-vertical-resize-handle",
|
||||
[-10, -10], [300, 300], [0, -10], ui);
|
||||
});
|
||||
|
@ -372,6 +372,11 @@ JSTerm.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
|
||||
this.hud.newConsoleOutput.dispatchMessageAdd(response);
|
||||
// @TODO figure out what to do about the callback.
|
||||
return;
|
||||
}
|
||||
let msg = new Messages.JavaScriptEvalOutput(response,
|
||||
errorMessage, errorDocLink);
|
||||
this.hud.output.addMessage(msg);
|
||||
@ -939,6 +944,10 @@ JSTerm.prototype = {
|
||||
|
||||
this._sidebarDestroy();
|
||||
|
||||
if (hud.NEW_CONSOLE_OUTPUT_ENABLED) {
|
||||
hud.newConsoleOutput.dispatchMessagesClear();
|
||||
}
|
||||
|
||||
this.emit("messages-cleared");
|
||||
},
|
||||
|
||||
|
@ -7,7 +7,8 @@
|
||||
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
|
||||
|
||||
DIRS += [
|
||||
'net'
|
||||
'net',
|
||||
'new-console-output',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
|
@ -0,0 +1,33 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
prepareMessage
|
||||
} = require("devtools/client/webconsole/new-console-output/utils/messages");
|
||||
|
||||
const {
|
||||
MESSAGE_ADD,
|
||||
MESSAGES_CLEAR,
|
||||
} = require("../constants");
|
||||
|
||||
function messageAdd(packet) {
|
||||
let message = prepareMessage(packet);
|
||||
return {
|
||||
type: MESSAGE_ADD,
|
||||
message
|
||||
};
|
||||
}
|
||||
|
||||
function messagesClear() {
|
||||
return {
|
||||
type: MESSAGES_CLEAR
|
||||
};
|
||||
}
|
||||
|
||||
exports.messageAdd = messageAdd;
|
||||
exports.messagesClear = messagesClear;
|
@ -0,0 +1,8 @@
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
DevToolsModules(
|
||||
'messages.js',
|
||||
)
|
@ -0,0 +1,68 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
createClass,
|
||||
createFactory,
|
||||
DOM: dom,
|
||||
PropTypes
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
|
||||
const { connect } = require("devtools/client/shared/vendor/react-redux");
|
||||
|
||||
const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/message-container").MessageContainer);
|
||||
|
||||
const ConsoleOutput = createClass({
|
||||
|
||||
propTypes: {
|
||||
jsterm: PropTypes.object.isRequired,
|
||||
// This function is created in mergeProps
|
||||
openVariablesView: PropTypes.func.isRequired,
|
||||
messages: PropTypes.array.isRequired
|
||||
},
|
||||
|
||||
displayName: "ConsoleOutput",
|
||||
|
||||
componentWillUpdate() {
|
||||
// @TODO Move this to a parent component.
|
||||
let node = ReactDOM.findDOMNode(this).parentNode.parentNode.parentNode;
|
||||
if (node.lastChild) {
|
||||
this.shouldScrollBottom = isScrolledToBottom(node.lastChild, node);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.shouldScrollBottom) {
|
||||
let node = ReactDOM.findDOMNode(this).parentNode.parentNode.parentNode;
|
||||
node.scrollTop = node.scrollHeight;
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
let messageNodes = this.props.messages.map(function(message) {
|
||||
return (
|
||||
MessageContainer({ message })
|
||||
);
|
||||
});
|
||||
return (
|
||||
dom.div({}, messageNodes)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function isScrolledToBottom(outputNode, scrollNode) {
|
||||
let lastNodeHeight = outputNode.lastChild ?
|
||||
outputNode.lastChild.clientHeight : 0;
|
||||
return scrollNode.scrollTop + scrollNode.clientHeight >=
|
||||
scrollNode.scrollHeight - lastNodeHeight / 2;
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
messages: state.messages
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(ConsoleOutput);
|
@ -0,0 +1,50 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// React & Redux
|
||||
const {
|
||||
createClass,
|
||||
createFactory,
|
||||
PropTypes
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
|
||||
const MessageContainer = createClass({
|
||||
|
||||
propTypes: {
|
||||
message: PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
displayName: "MessageContainer",
|
||||
|
||||
render() {
|
||||
const { message } = this.props;
|
||||
let MessageComponent = getMessageComponent(message.messageType);
|
||||
return MessageComponent({ message });
|
||||
}
|
||||
});
|
||||
|
||||
function getMessageComponent(messageType) {
|
||||
let MessageComponent;
|
||||
switch (messageType) {
|
||||
case "ConsoleApiCall":
|
||||
MessageComponent = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call").ConsoleApiCall;
|
||||
break;
|
||||
case "EvaluationResult":
|
||||
MessageComponent = require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result").EvaluationResult;
|
||||
break;
|
||||
case "PageError":
|
||||
MessageComponent = require("devtools/client/webconsole/new-console-output/components/message-types/page-error").PageError;
|
||||
break;
|
||||
}
|
||||
return createFactory(MessageComponent);
|
||||
}
|
||||
|
||||
module.exports.MessageContainer = MessageContainer;
|
||||
|
||||
// Exported so we can test it with unit tests.
|
||||
module.exports.getMessageComponent = getMessageComponent;
|
@ -0,0 +1,32 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// React & Redux
|
||||
const {
|
||||
DOM: dom,
|
||||
PropTypes
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
const {l10n} = require("devtools/client/webconsole/new-console-output/utils/messages");
|
||||
|
||||
MessageIcon.displayName = "MessageIcon";
|
||||
|
||||
MessageIcon.propTypes = {
|
||||
severity: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function MessageIcon(props) {
|
||||
const { severity } = props;
|
||||
|
||||
const title = l10n.getStr("severity." + severity);
|
||||
return dom.div({
|
||||
className: "icon",
|
||||
title
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.MessageIcon = MessageIcon;
|
@ -0,0 +1,28 @@
|
||||
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// React & Redux
|
||||
const {
|
||||
DOM: dom,
|
||||
PropTypes
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
|
||||
MessageRepeat.displayName = "MessageRepeat";
|
||||
|
||||
MessageRepeat.propTypes = {
|
||||
repeat: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
function MessageRepeat(props) {
|
||||
const { repeat } = props;
|
||||
const visibility = repeat > 1 ? "visible" : "hidden";
|
||||
return dom.span({className: "message-repeats", style: {visibility}}, repeat);
|
||||
}
|
||||
|
||||
exports.MessageRepeat = MessageRepeat;
|
@ -0,0 +1,66 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// React & Redux
|
||||
const {
|
||||
createFactory,
|
||||
DOM: dom,
|
||||
PropTypes
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat").MessageRepeat);
|
||||
const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
|
||||
|
||||
ConsoleApiCall.displayName = "ConsoleApiCall";
|
||||
|
||||
ConsoleApiCall.propTypes = {
|
||||
message: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
function ConsoleApiCall(props) {
|
||||
const { message } = props;
|
||||
const messageBody =
|
||||
dom.span({className: "message-body devtools-monospace"},
|
||||
formatTextContent(message.data.arguments));
|
||||
const icon = MessageIcon({severity: message.severity});
|
||||
const repeat = MessageRepeat({repeat: message.repeat});
|
||||
const children = [
|
||||
messageBody,
|
||||
repeat
|
||||
];
|
||||
|
||||
// @TODO Use of "is" is a temporary hack to get the category and severity
|
||||
// attributes to be applied. There are targeted in webconsole's CSS rules,
|
||||
// so if we remove this hack, we have to modify the CSS rules accordingly.
|
||||
return dom.div({
|
||||
class: "message cm-s-mozilla",
|
||||
is: "fdt-message",
|
||||
category: message.category,
|
||||
severity: message.severity
|
||||
},
|
||||
icon,
|
||||
dom.span({className: "message-body-wrapper"},
|
||||
dom.span({},
|
||||
dom.span({className: "message-flex-body"},
|
||||
children
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function formatTextContent(args) {
|
||||
return args.map(function(arg, i, arr) {
|
||||
const str = dom.span({className: "console-string"}, arg);
|
||||
if (i < arr.length - 1) {
|
||||
return [str, " "];
|
||||
}
|
||||
return str;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.ConsoleApiCall = ConsoleApiCall;
|
@ -0,0 +1,60 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// React & Redux
|
||||
const {
|
||||
createFactory,
|
||||
DOM: dom,
|
||||
PropTypes
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
|
||||
const VariablesViewLink = createFactory(require("devtools/client/webconsole/new-console-output/components/variables-view-link").VariablesViewLink);
|
||||
const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
|
||||
|
||||
DatePreview.displayName = "DatePreview";
|
||||
|
||||
DatePreview.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
function DatePreview(props) {
|
||||
const { data, category, severity } = props;
|
||||
const { preview } = data;
|
||||
|
||||
const dateString = new Date(preview.timestamp).toISOString();
|
||||
const textNodes = [
|
||||
VariablesViewLink({
|
||||
objectActor: data,
|
||||
label: "Date"
|
||||
}),
|
||||
dom.span({ className: "cm-string-2" }, ` ${dateString}`)
|
||||
];
|
||||
const icon = MessageIcon({ severity });
|
||||
|
||||
// @TODO Use of "is" is a temporary hack to get the category and severity
|
||||
// attributes to be applied. There are targeted in webconsole's CSS rules,
|
||||
// so if we remove this hack, we have to modify the CSS rules accordingly.
|
||||
return dom.div({
|
||||
class: "message cm-s-mozilla",
|
||||
is: "fdt-message",
|
||||
category: category,
|
||||
severity: severity
|
||||
},
|
||||
icon,
|
||||
dom.span({
|
||||
className: "message-body-wrapper message-body devtools-monospace"
|
||||
}, dom.span({},
|
||||
dom.span({ className: "class-Date" },
|
||||
textNodes
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports.DatePreview = DatePreview;
|
@ -0,0 +1,37 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// React & Redux
|
||||
const {
|
||||
createFactory,
|
||||
DOM: dom,
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
|
||||
|
||||
DefaultRenderer.displayName = "DefaultRenderer";
|
||||
|
||||
function DefaultRenderer(props) {
|
||||
const { category, severity } = props;
|
||||
|
||||
const icon = MessageIcon({ severity });
|
||||
|
||||
// @TODO Use of "is" is a temporary hack to get the category and severity
|
||||
// attributes to be applied. There are targeted in webconsole's CSS rules,
|
||||
// so if we remove this hack, we have to modify the CSS rules accordingly.
|
||||
return dom.div({
|
||||
class: "message cm-s-mozilla",
|
||||
is: "fdt-message",
|
||||
category: category,
|
||||
severity: severity
|
||||
},
|
||||
icon,
|
||||
"This evaluation result type is not supported yet."
|
||||
);
|
||||
}
|
||||
|
||||
module.exports.DefaultRenderer = DefaultRenderer;
|
@ -0,0 +1,42 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// React & Redux
|
||||
const {
|
||||
createFactory,
|
||||
PropTypes
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
|
||||
EvaluationResult.displayName = "EvaluationResult";
|
||||
|
||||
EvaluationResult.propTypes = {
|
||||
message: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
function EvaluationResult(props) {
|
||||
const { message } = props;
|
||||
let PreviewComponent = getPreviewComponent(message.data);
|
||||
|
||||
return PreviewComponent({
|
||||
data: message.data,
|
||||
category: message.category,
|
||||
severity: message.severity
|
||||
});
|
||||
}
|
||||
|
||||
function getPreviewComponent(data) {
|
||||
if (typeof data.class != "undefined") {
|
||||
switch (data.class) {
|
||||
case "Date":
|
||||
return createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/date-preview").DatePreview);
|
||||
}
|
||||
}
|
||||
return createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/default-renderer").DefaultRenderer);
|
||||
}
|
||||
|
||||
module.exports.EvaluationResult = EvaluationResult;
|
@ -0,0 +1,12 @@
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
DevToolsModules(
|
||||
'console-api-call.js',
|
||||
'date-preview.js',
|
||||
'default-renderer.js',
|
||||
'evaluation-result.js',
|
||||
'page-error.js',
|
||||
)
|
@ -0,0 +1,56 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// React & Redux
|
||||
const {
|
||||
createFactory,
|
||||
DOM: dom,
|
||||
PropTypes
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat").MessageRepeat);
|
||||
const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
|
||||
|
||||
PageError.displayName = "PageError";
|
||||
|
||||
PageError.propTypes = {
|
||||
message: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
function PageError(props) {
|
||||
const { message } = props;
|
||||
const messageBody =
|
||||
dom.span({className: "message-body devtools-monospace"},
|
||||
message.data.errorMessage);
|
||||
const repeat = MessageRepeat({repeat: message.repeat});
|
||||
const icon = MessageIcon({severity: message.severity});
|
||||
const children = [
|
||||
messageBody,
|
||||
repeat
|
||||
];
|
||||
|
||||
// @TODO Use of "is" is a temporary hack to get the category and severity
|
||||
// attributes to be applied. There are targeted in webconsole's CSS rules,
|
||||
// so if we remove this hack, we have to modify the CSS rules accordingly.
|
||||
return dom.div({
|
||||
class: "message cm-s-mozilla",
|
||||
is: "fdt-message",
|
||||
category: message.category,
|
||||
severity: message.severity
|
||||
},
|
||||
icon,
|
||||
dom.span({className: "message-body-wrapper"},
|
||||
dom.span({},
|
||||
dom.span({className: "message-flex-body"},
|
||||
children
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports.PageError = PageError;
|
@ -0,0 +1,16 @@
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
DIRS += [
|
||||
'message-types'
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'console-output.js',
|
||||
'message-container.js',
|
||||
'message-icon.js',
|
||||
'message-repeat.js',
|
||||
'variables-view-link.js'
|
||||
)
|
@ -0,0 +1,34 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// React & Redux
|
||||
const {
|
||||
DOM: dom,
|
||||
PropTypes
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
const {openVariablesView} = require("devtools/client/webconsole/new-console-output/utils/variables-view");
|
||||
|
||||
VariablesViewLink.displayName = "VariablesViewLink";
|
||||
|
||||
VariablesViewLink.propTypes = {
|
||||
objectActor: PropTypes.object.required,
|
||||
label: PropTypes.string.label,
|
||||
};
|
||||
|
||||
function VariablesViewLink(props) {
|
||||
const { objectActor, label } = props;
|
||||
|
||||
return dom.a({
|
||||
onClick: openVariablesView.bind(null, objectActor),
|
||||
className: "cm-variable",
|
||||
draggable: false,
|
||||
href: "#"
|
||||
}, label);
|
||||
}
|
||||
|
||||
module.exports.VariablesViewLink = VariablesViewLink;
|
77
devtools/client/webconsole/new-console-output/constants.js
Normal file
@ -0,0 +1,77 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const actionTypes = {
|
||||
MESSAGE_ADD: "MESSAGE_ADD",
|
||||
MESSAGES_CLEAR: "MESSAGES_CLEAR",
|
||||
};
|
||||
|
||||
const categories = {
|
||||
CATEGORY_NETWORK: 0,
|
||||
CATEGORY_CSS: 1,
|
||||
CATEGORY_JS: 2,
|
||||
CATEGORY_WEBDEV: 3,
|
||||
CATEGORY_INPUT: 4,
|
||||
CATEGORY_OUTPUT: 5,
|
||||
CATEGORY_SECURITY: 6,
|
||||
CATEGORY_SERVER: 7
|
||||
};
|
||||
|
||||
const severities = {
|
||||
SEVERITY_ERROR: 0,
|
||||
SEVERITY_WARNING: 1,
|
||||
SEVERITY_INFO: 2,
|
||||
SEVERITY_LOG: 3
|
||||
};
|
||||
|
||||
// The fragment of a CSS class name that identifies categories/severities.
|
||||
const fragments = {
|
||||
CATEGORY_CLASS_FRAGMENTS: [
|
||||
"network",
|
||||
"cssparser",
|
||||
"exception",
|
||||
"console",
|
||||
"input",
|
||||
"output",
|
||||
"security",
|
||||
"server",
|
||||
],
|
||||
SEVERITY_CLASS_FRAGMENTS: [
|
||||
"error",
|
||||
"warn",
|
||||
"info",
|
||||
"log",
|
||||
]
|
||||
};
|
||||
|
||||
// A mapping from the console API log event levels to the Web Console
|
||||
// severities.
|
||||
const levels = {
|
||||
LEVELS: {
|
||||
error: severities.SEVERITY_ERROR,
|
||||
exception: severities.SEVERITY_ERROR,
|
||||
assert: severities.SEVERITY_ERROR,
|
||||
warn: severities.SEVERITY_WARNING,
|
||||
info: severities.SEVERITY_INFO,
|
||||
log: severities.SEVERITY_LOG,
|
||||
trace: severities.SEVERITY_LOG,
|
||||
table: severities.SEVERITY_LOG,
|
||||
debug: severities.SEVERITY_LOG,
|
||||
dir: severities.SEVERITY_LOG,
|
||||
dirxml: severities.SEVERITY_LOG,
|
||||
group: severities.SEVERITY_LOG,
|
||||
groupCollapsed: severities.SEVERITY_LOG,
|
||||
groupEnd: severities.SEVERITY_LOG,
|
||||
time: severities.SEVERITY_LOG,
|
||||
timeEnd: severities.SEVERITY_LOG,
|
||||
count: severities.SEVERITY_LOG
|
||||
}
|
||||
};
|
||||
|
||||
// Combine into a single constants object
|
||||
module.exports = Object.assign({}, actionTypes, categories, severities,
|
||||
fragments, levels);
|
24
devtools/client/webconsole/new-console-output/main.js
Normal file
@ -0,0 +1,24 @@
|
||||
/* 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/. */
|
||||
|
||||
/* global BrowserLoader */
|
||||
|
||||
"use strict";
|
||||
|
||||
var { utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://devtools/client/shared/browser-loader.js");
|
||||
|
||||
// Initialize module loader and load all modules of the new inline
|
||||
// preview feature. The entire code-base doesn't need any extra
|
||||
// privileges and runs entirely in content scope.
|
||||
const NewConsoleOutputWrapper = BrowserLoader({
|
||||
baseURI: "resource://devtools/client/webconsole/new-console-output/",
|
||||
window: this}).require("./new-console-output-wrapper");
|
||||
|
||||
this.NewConsoleOutput = function(parentNode, jsterm) {
|
||||
console.log("Creating NewConsoleOutput", parentNode, NewConsoleOutputWrapper);
|
||||
return new NewConsoleOutputWrapper(parentNode, jsterm);
|
||||
};
|
28
devtools/client/webconsole/new-console-output/moz.build
Normal file
@ -0,0 +1,28 @@
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
DIRS += [
|
||||
'actions',
|
||||
'components',
|
||||
'reducers',
|
||||
'utils',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'constants.js',
|
||||
'main.js',
|
||||
'new-console-output-wrapper.js',
|
||||
'store.js',
|
||||
)
|
||||
|
||||
MOCHITEST_CHROME_MANIFESTS += [
|
||||
'test/components/chrome.ini',
|
||||
'test/utils/chrome.ini'
|
||||
]
|
||||
XPCSHELL_TESTS_MANIFESTS += [
|
||||
'test/actions/xpcshell.ini',
|
||||
'test/store/xpcshell.ini'
|
||||
]
|
||||
|
@ -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/. */
|
||||
"use strict";
|
||||
|
||||
// React & Redux
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
|
||||
const { Provider } = require("devtools/client/shared/vendor/react-redux");
|
||||
|
||||
const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
|
||||
const { store } = require("devtools/client/webconsole/new-console-output/store");
|
||||
|
||||
const ConsoleOutput = React.createFactory(require("devtools/client/webconsole/new-console-output/components/console-output"));
|
||||
|
||||
function NewConsoleOutputWrapper(parentNode, jsterm) {
|
||||
let childComponent = ConsoleOutput({ jsterm });
|
||||
let provider = React.createElement(
|
||||
Provider, { store: store }, childComponent);
|
||||
this.body = ReactDOM.render(provider, parentNode);
|
||||
}
|
||||
|
||||
NewConsoleOutputWrapper.prototype = {
|
||||
dispatchMessageAdd: (message) => {
|
||||
store.dispatch(actions.messageAdd(message));
|
||||
},
|
||||
dispatchMessagesClear: () => {
|
||||
store.dispatch(actions.messagesClear());
|
||||
}
|
||||
};
|
||||
|
||||
// Exports from this module
|
||||
module.exports = NewConsoleOutputWrapper;
|
@ -0,0 +1,12 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const { messages } = require("./messages");
|
||||
|
||||
exports.reducers = {
|
||||
messages
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const constants = require("devtools/client/webconsole/new-console-output/constants");
|
||||
|
||||
function messages(state = [], action) {
|
||||
switch (action.type) {
|
||||
case constants.MESSAGE_ADD:
|
||||
let newMessage = action.message;
|
||||
if (newMessage.allowRepeating && state.length > 0) {
|
||||
let lastMessage = state[state.length - 1];
|
||||
if (lastMessage.repeatId === newMessage.repeatId) {
|
||||
newMessage.repeat = lastMessage.repeat + 1;
|
||||
return state.slice(0, state.length - 1).concat(newMessage);
|
||||
}
|
||||
}
|
||||
return state.concat([ newMessage ]);
|
||||
case constants.MESSAGES_CLEAR:
|
||||
return [];
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
exports.messages = messages;
|
@ -0,0 +1,9 @@
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
DevToolsModules(
|
||||
'index.js',
|
||||
'messages.js',
|
||||
)
|
18
devtools/client/webconsole/new-console-output/store.js
Normal file
@ -0,0 +1,18 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const { combineReducers, createStore } = require("devtools/client/shared/vendor/redux");
|
||||
const { reducers } = require("./reducers/index");
|
||||
|
||||
function storeFactory(initialState = {}) {
|
||||
return createStore(combineReducers(reducers), initialState);
|
||||
}
|
||||
|
||||
// Provide the single store instance for app code.
|
||||
module.exports.store = storeFactory();
|
||||
// Provide the store factory for test code so that each test is working with
|
||||
// its own instance.
|
||||
module.exports.storeFactory = storeFactory;
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["../../../../.eslintrc.xpcshell"]
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
var { utils: Cu } = Components;
|
||||
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
||||
|
||||
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
DevToolsUtils.testing = true;
|
||||
DevToolsUtils.dumpn.wantLogging = true;
|
||||
DevToolsUtils.dumpv.wantVerbose = false;
|
||||
|
||||
// @TODO consolidate once we have a shared head. See #16
|
||||
const testPackets = new Map();
|
||||
testPackets.set("console.log", {
|
||||
"from": "server1.conn4.child1/consoleActor2",
|
||||
"type": "consoleAPICall",
|
||||
"message": {
|
||||
"arguments": [
|
||||
"foobar",
|
||||
"test"
|
||||
],
|
||||
"columnNumber": 1,
|
||||
"counter": null,
|
||||
"filename": "file:///test.html",
|
||||
"functionName": "",
|
||||
"groupName": "",
|
||||
"level": "log",
|
||||
"lineNumber": 1,
|
||||
"private": false,
|
||||
"styles": [],
|
||||
"timeStamp": 1455064271115,
|
||||
"timer": null,
|
||||
"workerType": "none",
|
||||
"category": "webdev"
|
||||
}
|
||||
});
|
@ -0,0 +1,37 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
messageAdd,
|
||||
messagesClear
|
||||
} = require("devtools/client/webconsole/new-console-output/actions/messages");
|
||||
const {
|
||||
prepareMessage
|
||||
} = require("devtools/client/webconsole/new-console-output/utils/messages");
|
||||
const constants = require("devtools/client/webconsole/new-console-output/constants");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function*() {
|
||||
const packet = testPackets.get("console.log");
|
||||
const action = messageAdd(packet);
|
||||
const expected = {
|
||||
type: constants.MESSAGE_ADD,
|
||||
// Prepare message is tested independently.
|
||||
message: prepareMessage(packet)
|
||||
};
|
||||
deepEqual(action, expected,
|
||||
"messageAdd action creator returns expected action object");
|
||||
});
|
||||
|
||||
add_task(function*() {
|
||||
const action = messagesClear();
|
||||
const expected = {
|
||||
type: constants.MESSAGES_CLEAR,
|
||||
};
|
||||
deepEqual(action, expected,
|
||||
"messagesClear action creator returns expected action object");
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
[DEFAULT]
|
||||
tags = devtools devtools-webconsole
|
||||
head = head.js
|
||||
tail =
|
||||
firefox-appdir = browser
|
||||
|
||||
[test_messages.js]
|
@ -0,0 +1,13 @@
|
||||
[DEFAULT]
|
||||
|
||||
support-files =
|
||||
head.js
|
||||
|
||||
[test_console-api-call.html]
|
||||
[test_console-api-call_repeat.html]
|
||||
[test_date-preview.html]
|
||||
[test_evaluation-result.html]
|
||||
[test_message-icon.html]
|
||||
[test_message-container.html]
|
||||
[test_message-repeat.html]
|
||||
[test_page-error.html]
|
@ -0,0 +1,263 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/* exported getPacket, renderComponent, shallowRenderComponent,
|
||||
cleanActualHTML, cleanExpectedHTML */
|
||||
|
||||
"use strict";
|
||||
|
||||
var { utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://testing-common/Assert.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
||||
var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
|
||||
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
var {DebuggerServer} = require("devtools/server/main");
|
||||
var {DebuggerClient} = require("devtools/shared/client/main");
|
||||
|
||||
const Services = require("Services");
|
||||
|
||||
DevToolsUtils.testing = true;
|
||||
var { require: browserRequire } = BrowserLoader({
|
||||
baseURI: "resource://devtools/client/webconsole/",
|
||||
window: this
|
||||
});
|
||||
|
||||
let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
|
||||
let React = browserRequire("devtools/client/shared/vendor/react");
|
||||
var TestUtils = React.addons.TestUtils;
|
||||
|
||||
let testCommands = new Map();
|
||||
testCommands.set("console.log()", {
|
||||
command: "console.log('foobar', 'test')",
|
||||
commandType: "consoleAPICall",
|
||||
expectedText: "foobar test"
|
||||
});
|
||||
testCommands.set("new Date()", {
|
||||
command: "new Date(448156800000)",
|
||||
commandType: "evaluationResult",
|
||||
expectedText: "Date 1984-03-15T00:00:00.000Z"
|
||||
});
|
||||
testCommands.set("pageError", {
|
||||
command: null,
|
||||
commandType: "pageError",
|
||||
expectedText: "ReferenceError: asdf is not defined"
|
||||
});
|
||||
|
||||
function* getPacket(command, type = "evaluationResult") {
|
||||
try {
|
||||
// Attach the console to the tab.
|
||||
let state = yield new Promise(function(resolve) {
|
||||
attachConsoleToTab(["ConsoleAPI"], resolve);
|
||||
});
|
||||
|
||||
// Run the command and get the packet.
|
||||
let packet;
|
||||
switch (type) {
|
||||
case "consoleAPICall":
|
||||
packet = yield new Promise((resolve) => {
|
||||
function onConsoleApiCall(apiCallType, apiCallPacket) {
|
||||
state.dbgClient.removeListener("consoleAPICall", onConsoleApiCall);
|
||||
resolve(apiCallPacket);
|
||||
}
|
||||
state.dbgClient.addListener("consoleAPICall", onConsoleApiCall);
|
||||
state.client.evaluateJS(`top.${command}`);
|
||||
});
|
||||
break;
|
||||
case "evaluationResult":
|
||||
packet = yield new Promise(resolve => {
|
||||
state.client.evaluateJS(command, resolve);
|
||||
});
|
||||
break;
|
||||
case "pageError":
|
||||
// @TODO: get packet with RDP
|
||||
packet = {
|
||||
"from": "server1.conn1.child1/consoleActor2",
|
||||
"type": "pageError",
|
||||
"pageError": {
|
||||
"errorMessage": "ReferenceError: asdf is not defined",
|
||||
"sourceName": "data:text/html,<script>asdf</script>",
|
||||
"lineText": "",
|
||||
"lineNumber": 1,
|
||||
"columnNumber": 1,
|
||||
"category": "content javascript",
|
||||
"timeStamp": 1455735574091,
|
||||
"warning": false,
|
||||
"error": false,
|
||||
"exception": true,
|
||||
"strict": false,
|
||||
"info": false,
|
||||
"private": false,
|
||||
"stacktrace": [{
|
||||
"columnNumber": 68,
|
||||
"filename": "test.html",
|
||||
"functionName": "baz",
|
||||
"language": 2,
|
||||
"lineNumber": 1
|
||||
}, {
|
||||
"columnNumber": 43,
|
||||
"filename": "test.html",
|
||||
"functionName": "bar",
|
||||
"language": 2,
|
||||
"lineNumber": 2
|
||||
}, {
|
||||
"columnNumber": 18,
|
||||
"filename": "test.html",
|
||||
"functionName": "foo",
|
||||
"language": 2,
|
||||
"lineNumber": 3
|
||||
}, {
|
||||
"columnNumber": 150,
|
||||
"filename": "test.html",
|
||||
"functionName": "",
|
||||
"language": 2,
|
||||
"lineNumber": 4
|
||||
}]
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
closeDebugger(state);
|
||||
return packet;
|
||||
} catch (e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
}
|
||||
}
|
||||
|
||||
function renderComponent(component, props) {
|
||||
const el = React.createElement(component, props, {});
|
||||
// By default, renderIntoDocument() won't work for stateless components, but
|
||||
// it will work if the stateless component is wrapped in a stateful one.
|
||||
// See https://github.com/facebook/react/issues/4839
|
||||
const wrappedEl = React.DOM.span({}, [el]);
|
||||
const renderedComponent = TestUtils.renderIntoDocument(wrappedEl);
|
||||
return ReactDOM.findDOMNode(renderedComponent).children[0];
|
||||
}
|
||||
|
||||
function shallowRenderComponent(component, props) {
|
||||
const el = React.createElement(component, props);
|
||||
const renderer = TestUtils.createRenderer();
|
||||
renderer.render(el, {});
|
||||
return renderer.getRenderOutput();
|
||||
}
|
||||
|
||||
function cleanActualHTML(htmlString) {
|
||||
return htmlString.replace(/ data-reactid=\".*?\"/g, "");
|
||||
}
|
||||
|
||||
function cleanExpectedHTML(htmlString) {
|
||||
return htmlString.replace(/(?:\r\n|\r|\n)\s*/g, "");
|
||||
}
|
||||
|
||||
// Helpers copied in from shared/webconsole/test/common.js
|
||||
function initCommon()
|
||||
{
|
||||
//Services.prefs.setBoolPref("devtools.debugger.log", true);
|
||||
}
|
||||
|
||||
function initDebuggerServer()
|
||||
{
|
||||
if (!DebuggerServer.initialized) {
|
||||
DebuggerServer.init();
|
||||
DebuggerServer.addBrowserActors();
|
||||
}
|
||||
DebuggerServer.allowChromeProcess = true;
|
||||
}
|
||||
|
||||
function connectToDebugger(aCallback)
|
||||
{
|
||||
initCommon();
|
||||
initDebuggerServer();
|
||||
|
||||
let transport = DebuggerServer.connectPipe();
|
||||
let client = new DebuggerClient(transport);
|
||||
|
||||
let dbgState = { dbgClient: client };
|
||||
client.connect().then(response => aCallback(dbgState, response));
|
||||
}
|
||||
|
||||
function closeDebugger(aState, aCallback)
|
||||
{
|
||||
aState.dbgClient.close(aCallback);
|
||||
aState.dbgClient = null;
|
||||
aState.client = null;
|
||||
}
|
||||
|
||||
function attachConsole(aListeners, aCallback) {
|
||||
_attachConsole(aListeners, aCallback);
|
||||
}
|
||||
function attachConsoleToTab(aListeners, aCallback) {
|
||||
_attachConsole(aListeners, aCallback, true);
|
||||
}
|
||||
function attachConsoleToWorker(aListeners, aCallback) {
|
||||
_attachConsole(aListeners, aCallback, true, true);
|
||||
}
|
||||
|
||||
function _attachConsole(aListeners, aCallback, aAttachToTab, aAttachToWorker)
|
||||
{
|
||||
function _onAttachConsole(aState, aResponse, aWebConsoleClient)
|
||||
{
|
||||
if (aResponse.error) {
|
||||
Cu.reportError("attachConsole failed: " + aResponse.error + " " +
|
||||
aResponse.message);
|
||||
}
|
||||
|
||||
aState.client = aWebConsoleClient;
|
||||
|
||||
aCallback(aState, aResponse);
|
||||
}
|
||||
|
||||
connectToDebugger(function _onConnect(aState, aResponse) {
|
||||
if (aResponse.error) {
|
||||
Cu.reportError("client.connect() failed: " + aResponse.error + " " +
|
||||
aResponse.message);
|
||||
aCallback(aState, aResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
if (aAttachToTab) {
|
||||
aState.dbgClient.listTabs(function _onListTabs(aResponse) {
|
||||
if (aResponse.error) {
|
||||
Cu.reportError("listTabs failed: " + aResponse.error + " " +
|
||||
aResponse.message);
|
||||
aCallback(aState, aResponse);
|
||||
return;
|
||||
}
|
||||
let tab = aResponse.tabs[aResponse.selected];
|
||||
aState.dbgClient.attachTab(tab.actor, function (response, tabClient) {
|
||||
if (aAttachToWorker) {
|
||||
var worker = new Worker("console-test-worker.js");
|
||||
worker.addEventListener("message", function listener() {
|
||||
worker.removeEventListener("message", listener);
|
||||
tabClient.listWorkers(function (response) {
|
||||
tabClient.attachWorker(response.workers[0].actor, function (response, workerClient) {
|
||||
workerClient.attachThread({}, function(aResponse) {
|
||||
aState.actor = workerClient.consoleActor;
|
||||
aState.dbgClient.attachConsole(workerClient.consoleActor, aListeners,
|
||||
_onAttachConsole.bind(null, aState));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
aState.actor = tab.consoleActor;
|
||||
aState.dbgClient.attachConsole(tab.consoleActor, aListeners,
|
||||
_onAttachConsole.bind(null, aState));
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
aState.dbgClient.getProcess().then(response => {
|
||||
aState.dbgClient.attachTab(response.form.actor, function () {
|
||||
let consoleActor = response.form.consoleActor;
|
||||
aState.actor = consoleActor;
|
||||
aState.dbgClient.attachConsole(consoleActor, aListeners,
|
||||
_onAttachConsole.bind(null, aState));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Test for ConsoleApiCall component</title>
|
||||
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript;version=1.8" src="head.js"></script>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for ConsoleApiCall component</p>
|
||||
|
||||
<script type="text/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
|
||||
const { ConsoleApiCall } = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call");
|
||||
|
||||
const packet = yield getPacket("console.log('foobar', 'test')", "consoleAPICall");
|
||||
const message = prepareMessage(packet);
|
||||
const rendered = renderComponent(ConsoleApiCall, {message});
|
||||
|
||||
const queryPath = "div.message.cm-s-mozilla span span.message-flex-body span.message-body.devtools-monospace";
|
||||
const messageBody = rendered.querySelectorAll(queryPath);
|
||||
const consoleStringNodes = messageBody[0].querySelectorAll("span.console-string");
|
||||
is(consoleStringNodes.length, 2, "ConsoleApiCall outputs expected HTML structure");
|
||||
is(messageBody[0].textContent, "foobar test", "ConsoleApiCall outputs expected text");
|
||||
|
||||
SimpleTest.finish()
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,36 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Test for ConsoleApiCall component with repeats</title>
|
||||
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript;version=1.8" src="head.js"></script>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for ConsoleApiCall component with repeats</p>
|
||||
|
||||
<script type="text/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
|
||||
const { ConsoleApiCall } = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call");
|
||||
|
||||
const packet = yield getPacket("console.log('foobar', 'test')", "consoleAPICall");
|
||||
const message = prepareMessage(packet);
|
||||
message.repeat = 107;
|
||||
const rendered = renderComponent(ConsoleApiCall, {message});
|
||||
|
||||
const messageBodyPath = "span > span.message-flex-body > span.message-body.devtools-monospace";
|
||||
const messageBody = rendered.querySelectorAll(messageBodyPath);
|
||||
is(messageBody[0].textContent, "foobar test", "ConsoleApiCall outputs expected text for repeated message");
|
||||
|
||||
const repeatPath = "span > span.message-flex-body > span.message-body.devtools-monospace + span.message-repeats";
|
||||
const repeat = rendered.querySelectorAll(repeatPath);
|
||||
is(repeat[0].textContent, `${message.repeat}`, "ConsoleApiCall outputs correct repeat count");
|
||||
|
||||
SimpleTest.finish()
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Test for DatePreview component</title>
|
||||
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript;version=1.8" src="head.js"></script>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for DatePreview component</p>
|
||||
|
||||
<script type="text/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
|
||||
const { DatePreview } = require("devtools/client/webconsole/new-console-output/components/message-types/date-preview");
|
||||
|
||||
const testCommand = testCommands.get("new Date()");
|
||||
const packet = yield getPacket(testCommand.command, testCommand.commandType);
|
||||
const message = prepareMessage(packet);
|
||||
const props = {
|
||||
data: message.data,
|
||||
severity: message.severity,
|
||||
category: message.category,
|
||||
};
|
||||
const rendered = renderComponent(DatePreview, props);
|
||||
|
||||
const queryPathBase = "div.message.cm-s-mozilla span.message-body-wrapper.message-body.devtools-monospace span span.class-Date";
|
||||
|
||||
const preview = rendered.querySelectorAll(queryPathBase);
|
||||
is(preview[0].textContent, testCommand.expectedText, "DatePreview outputs expected text");
|
||||
|
||||
const link = rendered.querySelectorAll(`${queryPathBase} a[draggable=false][href="#"].cm-variable`);
|
||||
is(link.length, 1, "DatePreview outputs the variables view link");
|
||||
|
||||
SimpleTest.finish()
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,70 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Test for EvaluationResult component</title>
|
||||
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript;version=1.8" src="head.js"></script>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for EvaluationResult component</p>
|
||||
|
||||
<script type="text/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
|
||||
const {
|
||||
EvaluationResult,
|
||||
getPreviewComponent
|
||||
} = require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result");
|
||||
|
||||
yield testFullRender();
|
||||
yield testGetPreviewComponent();
|
||||
|
||||
SimpleTest.finish()
|
||||
|
||||
/**
|
||||
* Test that passing in a message correctly wires up all the children.
|
||||
*
|
||||
* The different combinations of children are tested in separate per-component
|
||||
* tests. This test just ensures that this component pipes data to its children.
|
||||
*/
|
||||
function testFullRender() {
|
||||
const testValue = testCommands.get("new Date()");
|
||||
const packet = yield getPacket(testValue.command, testValue.commandType);
|
||||
const message = prepareMessage(packet);
|
||||
const props = {
|
||||
message
|
||||
};
|
||||
const rendered = renderComponent(EvaluationResult, props);
|
||||
|
||||
ok(rendered.textContent.includes(testValue.expectedText),
|
||||
"EvaluationResult pipes data to its children as expected");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that getPreviewComponent() returns correct component for each object type.
|
||||
*/
|
||||
function testGetPreviewComponent() {
|
||||
const testValues = [
|
||||
{
|
||||
commandObj: testCommands.get("new Date()"),
|
||||
expectedComponent: require("devtools/client/webconsole/new-console-output/components/message-types/date-preview").DatePreview
|
||||
}
|
||||
];
|
||||
|
||||
for (let testValue of testValues) {
|
||||
const { commandObj, expectedComponent } = testValue;
|
||||
const packet = yield getPacket(commandObj.command, commandObj.commandType);
|
||||
const message = prepareMessage(packet);
|
||||
const rendered = shallowRenderComponent(EvaluationResult, {message});
|
||||
is(rendered.type, expectedComponent,
|
||||
`EvaluationResult nests ${expectedComponent} based on command: ${commandObj.command}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,78 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Test for MessageContainer component</title>
|
||||
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript;version=1.8" src="head.js"></script>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for MessageContainer component</p>
|
||||
|
||||
<script type="text/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
|
||||
|
||||
const { MessageContainer } = require("devtools/client/webconsole/new-console-output/components/message-container");
|
||||
const { ConsoleApiCall } = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call");
|
||||
const { EvaluationResult } = require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result");
|
||||
const { PageError } = require("devtools/client/webconsole/new-console-output/components/message-types/page-error");
|
||||
|
||||
yield testFullRender();
|
||||
yield testGetMessageComponent();
|
||||
|
||||
SimpleTest.finish();
|
||||
|
||||
/**
|
||||
* Test that passing in a message correctly wires up all the children.
|
||||
*
|
||||
* The different combinations of children are tested in separate per-component
|
||||
* tests. This test just ensures that this component pipes data to its children.
|
||||
*/
|
||||
function testFullRender() {
|
||||
const testValue = testCommands.get("console.log()");
|
||||
const packet = yield getPacket(testValue.command, testValue.commandType);
|
||||
const message = prepareMessage(packet);
|
||||
const props = {
|
||||
message
|
||||
};
|
||||
const rendered = renderComponent(MessageContainer, props);
|
||||
|
||||
ok(rendered.textContent.includes(testValue.expectedText),
|
||||
"MessageContainer pipes data to its children as expected");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that getMessageComponent() returns correct component for each message type.
|
||||
*/
|
||||
function testGetMessageComponent() {
|
||||
const testValues = [
|
||||
{
|
||||
commandObj: testCommands.get("console.log()"),
|
||||
expectedComponent: ConsoleApiCall
|
||||
},
|
||||
{
|
||||
commandObj: testCommands.get("new Date()"),
|
||||
expectedComponent: EvaluationResult
|
||||
},
|
||||
{
|
||||
commandObj: testCommands.get("pageError"),
|
||||
expectedComponent: PageError
|
||||
}
|
||||
];
|
||||
|
||||
for (let testValue of testValues) {
|
||||
const { commandObj, expectedComponent } = testValue;
|
||||
const packet = yield getPacket(commandObj.command, commandObj.commandType);
|
||||
const message = prepareMessage(packet);
|
||||
const rendered = shallowRenderComponent(MessageContainer, {message});
|
||||
is(rendered.type, expectedComponent,
|
||||
`MessageContainer nests ${expectedComponent} based on command: ${commandObj.command}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Test for MessageRepeat component</title>
|
||||
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript;version=1.8" src="head.js"></script>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for MessageIcon component</p>
|
||||
|
||||
<script type="text/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
const {
|
||||
SEVERITY_CLASS_FRAGMENTS,
|
||||
SEVERITY_ERROR,
|
||||
} = require("devtools/client/webconsole/new-console-output/constants");
|
||||
const { MessageIcon } = require("devtools/client/webconsole/new-console-output/components/message-icon");
|
||||
|
||||
let severity = SEVERITY_CLASS_FRAGMENTS[SEVERITY_ERROR];
|
||||
const iconRendered = renderComponent(MessageIcon, { severity });
|
||||
ok(iconRendered.classList.contains("icon"), "MessageIcon has expected class");
|
||||
is(iconRendered.getAttribute("title"), "Error",
|
||||
"MessageIcon shows correct title attribute");
|
||||
|
||||
SimpleTest.finish();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Test for MessageRepeat component</title>
|
||||
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript;version=1.8" src="head.js"></script>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for MessageRepeat component</p>
|
||||
|
||||
<script type="text/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
const { MessageRepeat } = require("devtools/client/webconsole/new-console-output/components/message-repeat");
|
||||
|
||||
const repeatRendered = renderComponent(MessageRepeat, { repeat: 99 });
|
||||
ok(repeatRendered.classList.contains("message-repeats"), "MessageRepeat has expected class");
|
||||
is(repeatRendered.style.visibility, "visible", "MessageRepeat with 2+ repeats is visible");
|
||||
is(repeatRendered.textContent, "99", "MessageRepeat shows correct number of repeats");
|
||||
|
||||
const noRepeatRendered = renderComponent(MessageRepeat, { repeat: 1 });
|
||||
is(noRepeatRendered.style.visibility, "hidden", "MessageRepeat with 1 repeat is hidden");
|
||||
is(noRepeatRendered.textContent, "1", "MessageRepeat with 1 repeat shows correct number of repeats")
|
||||
|
||||
SimpleTest.finish();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Test for PageError component</title>
|
||||
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript;version=1.8" src="head.js"></script>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for PageError component</p>
|
||||
|
||||
<script type="text/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
|
||||
const { PageError } = require("devtools/client/webconsole/new-console-output/components/message-types/page-error");
|
||||
|
||||
const packet = yield getPacket(null, "pageError");
|
||||
const message = prepareMessage(packet);
|
||||
const rendered = renderComponent(PageError, {message});
|
||||
|
||||
const queryPath = "div.message.cm-s-mozilla span span.message-flex-body span.message-body.devtools-monospace";
|
||||
const messageBody = rendered.querySelectorAll(queryPath);
|
||||
is(messageBody.length, 1, "PageError outputs expected HTML structure");
|
||||
is(messageBody[0].textContent, testCommands.get("pageError").expectedText, "PageError outputs expected text");
|
||||
|
||||
SimpleTest.finish()
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,41 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/* exported storeFactory */
|
||||
|
||||
"use strict";
|
||||
|
||||
var { utils: Cu } = Components;
|
||||
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
||||
|
||||
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
DevToolsUtils.testing = true;
|
||||
DevToolsUtils.dumpn.wantLogging = true;
|
||||
DevToolsUtils.dumpv.wantVerbose = false;
|
||||
|
||||
const { storeFactory } = require("devtools/client/webconsole/new-console-output/store");
|
||||
|
||||
const testPackets = new Map();
|
||||
testPackets.set("console.log", {
|
||||
"from": "server1.conn4.child1/consoleActor2",
|
||||
"type": "consoleAPICall",
|
||||
"message": {
|
||||
"arguments": [
|
||||
"foobar",
|
||||
"test"
|
||||
],
|
||||
"columnNumber": 1,
|
||||
"counter": null,
|
||||
"filename": "file:///test.html",
|
||||
"functionName": "",
|
||||
"groupName": "",
|
||||
"level": "log",
|
||||
"lineNumber": 1,
|
||||
"private": false,
|
||||
"styles": [],
|
||||
"timeStamp": 1455064271115,
|
||||
"timer": null,
|
||||
"workerType": "none",
|
||||
"category": "webdev"
|
||||
}
|
||||
});
|
@ -0,0 +1,59 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
|
||||
const packet = testPackets.get("console.log");
|
||||
const {
|
||||
getRepeatId,
|
||||
prepareMessage
|
||||
} = require("devtools/client/webconsole/new-console-output/utils/messages");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding a message to the store.
|
||||
*/
|
||||
add_task(function*() {
|
||||
const { getState, dispatch } = storeFactory();
|
||||
|
||||
dispatch(actions.messageAdd(packet));
|
||||
|
||||
const expectedMessage = prepareMessage(packet);
|
||||
|
||||
deepEqual(getState().messages, [expectedMessage],
|
||||
"MESSAGE_ADD action adds a message");
|
||||
});
|
||||
|
||||
/**
|
||||
* Test repeating messages in the store.
|
||||
*/
|
||||
add_task(function*() {
|
||||
const { getState, dispatch } = storeFactory();
|
||||
|
||||
dispatch(actions.messageAdd(packet));
|
||||
dispatch(actions.messageAdd(packet));
|
||||
dispatch(actions.messageAdd(packet));
|
||||
|
||||
const expectedMessage = prepareMessage(packet);
|
||||
expectedMessage.repeat = 3;
|
||||
|
||||
deepEqual(getState().messages, [expectedMessage],
|
||||
"Adding same message to the store twice results in repeated message");
|
||||
});
|
||||
|
||||
/**
|
||||
* Test getRepeatId().
|
||||
*/
|
||||
add_task(function*() {
|
||||
const message1 = prepareMessage(packet);
|
||||
const message2 = prepareMessage(packet);
|
||||
equal(getRepeatId(message1), getRepeatId(message2),
|
||||
"getRepeatId() returns same repeat id for objects with the same values");
|
||||
|
||||
message2.data.arguments = ["new args"];
|
||||
notEqual(getRepeatId(message1), getRepeatId(message2),
|
||||
"getRepeatId() returns different repeat ids for different values");
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
[DEFAULT]
|
||||
tags = devtools devtools-webconsole
|
||||
head = head.js
|
||||
tail =
|
||||
firefox-appdir = browser
|
||||
|
||||
[test_messages.js]
|
@ -0,0 +1,6 @@
|
||||
[DEFAULT]
|
||||
|
||||
support-files =
|
||||
../components/head.js
|
||||
|
||||
[test_getRepeatId.html]
|
@ -0,0 +1,108 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Test for getRepeatId()</title>
|
||||
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript;version=1.8" src="head.js"></script>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for getRepeatId()</p>
|
||||
|
||||
<script type="text/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
const {
|
||||
prepareMessage,
|
||||
getRepeatId
|
||||
} = require("devtools/client/webconsole/new-console-output/utils/messages");
|
||||
|
||||
yield testDuplicateValues();
|
||||
yield testDifferentValues();
|
||||
yield testDifferentSeverities();
|
||||
yield testFalsyValues();
|
||||
yield testConsoleVsJSTerm();
|
||||
|
||||
SimpleTest.finish();
|
||||
|
||||
function testDuplicateValues() {
|
||||
const packet1 = yield getPacket("console.log('same')", "consoleAPICall");
|
||||
const packet2 = yield getPacket("console.log('same')", "consoleAPICall");
|
||||
|
||||
const message1 = prepareMessage(packet1);
|
||||
const message2 = prepareMessage(packet2);
|
||||
|
||||
is(getRepeatId(message1.data), getRepeatId(message2.data),
|
||||
"getRepeatId() returns same repeat id for objects with the same values");
|
||||
}
|
||||
|
||||
function testDifferentValues() {
|
||||
const packet1 = yield getPacket("console.log('same')", "consoleAPICall");
|
||||
const packet2 = yield getPacket("console.log('diff')", "consoleAPICall");
|
||||
|
||||
const message1 = prepareMessage(packet1);
|
||||
const message2 = prepareMessage(packet2);
|
||||
|
||||
isnot(getRepeatId(message1.data), getRepeatId(message2.data),
|
||||
"getRepeatId() returns different repeat ids for different values");
|
||||
}
|
||||
|
||||
function testDifferentSeverities() {
|
||||
const packet1 = yield getPacket("console.log('test')", "consoleAPICall");
|
||||
const packet2 = yield getPacket("console.warn('test')", "consoleAPICall");
|
||||
|
||||
const message1 = prepareMessage(packet1);
|
||||
const message2 = prepareMessage(packet2);
|
||||
|
||||
isnot(getRepeatId(message1.data), getRepeatId(message2.data),
|
||||
"getRepeatId() returns different repeat ids for different severities");
|
||||
}
|
||||
|
||||
function testFalsyValues() {
|
||||
const packetNaN = yield getPacket("console.log(NaN)", "consoleAPICall");
|
||||
const packetUnd = yield getPacket("console.log(undefined)", "consoleAPICall");
|
||||
const packetNul = yield getPacket("console.log(null)", "consoleAPICall");
|
||||
|
||||
const messageNaN = prepareMessage(packetNaN);
|
||||
const messageUnd = prepareMessage(packetUnd);
|
||||
const messageNul = prepareMessage(packetNul);
|
||||
|
||||
const repeatIds = new Set([
|
||||
getRepeatId(messageNaN.data),
|
||||
getRepeatId(messageUnd.data),
|
||||
getRepeatId(messageNul.data)]
|
||||
);
|
||||
is(repeatIds.size, 3,
|
||||
"getRepeatId() handles falsy values distinctly");
|
||||
|
||||
const packetNaN2 = yield getPacket("console.log(NaN)", "consoleAPICall");
|
||||
const packetUnd2 = yield getPacket("console.log(undefined)", "consoleAPICall");
|
||||
const packetNul2 = yield getPacket("console.log(null)", "consoleAPICall");
|
||||
|
||||
const messageNaN2 = prepareMessage(packetNaN2);
|
||||
const messageUnd2 = prepareMessage(packetUnd2);
|
||||
const messageNul2 = prepareMessage(packetNul2);
|
||||
|
||||
is(getRepeatId(messageNaN.data), getRepeatId(messageNaN2.data),
|
||||
"getRepeatId() handles NaN values");
|
||||
is(getRepeatId(messageUnd.data), getRepeatId(messageUnd2.data),
|
||||
"getRepeatId() handles undefined values");
|
||||
is(getRepeatId(messageNul.data), getRepeatId(messageNul2.data),
|
||||
"getRepeatId() handles null values");
|
||||
}
|
||||
|
||||
function testConsoleVsJSTerm() {
|
||||
const packet1 = yield getPacket("console.log(undefined)", "consoleAPICall");
|
||||
const packet2 = yield getPacket("undefined");
|
||||
|
||||
const message1 = prepareMessage(packet1);
|
||||
const message2 = prepareMessage(packet2);
|
||||
|
||||
isnot(getRepeatId(message1.data), getRepeatId(message2.data),
|
||||
"getRepeatId() returns different repeat ids for console vs JSTerm");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,93 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
CATEGORY_CLASS_FRAGMENTS,
|
||||
CATEGORY_JS,
|
||||
CATEGORY_WEBDEV,
|
||||
CATEGORY_OUTPUT,
|
||||
LEVELS,
|
||||
SEVERITY_CLASS_FRAGMENTS,
|
||||
SEVERITY_ERROR,
|
||||
SEVERITY_WARNING,
|
||||
SEVERITY_LOG,
|
||||
} = require("../constants");
|
||||
const WebConsoleUtils = require("devtools/shared/webconsole/utils").Utils;
|
||||
const STRINGS_URI = "chrome://devtools/locale/webconsole.properties";
|
||||
const l10n = new WebConsoleUtils.L10n(STRINGS_URI);
|
||||
|
||||
function prepareMessage(packet) {
|
||||
// @TODO turn this into an Immutable Record.
|
||||
let allowRepeating;
|
||||
let category;
|
||||
let data;
|
||||
let messageType;
|
||||
let repeat;
|
||||
let repeatId;
|
||||
let severity;
|
||||
|
||||
switch (packet.type) {
|
||||
case "consoleAPICall":
|
||||
data = Object.assign({}, packet.message);
|
||||
allowRepeating = true;
|
||||
category = CATEGORY_CLASS_FRAGMENTS[CATEGORY_WEBDEV];
|
||||
messageType = "ConsoleApiCall";
|
||||
repeat = 1;
|
||||
repeatId = getRepeatId(data);
|
||||
severity = SEVERITY_CLASS_FRAGMENTS[LEVELS[data.level]];
|
||||
break;
|
||||
case "pageError":
|
||||
data = Object.assign({}, packet.pageError);
|
||||
allowRepeating = true;
|
||||
category = CATEGORY_CLASS_FRAGMENTS[CATEGORY_JS];
|
||||
messageType = "PageError";
|
||||
repeat = 1;
|
||||
repeatId = getRepeatId(data);
|
||||
|
||||
severity = SEVERITY_CLASS_FRAGMENTS[SEVERITY_ERROR];
|
||||
if (data.warning || data.strict) {
|
||||
severity = SEVERITY_CLASS_FRAGMENTS[SEVERITY_WARNING];
|
||||
} else if (data.info) {
|
||||
severity = SEVERITY_CLASS_FRAGMENTS[SEVERITY_LOG];
|
||||
}
|
||||
break;
|
||||
case "evaluationResult":
|
||||
default:
|
||||
data = Object.assign({}, packet.result);
|
||||
allowRepeating = true;
|
||||
category = CATEGORY_CLASS_FRAGMENTS[CATEGORY_OUTPUT];
|
||||
messageType = "EvaluationResult";
|
||||
repeat = 1;
|
||||
repeatId = getRepeatId(data);
|
||||
severity = SEVERITY_CLASS_FRAGMENTS[SEVERITY_LOG];
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
allowRepeating,
|
||||
category,
|
||||
data,
|
||||
messageType,
|
||||
repeat,
|
||||
repeatId,
|
||||
severity
|
||||
};
|
||||
}
|
||||
|
||||
function getRepeatId(message) {
|
||||
let clonedMessage = JSON.parse(JSON.stringify(message));
|
||||
delete clonedMessage.timeStamp;
|
||||
delete clonedMessage.uniqueID;
|
||||
return JSON.stringify(clonedMessage);
|
||||
}
|
||||
|
||||
exports.prepareMessage = prepareMessage;
|
||||
// Export for use in testing.
|
||||
exports.getRepeatId = getRepeatId;
|
||||
|
||||
exports.l10n = l10n;
|
@ -0,0 +1,9 @@
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
DevToolsModules(
|
||||
'messages.js',
|
||||
'variables-view.js',
|
||||
)
|
@ -0,0 +1,17 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
/* global window */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @TODO Remove this.
|
||||
*
|
||||
* Once JSTerm is also written in React/Redux, these will be actions.
|
||||
*/
|
||||
exports.openVariablesView = (objectActor) => {
|
||||
window.jsterm.openVariablesView({objectActor});
|
||||
};
|
@ -198,6 +198,7 @@ const MIN_FONT_SIZE = 10;
|
||||
const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout";
|
||||
const PREF_PERSISTLOG = "devtools.webconsole.persistlog";
|
||||
const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
|
||||
const PREF_NEW_FRONTEND_ENABLED = "devtools.webconsole.new-frontend-enabled";
|
||||
|
||||
/**
|
||||
* A WebConsoleFrame instance is an interactive console initialized *per target*
|
||||
@ -508,6 +509,8 @@ WebConsoleFrame.prototype = {
|
||||
_initUI: function() {
|
||||
this.document = this.window.document;
|
||||
this.rootElement = this.document.documentElement;
|
||||
this.NEW_CONSOLE_OUTPUT_ENABLED = !this.owner._browserConsole &&
|
||||
Services.prefs.getBoolPref(PREF_NEW_FRONTEND_ENABLED);
|
||||
|
||||
this._initDefaultFilterPrefs();
|
||||
|
||||
@ -568,6 +571,22 @@ WebConsoleFrame.prototype = {
|
||||
this.jsterm = new JSTerm(this);
|
||||
this.jsterm.init();
|
||||
|
||||
if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
|
||||
// @TODO Remove this once JSTerm is handled with React/Redux.
|
||||
this.window.jsterm = this.jsterm;
|
||||
console.log("Entering experimental mode for console frontend");
|
||||
|
||||
// XXX: We should actually stop output from happening on old output
|
||||
// panel, but for now let's just hide it.
|
||||
this.experimentalOutputNode = this.outputNode.cloneNode();
|
||||
this.outputNode.hidden = true;
|
||||
this.outputNode.parentNode.appendChild(this.experimentalOutputNode);
|
||||
// @TODO Once the toolbox has been converted to React, see if passing
|
||||
// in JSTerm is still necessary.
|
||||
this.newConsoleOutput = new this.window.NewConsoleOutput(this.experimentalOutputNode, this.jsterm);
|
||||
console.log("Created newConsoleOutput", this.newConsoleOutput);
|
||||
}
|
||||
|
||||
this.resize();
|
||||
this.window.addEventListener("resize", this.resize, true);
|
||||
this.jsterm.on("sidebar-opened", this.resize);
|
||||
@ -3332,6 +3351,10 @@ WebConsoleConnectionProxy.prototype = {
|
||||
*/
|
||||
_onPageError: function(type, packet) {
|
||||
if (this.webConsoleFrame && packet.from == this._consoleActor) {
|
||||
if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
|
||||
this.webConsoleFrame.newConsoleOutput.dispatchMessageAdd(packet);
|
||||
return;
|
||||
}
|
||||
this.webConsoleFrame.handlePageError(packet.pageError);
|
||||
}
|
||||
},
|
||||
@ -3364,7 +3387,11 @@ WebConsoleConnectionProxy.prototype = {
|
||||
*/
|
||||
_onConsoleAPICall: function(type, packet) {
|
||||
if (this.webConsoleFrame && packet.from == this._consoleActor) {
|
||||
this.webConsoleFrame.handleConsoleAPICall(packet.message);
|
||||
if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
|
||||
this.webConsoleFrame.newConsoleOutput.dispatchMessageAdd(packet);
|
||||
} else {
|
||||
this.webConsoleFrame.handleConsoleAPICall(packet.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -26,6 +26,8 @@
|
||||
|
||||
<script type="application/javascript;version=1.8"
|
||||
src="chrome://devtools/content/shared/theme-switching.js"/>
|
||||
<script type="application/javascript;version=1.8"
|
||||
src="resource://devtools/client/webconsole/new-console-output/main.js"/>
|
||||
<script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
|
||||
<script type="text/javascript" src="resource://devtools/client/webconsole/net/main.js"/>
|
||||
<script type="text/javascript"><![CDATA[
|
||||
|
@ -152,6 +152,9 @@ var PageStyleActor = protocol.ActorClass({
|
||||
// Stores the association of DOM objects -> actors
|
||||
this.refMap = new Map();
|
||||
|
||||
// Maps document elements to style elements, used to add new rules.
|
||||
this.styleElements = new WeakMap();
|
||||
|
||||
this.onFrameUnload = this.onFrameUnload.bind(this);
|
||||
events.on(this.inspector.tabActor, "will-navigate", this.onFrameUnload);
|
||||
|
||||
@ -169,7 +172,7 @@ var PageStyleActor = protocol.ActorClass({
|
||||
this.walker = null;
|
||||
this.refMap = null;
|
||||
this.cssLogic = null;
|
||||
this._styleElement = null;
|
||||
this.styleElements = null;
|
||||
|
||||
for (let sheet of this._watchedSheets) {
|
||||
sheet.off("style-applied", this._styleApplied);
|
||||
@ -971,23 +974,26 @@ var PageStyleActor = protocol.ActorClass({
|
||||
* On page navigation, tidy up remaining objects.
|
||||
*/
|
||||
onFrameUnload: function() {
|
||||
this._styleElement = null;
|
||||
this.styleElements = new WeakMap();
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper function to addNewRule to construct a new style tag in the document.
|
||||
* Helper function to addNewRule to get or create a style tag in the provided
|
||||
* document.
|
||||
*
|
||||
* @param {Document} document
|
||||
* The document in which the style element should be appended.
|
||||
* @returns DOMElement of the style tag
|
||||
*/
|
||||
get styleElement() {
|
||||
if (!this._styleElement) {
|
||||
let document = this.inspector.window.document;
|
||||
getStyleElement: function(document) {
|
||||
if (!this.styleElements.has(document)) {
|
||||
let style = document.createElementNS(XHTML_NS, "style");
|
||||
style.setAttribute("type", "text/css");
|
||||
document.documentElement.appendChild(style);
|
||||
this._styleElement = style;
|
||||
this.styleElements.set(document, style);
|
||||
}
|
||||
|
||||
return this._styleElement;
|
||||
return this.styleElements.get(document);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1016,7 +1022,7 @@ var PageStyleActor = protocol.ActorClass({
|
||||
*/
|
||||
addNewRule: method(Task.async(function* (node, pseudoClasses,
|
||||
editAuthored = false) {
|
||||
let style = this.styleElement;
|
||||
let style = this.getStyleElement(node.rawNode.ownerDocument);
|
||||
let sheet = style.sheet;
|
||||
let cssRules = sheet.cssRules;
|
||||
let rawNode = node.rawNode;
|
||||
|
@ -189,7 +189,12 @@
|
||||
<issue id="UnlocalizedSms" severity="error" />
|
||||
<issue id="UnusedNamespace" severity="error" />
|
||||
<issue id="UnusedQuantity" severity="error" />
|
||||
<issue id="UnusedResources" severity="error" />
|
||||
<issue id="UnusedResources" severity="error">
|
||||
<!-- The moz.build based build system leaves a .mkdir.done file lying around in the
|
||||
preprocessed_resources res/raw folder. Lint reports it as unused. We should get
|
||||
rid of the file eventually. See bug 1268948. -->
|
||||
<ignore path="**/raw/.mkdir.done" />
|
||||
</issue>
|
||||
<issue id="Usability" severity="error" />
|
||||
<issue id="UseCheckPermission" severity="error" />
|
||||
<issue id="UseCompoundDrawables" severity="error" />
|
||||
|
13
mobile/android/base/crashreporter/res/values/colors.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?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/. -->
|
||||
|
||||
<resources>
|
||||
<!-- Crash Reporter Colors -->
|
||||
<color name="textbox_background">#FFF</color>
|
||||
<color name="textbox_background_disabled">#DDD</color>
|
||||
<color name="textbox_stroke">#000</color>
|
||||
<color name="textbox_stroke_disabled">#666</color>
|
||||
</resources>
|
||||
|
15
mobile/android/base/crashreporter/res/values/styles.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?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/. -->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Crash Reporter Styles -->
|
||||
<style name="CrashReporter" />
|
||||
|
||||
<style name="CrashReporter.EditText">
|
||||
<item name="android:background">@drawable/textbox_bg</item>
|
||||
<item name="android:padding">10dp</item>
|
||||
<item name="android:textAppearance">@style/TextAppearance</item>
|
||||
</style>
|
||||
</resources>
|
@ -11,7 +11,6 @@ import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Environment;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.WorkerThread;
|
||||
import org.json.JSONArray;
|
||||
import org.mozilla.gecko.adjust.AdjustHelperInterface;
|
||||
@ -59,7 +58,8 @@ import org.mozilla.gecko.permissions.Permissions;
|
||||
import org.mozilla.gecko.preferences.ClearOnShutdownPref;
|
||||
import org.mozilla.gecko.preferences.GeckoPreferences;
|
||||
import org.mozilla.gecko.promotion.AddToHomeScreenPromotion;
|
||||
import org.mozilla.gecko.promotion.SimpleHelperUI;
|
||||
import org.mozilla.gecko.promotion.BookmarkStateChangeDelegate;
|
||||
import org.mozilla.gecko.promotion.ReaderViewBookmarkPromotion;
|
||||
import org.mozilla.gecko.prompts.Prompt;
|
||||
import org.mozilla.gecko.prompts.PromptListItem;
|
||||
import org.mozilla.gecko.reader.SavedReaderViewHelper;
|
||||
@ -132,7 +132,6 @@ import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.view.MenuItemCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
@ -214,9 +213,12 @@ public class BrowserApp extends GeckoApp
|
||||
// Request ID for startActivityForResult.
|
||||
private static final int ACTIVITY_REQUEST_PREFERENCES = 1001;
|
||||
private static final int ACTIVITY_REQUEST_TAB_QUEUE = 2001;
|
||||
private static final int ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK = 3001;
|
||||
private static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002;
|
||||
private static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003;
|
||||
public static final int ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK = 3001;
|
||||
public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002;
|
||||
public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003;
|
||||
public static final int ACTIVITY_REQUEST_TRIPLE_READERVIEW = 4001;
|
||||
public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK = 4002;
|
||||
public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE = 4003;
|
||||
|
||||
public static final String ACTION_VIEW_MULTIPLE = AppConstants.ANDROID_PACKAGE_NAME + ".action.VIEW_MULTIPLE";
|
||||
|
||||
@ -307,7 +309,9 @@ public class BrowserApp extends GeckoApp
|
||||
|
||||
private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
|
||||
(BrowserAppDelegate) new AddToHomeScreenPromotion(),
|
||||
(BrowserAppDelegate) new ScreenshotDelegate()
|
||||
(BrowserAppDelegate) new ScreenshotDelegate(),
|
||||
(BrowserAppDelegate) new BookmarkStateChangeDelegate(),
|
||||
(BrowserAppDelegate) new ReaderViewBookmarkPromotion()
|
||||
));
|
||||
|
||||
@NonNull
|
||||
@ -378,39 +382,6 @@ public class BrowserApp extends GeckoApp
|
||||
case PAGE_SHOW:
|
||||
tab.loadFavicon();
|
||||
break;
|
||||
case BOOKMARK_ADDED:
|
||||
// We always show the special offline snackbar whenever we bookmark a reader page.
|
||||
// It's possible that the page is already stored offline, however this is highly
|
||||
// unlikely, and even so it is probably nicer to show the same offline notification
|
||||
// every time we bookmark an about:reader page.
|
||||
if (!AboutPages.isAboutReader(tab.getURL())) {
|
||||
showBookmarkAddedSnackbar();
|
||||
} else {
|
||||
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
|
||||
|
||||
final boolean hasFirstReaderViewPromptBeenShownBefore = prefs.getBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, false);
|
||||
|
||||
if (hasFirstReaderViewPromptBeenShownBefore) {
|
||||
showReaderModeBookmarkAddedSnackbar();
|
||||
} else {
|
||||
SimpleHelperUI.show(this,
|
||||
SimpleHelperUI.FIRST_RVBP_SHOWN_TELEMETRYEXTRA,
|
||||
ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK,
|
||||
R.string.helper_first_offline_bookmark_title, R.string.helper_first_offline_bookmark_message,
|
||||
R.drawable.helper_first_readerview_bookmark, R.string.helper_first_offline_bookmark_button,
|
||||
ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS,
|
||||
ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE);
|
||||
|
||||
GeckoSharedPrefs.forProfile(this)
|
||||
.edit()
|
||||
.putBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, true)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case BOOKMARK_REMOVED:
|
||||
showBookmarkRemovedSnackbar();
|
||||
break;
|
||||
|
||||
case UNSELECTED:
|
||||
// We receive UNSELECTED immediately after the SELECTED listeners run
|
||||
@ -466,49 +437,6 @@ public class BrowserApp extends GeckoApp
|
||||
}
|
||||
}
|
||||
|
||||
private void showBookmarkAddedSnackbar() {
|
||||
// This flow is from the option menu which has check to see if a bookmark was already added.
|
||||
// So, it is safe here to show the snackbar that bookmark_added without any checks.
|
||||
|
||||
final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "bookmark_options");
|
||||
showBookmarkDialog();
|
||||
}
|
||||
};
|
||||
|
||||
SnackbarHelper.showSnackbarWithAction(this,
|
||||
getResources().getString(R.string.bookmark_added),
|
||||
Snackbar.LENGTH_LONG,
|
||||
getResources().getString(R.string.bookmark_options),
|
||||
callback);
|
||||
}
|
||||
|
||||
private void showReaderModeBookmarkAddedSnackbar() {
|
||||
final Drawable iconDownloaded = DrawableUtil.tintDrawable(getContext(), R.drawable.status_icon_readercache, Color.WHITE);
|
||||
|
||||
final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(PanelType.BOOKMARKS));
|
||||
}
|
||||
};
|
||||
|
||||
SnackbarHelper.showSnackbarWithActionAndColors(this,
|
||||
getResources().getString(R.string.reader_saved_offline),
|
||||
Snackbar.LENGTH_LONG,
|
||||
getResources().getString(R.string.reader_switch_to_bookmarks),
|
||||
callback,
|
||||
iconDownloaded,
|
||||
ContextCompat.getColor(this, R.color.link_blue),
|
||||
Color.WHITE);
|
||||
}
|
||||
|
||||
private void showBookmarkRemovedSnackbar() {
|
||||
SnackbarHelper.showSnackbar(this, getResources().getString(R.string.bookmark_removed), Snackbar.LENGTH_LONG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (AndroidGamepadManager.handleKeyEvent(event)) {
|
||||
@ -1086,7 +1014,7 @@ public class BrowserApp extends GeckoApp
|
||||
}
|
||||
|
||||
EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
|
||||
"Prompt:ShowTop");
|
||||
"Prompt:ShowTop");
|
||||
|
||||
processTabQueue();
|
||||
|
||||
@ -1265,57 +1193,6 @@ public class BrowserApp extends GeckoApp
|
||||
mBrowserToolbar.setOnKeyListener(this);
|
||||
}
|
||||
|
||||
private void showBookmarkDialog() {
|
||||
final Resources res = getResources();
|
||||
final Tab tab = Tabs.getInstance().getSelectedTab();
|
||||
|
||||
final Prompt ps = new Prompt(this, new Prompt.PromptCallback() {
|
||||
@Override
|
||||
public void onPromptFinished(String result) {
|
||||
int itemId = -1;
|
||||
try {
|
||||
itemId = new JSONObject(result).getInt("button");
|
||||
} catch (JSONException ex) {
|
||||
Log.e(LOGTAG, "Exception reading bookmark prompt result", ex);
|
||||
}
|
||||
|
||||
if (tab == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemId == 0) {
|
||||
final String extrasId = res.getResourceEntryName(R.string.contextmenu_edit_bookmark);
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
|
||||
TelemetryContract.Method.DIALOG, extrasId);
|
||||
|
||||
new EditBookmarkDialog(BrowserApp.this).show(tab.getURL());
|
||||
} else if (itemId == 1) {
|
||||
final String extrasId = res.getResourceEntryName(R.string.contextmenu_add_to_launcher);
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
|
||||
TelemetryContract.Method.DIALOG, extrasId);
|
||||
|
||||
final String url = tab.getURL();
|
||||
final String title = tab.getDisplayTitle();
|
||||
|
||||
if (url != null && title != null) {
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GeckoAppShell.createShortcut(title, url);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final PromptListItem[] items = new PromptListItem[2];
|
||||
items[0] = new PromptListItem(res.getString(R.string.contextmenu_edit_bookmark));
|
||||
items[1] = new PromptListItem(res.getString(R.string.contextmenu_add_to_launcher));
|
||||
|
||||
ps.show("", "", items, ListView.CHOICE_MODE_NONE);
|
||||
}
|
||||
|
||||
private void setDynamicToolbarEnabled(boolean enabled) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
@ -2341,7 +2218,7 @@ public class BrowserApp extends GeckoApp
|
||||
return true;
|
||||
}
|
||||
|
||||
private void openUrlAndStopEditing(String url) {
|
||||
public void openUrlAndStopEditing(String url) {
|
||||
openUrlAndStopEditing(url, null, false);
|
||||
}
|
||||
|
||||
@ -2693,15 +2570,11 @@ public class BrowserApp extends GeckoApp
|
||||
TabQueueHelper.processTabQueuePromptResponse(resultCode, this);
|
||||
break;
|
||||
|
||||
case ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK:
|
||||
if (resultCode == ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS) {
|
||||
openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(PanelType.BOOKMARKS));
|
||||
} else if (resultCode == ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE) {
|
||||
showReaderModeBookmarkAddedSnackbar();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
for (final BrowserAppDelegate delegate : delegates) {
|
||||
delegate.onActivityResult(this, requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
@ -3170,12 +3043,10 @@ public class BrowserApp extends GeckoApp
|
||||
// Action providers are available only ICS+.
|
||||
if (Versions.feature14Plus) {
|
||||
GeckoMenuItem share = (GeckoMenuItem) mMenu.findItem(R.id.share);
|
||||
final GeckoMenuItem quickShare = (GeckoMenuItem) mMenu.findItem(R.id.quickshare);
|
||||
|
||||
GeckoActionProvider provider = GeckoActionProvider.getForType(GeckoActionProvider.DEFAULT_MIME_TYPE, this);
|
||||
|
||||
share.setActionProvider(provider);
|
||||
quickShare.setActionProvider(provider);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -3272,7 +3143,6 @@ public class BrowserApp extends GeckoApp
|
||||
final MenuItem back = aMenu.findItem(R.id.back);
|
||||
final MenuItem forward = aMenu.findItem(R.id.forward);
|
||||
final MenuItem share = aMenu.findItem(R.id.share);
|
||||
final MenuItem quickShare = aMenu.findItem(R.id.quickshare);
|
||||
final MenuItem bookmarksList = aMenu.findItem(R.id.bookmarks_list);
|
||||
final MenuItem historyList = aMenu.findItem(R.id.history_list);
|
||||
final MenuItem saveAsPDF = aMenu.findItem(R.id.save_as_pdf);
|
||||
@ -3300,7 +3170,6 @@ public class BrowserApp extends GeckoApp
|
||||
back.setEnabled(false);
|
||||
forward.setEnabled(false);
|
||||
share.setEnabled(false);
|
||||
quickShare.setEnabled(false);
|
||||
saveAsPDF.setEnabled(false);
|
||||
print.setEnabled(false);
|
||||
findInPage.setEnabled(false);
|
||||
@ -3400,9 +3269,6 @@ public class BrowserApp extends GeckoApp
|
||||
|
||||
// Action providers are available only ICS+.
|
||||
if (Versions.feature14Plus) {
|
||||
quickShare.setVisible(shareVisible);
|
||||
quickShare.setEnabled(shareEnabled);
|
||||
|
||||
// This provider also applies to the quick share menu item.
|
||||
final GeckoActionProvider provider = ((GeckoMenuItem) share).getGeckoActionProvider();
|
||||
if (provider != null) {
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
package org.mozilla.gecko;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.mozilla.gecko.tabs.TabsPanel;
|
||||
@ -58,4 +59,13 @@ public abstract class BrowserAppDelegate {
|
||||
* Called when the tabs tray is closed.
|
||||
*/
|
||||
public void onTabsTrayHidden(BrowserApp browserApp, TabsPanel tabsPanel) {}
|
||||
|
||||
/**
|
||||
* Called when an activity started using startActivityForResult() returns.
|
||||
*
|
||||
* Delegates should only use request and result codes declared in BrowserApp itself (as opposed
|
||||
* to declarations in the delegate), in order to avoid conflicts.
|
||||
*/
|
||||
public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,30 @@
|
||||
|
||||
package org.mozilla.gecko;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.WorkerThread;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
|
||||
import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
|
||||
import org.mozilla.gecko.annotation.RobocopTarget;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.db.LocalBrowserDB;
|
||||
import org.mozilla.gecko.db.StubBrowserDB;
|
||||
import org.mozilla.gecko.distribution.Distribution;
|
||||
import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
|
||||
import org.mozilla.gecko.mozglue.ContextUtils;
|
||||
import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
|
||||
import org.mozilla.gecko.util.INIParser;
|
||||
import org.mozilla.gecko.util.INISection;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@ -20,31 +44,6 @@ import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.annotation.RobocopTarget;
|
||||
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
|
||||
import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.db.LocalBrowserDB;
|
||||
import org.mozilla.gecko.db.StubBrowserDB;
|
||||
import org.mozilla.gecko.distribution.Distribution;
|
||||
import org.mozilla.gecko.mozglue.ContextUtils;
|
||||
import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
|
||||
import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
|
||||
import org.mozilla.gecko.util.INIParser;
|
||||
import org.mozilla.gecko.util.INISection;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.WorkerThread;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
public final class GeckoProfile {
|
||||
private static final String LOGTAG = "GeckoProfile";
|
||||
|
||||
@ -719,7 +718,7 @@ public final class GeckoProfile {
|
||||
return getProfileCreationDateFromTimesFile();
|
||||
} catch (final IOException e) {
|
||||
Log.d(LOGTAG, "Unable to retrieve profile creation date from times.json. Getting from system...");
|
||||
final long packageInstallMillis = org.mozilla.gecko.util.ContextUtils.getPackageInstallTime(context);
|
||||
final long packageInstallMillis = org.mozilla.gecko.util.ContextUtils.getCurrentPackageInfo(context).firstInstallTime;
|
||||
try {
|
||||
persistProfileCreationDateToTimesFile(packageInstallMillis);
|
||||
} catch (final IOException ioEx) {
|
||||
|
@ -1,246 +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;
|
||||
|
||||
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.text.format.DateUtils;
|
||||
import org.mozilla.gecko.db.RemoteClient;
|
||||
import org.mozilla.gecko.db.RemoteTab;
|
||||
import org.mozilla.gecko.home.TwoLinePageRow;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseExpandableListAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An adapter that populates group and child views with remote client and tab
|
||||
* data maintained in a monolithic static array.
|
||||
* <p>
|
||||
* The group and child view resources are parameters to allow future
|
||||
* specialization to home fragment styles.
|
||||
*/
|
||||
public class RemoteTabsExpandableListAdapter extends BaseExpandableListAdapter {
|
||||
/**
|
||||
* If a device claims to have synced before this date, we will assume it has never synced.
|
||||
*/
|
||||
private static final Date EARLIEST_VALID_SYNCED_DATE;
|
||||
static {
|
||||
final Calendar c = GregorianCalendar.getInstance();
|
||||
c.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
|
||||
EARLIEST_VALID_SYNCED_DATE = c.getTime();
|
||||
}
|
||||
protected final ArrayList<RemoteClient> clients;
|
||||
private final boolean showGroupIndicator;
|
||||
protected int groupLayoutId;
|
||||
protected int childLayoutId;
|
||||
|
||||
public static class GroupViewHolder {
|
||||
final TextView nameView;
|
||||
final TextView lastModifiedView;
|
||||
final ImageView deviceTypeView;
|
||||
final ImageView deviceExpandedView;
|
||||
|
||||
public GroupViewHolder(View view) {
|
||||
nameView = (TextView) view.findViewById(R.id.client);
|
||||
lastModifiedView = (TextView) view.findViewById(R.id.last_synced);
|
||||
deviceTypeView = (ImageView) view.findViewById(R.id.device_type);
|
||||
deviceExpandedView = (ImageView) view.findViewById(R.id.device_expanded);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new adapter.
|
||||
* <p>
|
||||
* It's fine to create with clients to be null, and then to use
|
||||
* {@link RemoteTabsExpandableListAdapter#replaceClients(List)} to
|
||||
* update this list of clients.
|
||||
*
|
||||
* @param groupLayoutId
|
||||
* @param childLayoutId
|
||||
* @param clients
|
||||
* initial list of clients; can be null.
|
||||
* @param showGroupIndicator
|
||||
*/
|
||||
public RemoteTabsExpandableListAdapter(int groupLayoutId, int childLayoutId, List<RemoteClient> clients, boolean showGroupIndicator) {
|
||||
this.groupLayoutId = groupLayoutId;
|
||||
this.childLayoutId = childLayoutId;
|
||||
this.clients = new ArrayList<>();
|
||||
if (clients != null) {
|
||||
this.clients.addAll(clients);
|
||||
}
|
||||
this.showGroupIndicator = showGroupIndicator;
|
||||
}
|
||||
|
||||
public void replaceClients(List<RemoteClient> clients) {
|
||||
this.clients.clear();
|
||||
if (clients != null) {
|
||||
this.clients.addAll(clients);
|
||||
this.notifyDataSetChanged();
|
||||
} else {
|
||||
this.notifyDataSetInvalidated();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return false; // Client GUIDs are stable, but tab hashes are not.
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getGroupId(int groupPosition) {
|
||||
return clients.get(groupPosition).guid.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGroupCount() {
|
||||
return clients.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getGroup(int groupPosition) {
|
||||
return clients.get(groupPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildrenCount(int groupPosition) {
|
||||
return clients.get(groupPosition).tabs.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
|
||||
final Context context = parent.getContext();
|
||||
final View view;
|
||||
if (convertView != null) {
|
||||
view = convertView;
|
||||
} else {
|
||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||
view = inflater.inflate(groupLayoutId, parent, false);
|
||||
final GroupViewHolder holder = new GroupViewHolder(view);
|
||||
view.setTag(holder);
|
||||
}
|
||||
|
||||
final RemoteClient client = clients.get(groupPosition);
|
||||
updateClientsItemView(isExpanded, context, view, client);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public void updateClientsItemView(final boolean isExpanded, final Context context, final View view, final RemoteClient client) {
|
||||
final GroupViewHolder holder = (GroupViewHolder) view.getTag();
|
||||
|
||||
// UI elements whose state depends on isExpanded, roughly from left to
|
||||
// right: device type icon; client name text color; expanded state
|
||||
// indicator.
|
||||
final int deviceTypeResId;
|
||||
final int textColorResId;
|
||||
final int deviceExpandedResId;
|
||||
|
||||
if (isExpanded && !client.tabs.isEmpty()) {
|
||||
deviceTypeResId = "desktop".equals(client.deviceType) ? R.drawable.sync_desktop : R.drawable.sync_mobile;
|
||||
textColorResId = R.color.placeholder_active_grey;
|
||||
deviceExpandedResId = showGroupIndicator ? R.drawable.arrow_down : R.drawable.home_group_collapsed;
|
||||
} else {
|
||||
deviceTypeResId = "desktop".equals(client.deviceType) ? R.drawable.sync_desktop_inactive : R.drawable.sync_mobile_inactive;
|
||||
textColorResId = R.color.tabs_tray_icon_grey;
|
||||
deviceExpandedResId = showGroupIndicator ? R.drawable.home_group_collapsed : 0;
|
||||
}
|
||||
|
||||
// Now update the UI.
|
||||
holder.nameView.setText(client.name);
|
||||
holder.nameView.setTextColor(ContextCompat.getColor(context, textColorResId));
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
|
||||
// It's OK to access the DB on the main thread here, as we're just
|
||||
// getting a string.
|
||||
final GeckoProfile profile = GeckoProfile.get(context);
|
||||
holder.lastModifiedView.setText(getLastSyncedString(context, now, client.lastModified));
|
||||
|
||||
// These views exists only in some of our group views: they are present
|
||||
// for the home panel groups and not for the tabs panel groups.
|
||||
// Therefore, we must handle null.
|
||||
if (holder.deviceTypeView != null) {
|
||||
holder.deviceTypeView.setImageResource(deviceTypeResId);
|
||||
}
|
||||
|
||||
if (holder.deviceExpandedView != null) {
|
||||
// If there are no tabs to display, don't show an indicator at all.
|
||||
holder.deviceExpandedView.setImageResource(client.tabs.isEmpty() ? 0 : deviceExpandedResId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChildSelectable(int groupPosition, int childPosition) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChild(int groupPosition, int childPosition) {
|
||||
return clients.get(groupPosition).tabs.get(childPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getChildId(int groupPosition, int childPosition) {
|
||||
return clients.get(groupPosition).tabs.get(childPosition).hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
|
||||
final Context context = parent.getContext();
|
||||
final View view;
|
||||
if (convertView != null) {
|
||||
view = convertView;
|
||||
} else {
|
||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||
view = inflater.inflate(childLayoutId, parent, false);
|
||||
}
|
||||
|
||||
final RemoteClient client = clients.get(groupPosition);
|
||||
final RemoteTab tab = client.tabs.get(childPosition);
|
||||
|
||||
// The view is a TwoLinePageRow only for some of our child views: it's
|
||||
// present for the home panel children and not for the tabs panel
|
||||
// children. Therefore, we must handle one case manually.
|
||||
if (view instanceof TwoLinePageRow) {
|
||||
((TwoLinePageRow) view).update(tab.title, tab.url);
|
||||
} else {
|
||||
final TextView titleView = (TextView) view.findViewById(R.id.title);
|
||||
titleView.setText(TextUtils.isEmpty(tab.title) ? tab.url : tab.title);
|
||||
|
||||
final TextView urlView = (TextView) view.findViewById(R.id.url);
|
||||
urlView.setText(tab.url);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a relative "Last synced" time span for the given tab record.
|
||||
*
|
||||
* @param now local time.
|
||||
* @param time to format string for.
|
||||
* @return string describing time span
|
||||
*/
|
||||
public static String getLastSyncedString(Context context, long now, long time) {
|
||||
if (new Date(time).before(EARLIEST_VALID_SYNCED_DATE)) {
|
||||
return context.getString(R.string.remote_tabs_never_synced);
|
||||
}
|
||||
final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
|
||||
return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ public class RemoteClient implements Parcelable {
|
||||
this.name = name;
|
||||
this.lastModified = lastModified;
|
||||
this.deviceType = deviceType;
|
||||
this.tabs = new ArrayList<RemoteTab>();
|
||||
this.tabs = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -62,4 +62,8 @@ public class RemoteClient implements Parcelable {
|
||||
return new RemoteClient[size];
|
||||
}
|
||||
};
|
||||
|
||||
public boolean isDesktop() {
|
||||
return "desktop".equals(deviceType);
|
||||
}
|
||||
}
|
||||
|
@ -4,17 +4,13 @@
|
||||
|
||||
package org.mozilla.gecko.db;
|
||||
|
||||
import org.mozilla.gecko.RemoteTabsExpandableListAdapter;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* A thin representation of a remote tab.
|
||||
* <p>
|
||||
* We use the hash of the tab as the ID in
|
||||
* {@link RemoteTabsExpandableListAdapter#getClientId(int)}, and therefore we
|
||||
* must implement equality as well. These are generated functions.
|
||||
* These are generated functions.
|
||||
*/
|
||||
public class RemoteTab implements Parcelable {
|
||||
public final String title;
|
||||
|
@ -6,13 +6,13 @@
|
||||
package org.mozilla.gecko.dlc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContent;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
|
||||
import org.mozilla.gecko.util.ContextUtils;
|
||||
|
||||
/**
|
||||
* Study: Scan the catalog for "new" content available for download.
|
||||
@ -64,14 +64,10 @@ public class StudyAction extends BaseAction {
|
||||
|
||||
final String appVersionPattern = content.getAppVersionPattern();
|
||||
if (!TextUtils.isEmpty(appVersionPattern)) {
|
||||
try {
|
||||
final String appVersion = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
|
||||
if (!appVersion.matches(appVersionPattern)) {
|
||||
Log.d(LOGTAG, String.format("App version (%s) does not match pattern: %s", appVersion, appVersionPattern));
|
||||
return false;
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new AssertionError("Should not happen: Can't get package info of own package");
|
||||
final String appVersion = ContextUtils.getCurrentPackageInfo(context).versionName;
|
||||
if (!appVersion.matches(appVersionPattern)) {
|
||||
Log.d(LOGTAG, String.format("App version (%s) does not match pattern: %s", appVersion, appVersionPattern));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,323 @@
|
||||
/* -*- 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.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v4.util.Pair;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.db.RemoteClient;
|
||||
import org.mozilla.gecko.db.RemoteTab;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mozilla.gecko.home.CombinedHistoryItem.ItemType.*;
|
||||
|
||||
public class ClientsAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
|
||||
public static final String LOGTAG = "GeckoClientsAdapter";
|
||||
|
||||
/**
|
||||
* If a device claims to have synced before this date, we will assume it has never synced.
|
||||
*/
|
||||
public static final Date EARLIEST_VALID_SYNCED_DATE;
|
||||
static {
|
||||
final Calendar c = GregorianCalendar.getInstance();
|
||||
c.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
|
||||
EARLIEST_VALID_SYNCED_DATE = c.getTime();
|
||||
}
|
||||
|
||||
List<Pair<String, Integer>> adapterList = new LinkedList<>();
|
||||
|
||||
// List of hidden remote clients.
|
||||
// Only accessed from the UI thread.
|
||||
protected final List<RemoteClient> hiddenClients = new ArrayList<>();
|
||||
private Map<String, RemoteClient> visibleClients = new HashMap<>();
|
||||
|
||||
// Maintain group collapsed and hidden state. Only accessed from the UI thread.
|
||||
protected static RemoteTabsExpandableListState sState;
|
||||
|
||||
private final Context context;
|
||||
|
||||
public ClientsAdapter(Context context) {
|
||||
this.context = context;
|
||||
|
||||
// This races when multiple Fragments are created. That's okay: one
|
||||
// will win, and thereafter, all will be okay. If we create and then
|
||||
// drop an instance the shared SharedPreferences backing all the
|
||||
// instances will maintain the state for us. Since everything happens on
|
||||
// the UI thread, this doesn't even need to be volatile.
|
||||
if (sState == null) {
|
||||
sState = new RemoteTabsExpandableListState(GeckoSharedPrefs.forProfile(context));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
final View view;
|
||||
|
||||
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
|
||||
|
||||
switch (itemType) {
|
||||
case NAVIGATION_BACK:
|
||||
view = inflater.inflate(R.layout.home_combined_back_item, parent, false);
|
||||
return new CombinedHistoryItem.HistoryItem(view);
|
||||
|
||||
case CLIENT:
|
||||
view = inflater.inflate(R.layout.home_remote_tabs_group, parent, false);
|
||||
return new CombinedHistoryItem.ClientItem(view);
|
||||
|
||||
case CHILD:
|
||||
view = inflater.inflate(R.layout.home_item_row, parent, false);
|
||||
return new CombinedHistoryItem.HistoryItem(view);
|
||||
|
||||
case HIDDEN_DEVICES:
|
||||
view = inflater.inflate(R.layout.home_remote_tabs_hidden_devices, parent, false);
|
||||
return new CombinedHistoryItem.BasicItem(view);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder (CombinedHistoryItem holder, final int position){
|
||||
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
|
||||
|
||||
switch (itemType) {
|
||||
case CLIENT:
|
||||
final CombinedHistoryItem.ClientItem clientItem = (CombinedHistoryItem.ClientItem) holder;
|
||||
final String clientGuid = adapterList.get(position).first;
|
||||
final RemoteClient client = visibleClients.get(clientGuid);
|
||||
clientItem.bind(context, client, sState.isClientCollapsed(clientGuid));
|
||||
break;
|
||||
|
||||
case CHILD:
|
||||
final Pair<String, Integer> pair = adapterList.get(position);
|
||||
RemoteTab remoteTab = visibleClients.get(pair.first).tabs.get(pair.second);
|
||||
((CombinedHistoryItem.HistoryItem) holder).bind(remoteTab);
|
||||
break;
|
||||
|
||||
case HIDDEN_DEVICES:
|
||||
final String hiddenDevicesLabel = context.getResources().getString(R.string.home_remote_tabs_many_hidden_devices, hiddenClients.size());
|
||||
((TextView) holder.itemView).setText(hiddenDevicesLabel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount () {
|
||||
return adapterList.size();
|
||||
}
|
||||
|
||||
private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
|
||||
if (position == 0) {
|
||||
return NAVIGATION_BACK;
|
||||
}
|
||||
|
||||
final Pair<String, Integer> pair = adapterList.get(position);
|
||||
if (pair == null) {
|
||||
return HIDDEN_DEVICES;
|
||||
} else if (pair.second == -1) {
|
||||
return CLIENT;
|
||||
} else {
|
||||
return CHILD;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void setClients(List<RemoteClient> clients) {
|
||||
adapterList.clear();
|
||||
adapterList.add(null);
|
||||
|
||||
hiddenClients.clear();
|
||||
visibleClients.clear();
|
||||
|
||||
for (RemoteClient client : clients) {
|
||||
final String guid = client.guid;
|
||||
if (sState.isClientHidden(guid)) {
|
||||
hiddenClients.add(client);
|
||||
} else {
|
||||
visibleClients.put(guid, client);
|
||||
adapterList.addAll(getVisibleItems(client));
|
||||
}
|
||||
}
|
||||
|
||||
// Add item for unhiding clients.
|
||||
if (!hiddenClients.isEmpty()) {
|
||||
adapterList.add(null);
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private static List<Pair<String, Integer>> getVisibleItems(RemoteClient client) {
|
||||
List<Pair<String, Integer>> list = new LinkedList<>();
|
||||
final String guid = client.guid;
|
||||
list.add(new Pair<>(guid, -1));
|
||||
if (!sState.isClientCollapsed(client.guid)) {
|
||||
for (int i = 0; i < client.tabs.size(); i++) {
|
||||
list.add(new Pair<>(guid, i));
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<RemoteClient> getHiddenClients() {
|
||||
return hiddenClients;
|
||||
}
|
||||
|
||||
public void toggleClient(int position) {
|
||||
final Pair<String, Integer> pair = adapterList.get(position);
|
||||
if (pair.second != -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String clientGuid = pair.first;
|
||||
final RemoteClient client = visibleClients.get(clientGuid);
|
||||
|
||||
final boolean isCollapsed = sState.isClientCollapsed(clientGuid);
|
||||
|
||||
sState.setClientCollapsed(clientGuid, !isCollapsed);
|
||||
notifyItemChanged(position);
|
||||
|
||||
if (isCollapsed) {
|
||||
for (int i = client.tabs.size() - 1; i > -1; i--) {
|
||||
// Insert child tabs at the index right after the client item that was clicked.
|
||||
adapterList.add(position + 1, new Pair<>(clientGuid, i));
|
||||
}
|
||||
notifyItemRangeInserted(position + 1, client.tabs.size());
|
||||
} else {
|
||||
int i = client.tabs.size();
|
||||
while (i > 0) {
|
||||
adapterList.remove(position + 1);
|
||||
i--;
|
||||
}
|
||||
notifyItemRangeRemoved(position + 1, client.tabs.size());
|
||||
}
|
||||
}
|
||||
|
||||
public void unhideClients(List<RemoteClient> selectedClients) {
|
||||
final int numClients = selectedClients.size();
|
||||
if (numClients == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int insertionIndex = adapterList.size() - 1;
|
||||
int itemCount = numClients;
|
||||
|
||||
for (RemoteClient client : selectedClients) {
|
||||
final String clientGuid = client.guid;
|
||||
|
||||
sState.setClientHidden(clientGuid, false);
|
||||
hiddenClients.remove(client);
|
||||
|
||||
visibleClients.put(clientGuid, client);
|
||||
sState.setClientCollapsed(clientGuid, false);
|
||||
adapterList.addAll(adapterList.size() - 1, getVisibleItems(client));
|
||||
|
||||
itemCount += client.tabs.size();
|
||||
}
|
||||
|
||||
notifyItemRangeInserted(insertionIndex, itemCount);
|
||||
|
||||
final int hiddenDevicesIndex = adapterList.size() - 1;
|
||||
if (hiddenClients.isEmpty()) {
|
||||
// No more hidden clients, remove "unhide" item.
|
||||
adapterList.remove(hiddenDevicesIndex);
|
||||
notifyItemRemoved(hiddenDevicesIndex);
|
||||
} else {
|
||||
// Update "hidden clients" item because number of hidden clients changed.
|
||||
notifyItemChanged(hiddenDevicesIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeItem(int position) {
|
||||
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
|
||||
switch (itemType) {
|
||||
case CLIENT:
|
||||
final String clientGuid = adapterList.get(position).first;
|
||||
final RemoteClient client = visibleClients.remove(clientGuid);
|
||||
final boolean hadHiddenClients = !hiddenClients.isEmpty();
|
||||
|
||||
int removeCount = sState.isClientCollapsed(clientGuid) ? 1 : client.tabs.size() + 1;
|
||||
int c = removeCount;
|
||||
while (c > 0) {
|
||||
adapterList.remove(position);
|
||||
c--;
|
||||
}
|
||||
notifyItemRangeRemoved(position, removeCount);
|
||||
|
||||
sState.setClientHidden(clientGuid, true);
|
||||
hiddenClients.add(client);
|
||||
|
||||
if (!hadHiddenClients) {
|
||||
// Add item for unhiding clients;
|
||||
adapterList.add(null);
|
||||
notifyItemInserted(adapterList.size() - 1);
|
||||
} else {
|
||||
// Update "hidden clients" item because number of hidden clients changed.
|
||||
notifyItemChanged(adapterList.size() - 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
|
||||
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
|
||||
HomeContextMenuInfo info;
|
||||
final Pair<String, Integer> pair = adapterList.get(position);
|
||||
switch (itemType) {
|
||||
case CHILD:
|
||||
info = new HomeContextMenuInfo(view, position, -1);
|
||||
return populateChildInfoFromTab(info, visibleClients.get(pair.first).tabs.get(pair.second));
|
||||
|
||||
case CLIENT:
|
||||
info = new CombinedHistoryPanel.RemoteTabsClientContextMenuInfo(view, position, -1, visibleClients.get(pair.first));
|
||||
return info;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, RemoteTab tab) {
|
||||
info.url = tab.url;
|
||||
info.title = tab.title;
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a relative "Last synced" time span for the given tab record.
|
||||
*
|
||||
* @param now local time.
|
||||
* @param time to format string for.
|
||||
* @return string describing time span
|
||||
*/
|
||||
public static String getLastSyncedString(Context context, long now, long time) {
|
||||
if (new Date(time).before(EARLIEST_VALID_SYNCED_DATE)) {
|
||||
return context.getString(R.string.remote_tabs_never_synced);
|
||||
}
|
||||
final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
|
||||
return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
|
||||
}
|
||||
}
|
@ -4,101 +4,49 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
package org.mozilla.gecko.home;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import org.json.JSONArray;
|
||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.db.RemoteClient;
|
||||
import org.mozilla.gecko.db.RemoteTab;
|
||||
import org.mozilla.gecko.home.CombinedHistoryPanel.SectionHeader;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
|
||||
private static final int SYNCED_DEVICES_SMARTFOLDER_INDEX = 0;
|
||||
|
||||
public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistoryItem> {
|
||||
private static final String LOGTAG = "GeckoCombinedHistAdapt";
|
||||
// Array for the time ranges in milliseconds covered by each section.
|
||||
static final HistorySectionsHelper.SectionDateRange[] sectionDateRangeArray = new HistorySectionsHelper.SectionDateRange[SectionHeader.values().length];
|
||||
|
||||
public enum ItemType {
|
||||
CLIENT, HIDDEN_DEVICES, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD;
|
||||
|
||||
public static ItemType viewTypeToItemType(int viewType) {
|
||||
if (viewType >= ItemType.values().length) {
|
||||
Log.e(LOGTAG, "No corresponding ItemType!");
|
||||
}
|
||||
return ItemType.values()[viewType];
|
||||
}
|
||||
|
||||
public static int itemTypeToViewType(ItemType itemType) {
|
||||
return itemType.ordinal();
|
||||
}
|
||||
// Semantic names for the time covered by each section
|
||||
public enum SectionHeader {
|
||||
TODAY,
|
||||
YESTERDAY,
|
||||
WEEK,
|
||||
THIS_MONTH,
|
||||
MONTH_AGO,
|
||||
TWO_MONTHS_AGO,
|
||||
THREE_MONTHS_AGO,
|
||||
FOUR_MONTHS_AGO,
|
||||
FIVE_MONTHS_AGO,
|
||||
OLDER_THAN_SIX_MONTHS
|
||||
}
|
||||
|
||||
private List<RemoteClient> remoteClients = Collections.emptyList();
|
||||
private List<RemoteTab> clientChildren;
|
||||
private int remoteClientIndexOfParent = -1;
|
||||
private Cursor historyCursor;
|
||||
|
||||
// Maintain group collapsed and hidden state. Only accessed from the UI thread.
|
||||
protected static RemoteTabsExpandableListState sState;
|
||||
|
||||
// List of hidden remote clients.
|
||||
// Only accessed from the UI thread.
|
||||
protected final List<RemoteClient> hiddenClients = new ArrayList<>();
|
||||
private DevicesUpdateHandler devicesUpdateHandler;
|
||||
private int deviceCount = 0;
|
||||
|
||||
// We use a sparse array to store each section header's position in the panel [more cheaply than a HashMap].
|
||||
private final SparseArray<CombinedHistoryPanel.SectionHeader> sectionHeaders;
|
||||
private final SparseArray<SectionHeader> sectionHeaders;
|
||||
|
||||
private final Context context;
|
||||
|
||||
public CombinedHistoryAdapter(Context context, int savedParentIndex) {
|
||||
public CombinedHistoryAdapter(Resources resources) {
|
||||
super();
|
||||
this.context = context;
|
||||
sectionHeaders = new SparseArray<>();
|
||||
|
||||
// This races when multiple Fragments are created. That's okay: one
|
||||
// will win, and thereafter, all will be okay. If we create and then
|
||||
// drop an instance the shared SharedPreferences backing all the
|
||||
// instances will maintain the state for us. Since everything happens on
|
||||
// the UI thread, this doesn't even need to be volatile.
|
||||
if (sState == null) {
|
||||
sState = new RemoteTabsExpandableListState(GeckoSharedPrefs.forProfile(context));
|
||||
}
|
||||
remoteClientIndexOfParent = savedParentIndex;
|
||||
}
|
||||
|
||||
public void setClients(List<RemoteClient> clients) {
|
||||
hiddenClients.clear();
|
||||
remoteClients.clear();
|
||||
|
||||
final Iterator<RemoteClient> it = clients.iterator();
|
||||
while (it.hasNext()) {
|
||||
final RemoteClient client = it.next();
|
||||
if (sState.isClientHidden(client.guid)) {
|
||||
hiddenClients.add(client);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
remoteClients = clients;
|
||||
|
||||
// Add item for unhiding clients.
|
||||
if (!hiddenClients.isEmpty()) {
|
||||
remoteClients.add(null);
|
||||
}
|
||||
|
||||
notifyItemRangeChanged(0, remoteClients.size());
|
||||
HistorySectionsHelper.updateRecentSectionOffset(resources, sectionDateRangeArray);
|
||||
}
|
||||
|
||||
public void setHistory(Cursor history) {
|
||||
@ -107,102 +55,70 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void removeItem(int position) {
|
||||
final ItemType itemType = getItemTypeForPosition(position);
|
||||
switch (itemType) {
|
||||
case CLIENT:
|
||||
final boolean hadHiddenClients = !hiddenClients.isEmpty();
|
||||
final RemoteClient client = remoteClients.remove(transformAdapterPositionForDataStructure(ItemType.CLIENT, position));
|
||||
notifyItemRemoved(position);
|
||||
public interface DevicesUpdateHandler {
|
||||
void onDeviceCountUpdated(int count);
|
||||
}
|
||||
|
||||
sState.setClientHidden(client.guid, true);
|
||||
hiddenClients.add(client);
|
||||
if (!hadHiddenClients) {
|
||||
// Add item for unhiding clients;
|
||||
remoteClients.add(null);
|
||||
} else {
|
||||
// Update "hidden clients" item because number of hidden clients changed.
|
||||
notifyItemChanged(remoteClients.size() - 1);
|
||||
public DevicesUpdateHandler getDeviceUpdateHandler() {
|
||||
if (devicesUpdateHandler == null) {
|
||||
devicesUpdateHandler = new DevicesUpdateHandler() {
|
||||
@Override
|
||||
public void onDeviceCountUpdated(int count) {
|
||||
deviceCount = count;
|
||||
notifyItemChanged(0);
|
||||
}
|
||||
break;
|
||||
};
|
||||
}
|
||||
return devicesUpdateHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedHistoryItem onCreateViewHolder(ViewGroup viewGroup, int viewType) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
|
||||
final View view;
|
||||
|
||||
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
|
||||
|
||||
switch (itemType) {
|
||||
case SYNCED_DEVICES:
|
||||
view = inflater.inflate(R.layout.home_smartfolder, viewGroup, false);
|
||||
return new CombinedHistoryItem.SmartFolder(view);
|
||||
|
||||
case SECTION_HEADER:
|
||||
view = inflater.inflate(R.layout.home_header_row, viewGroup, false);
|
||||
return new CombinedHistoryItem.BasicItem(view);
|
||||
|
||||
case HISTORY:
|
||||
notifyItemRemoved(position);
|
||||
view = inflater.inflate(R.layout.home_item_row, viewGroup, false);
|
||||
return new CombinedHistoryItem.HistoryItem(view);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unexpected Home Panel item type");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(CombinedHistoryItem viewHolder, int position) {
|
||||
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
|
||||
final int localPosition = transformAdapterPositionForDataStructure(itemType, position);
|
||||
|
||||
switch (itemType) {
|
||||
case SYNCED_DEVICES:
|
||||
((CombinedHistoryItem.SmartFolder) viewHolder).bind(R.drawable.cloud, R.string.home_synced_devices_smartfolder, R.string.home_synced_devices_number, deviceCount);
|
||||
break;
|
||||
|
||||
case SECTION_HEADER:
|
||||
((TextView) viewHolder.itemView).setText(getSectionHeaderTitle(sectionHeaders.get(localPosition)));
|
||||
break;
|
||||
|
||||
case HISTORY:
|
||||
if (historyCursor == null || !historyCursor.moveToPosition(localPosition)) {
|
||||
throw new IllegalStateException("Couldn't move cursor to position " + localPosition);
|
||||
}
|
||||
((CombinedHistoryItem.HistoryItem) viewHolder).bind(historyCursor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void unhideClients(List<RemoteClient> selectedClients) {
|
||||
if (selectedClients.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (RemoteClient client : selectedClients) {
|
||||
sState.setClientHidden(client.guid, false);
|
||||
hiddenClients.remove(client);
|
||||
}
|
||||
|
||||
final int insertIndex = remoteClients.size() - 1;
|
||||
|
||||
remoteClients.addAll(insertIndex, selectedClients);
|
||||
notifyItemRangeInserted(insertIndex, selectedClients.size());
|
||||
|
||||
if (hiddenClients.isEmpty()) {
|
||||
// No more hidden clients, remove "unhide" item.
|
||||
remoteClients.remove(remoteClients.size() - 1);
|
||||
} else {
|
||||
// Update "hidden clients" item because number of hidden clients changed.
|
||||
notifyItemChanged(remoteClients.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
public List<RemoteClient> getHiddenClients() {
|
||||
return hiddenClients;
|
||||
}
|
||||
|
||||
public JSONArray getCurrentChildTabs() {
|
||||
if (clientChildren != null) {
|
||||
final JSONArray urls = new JSONArray();
|
||||
for (int i = 1; i < clientChildren.size(); i++) {
|
||||
urls.put(clientChildren.get(i).url);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getParentIndex() {
|
||||
return remoteClientIndexOfParent;
|
||||
}
|
||||
|
||||
private boolean isInChildView() {
|
||||
return remoteClientIndexOfParent != -1;
|
||||
}
|
||||
|
||||
public void showChildView(int parentPosition) {
|
||||
if (clientChildren == null) {
|
||||
clientChildren = new ArrayList<>();
|
||||
}
|
||||
// Handle "back" view.
|
||||
clientChildren.add(null);
|
||||
remoteClientIndexOfParent = transformAdapterPositionForDataStructure(ItemType.CLIENT, parentPosition);
|
||||
clientChildren.addAll(remoteClients.get(remoteClientIndexOfParent).tabs);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public boolean exitChildView() {
|
||||
if (!isInChildView()) {
|
||||
return false;
|
||||
}
|
||||
remoteClientIndexOfParent = -1;
|
||||
clientChildren.clear();
|
||||
notifyDataSetChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
private ItemType getItemTypeForPosition(int position) {
|
||||
return ItemType.viewTypeToItemType(getItemViewType(position));
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an adapter position to the position for the data structure backing the item type.
|
||||
*
|
||||
@ -213,108 +129,36 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
|
||||
* @param position position in the adapter
|
||||
* @return position of the item in the data structure
|
||||
*/
|
||||
private int transformAdapterPositionForDataStructure(ItemType type, int position) {
|
||||
if (type == ItemType.CLIENT) {
|
||||
private int transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType type, int position) {
|
||||
if (type == CombinedHistoryItem.ItemType.SECTION_HEADER) {
|
||||
return position;
|
||||
} else if (type == ItemType.SECTION_HEADER) {
|
||||
return position - remoteClients.size();
|
||||
} else if (type == ItemType.HISTORY) {
|
||||
return position - remoteClients.size() - getHeadersBefore(position);
|
||||
} else if (type == CombinedHistoryItem.ItemType.HISTORY){
|
||||
return position - getHeadersBefore(position) - CombinedHistoryPanel.NUM_SMART_FOLDERS;
|
||||
} else {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
|
||||
final ItemType itemType = getItemTypeForPosition(position);
|
||||
HomeContextMenuInfo info;
|
||||
switch (itemType) {
|
||||
case CHILD:
|
||||
info = new HomeContextMenuInfo(view, position, -1);
|
||||
return CombinedHistoryPanel.populateChildInfoFromTab(info, clientChildren.get(position));
|
||||
case HISTORY:
|
||||
info = new HomeContextMenuInfo(view, position, -1);
|
||||
historyCursor.moveToPosition(transformAdapterPositionForDataStructure(ItemType.HISTORY, position));
|
||||
return CombinedHistoryPanel.populateHistoryInfoFromCursor(info, historyCursor);
|
||||
case CLIENT:
|
||||
final int clientPosition = transformAdapterPositionForDataStructure(ItemType.CLIENT, position);
|
||||
info = new CombinedHistoryPanel.RemoteTabsClientContextMenuInfo(view, position, -1, remoteClients.get(clientPosition));
|
||||
return info;
|
||||
private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
|
||||
if (position == SYNCED_DEVICES_SMARTFOLDER_INDEX) {
|
||||
return CombinedHistoryItem.ItemType.SYNCED_DEVICES;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedHistoryItem onCreateViewHolder(ViewGroup viewGroup, int viewType) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
|
||||
final View view;
|
||||
|
||||
final ItemType itemType = ItemType.viewTypeToItemType(viewType);
|
||||
|
||||
switch (itemType) {
|
||||
case CLIENT:
|
||||
view = inflater.inflate(R.layout.home_remote_tabs_group, viewGroup, false);
|
||||
return new CombinedHistoryItem.ClientItem(view);
|
||||
|
||||
case HIDDEN_DEVICES:
|
||||
view = inflater.inflate(R.layout.home_remote_tabs_hidden_devices, viewGroup, false);
|
||||
return new CombinedHistoryItem.BasicItem(view);
|
||||
|
||||
case NAVIGATION_BACK:
|
||||
view = inflater.inflate(R.layout.home_combined_back_item, viewGroup, false);
|
||||
return new CombinedHistoryItem.HistoryItem(view);
|
||||
|
||||
case SECTION_HEADER:
|
||||
view = inflater.inflate(R.layout.home_header_row, viewGroup, false);
|
||||
return new CombinedHistoryItem.BasicItem(view);
|
||||
|
||||
case CHILD:
|
||||
case HISTORY:
|
||||
view = inflater.inflate(R.layout.home_item_row, viewGroup, false);
|
||||
return new CombinedHistoryItem.HistoryItem(view);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unexpected Home Panel item type");
|
||||
final int sectionPosition = transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType.SECTION_HEADER, position);
|
||||
if (sectionHeaders.get(sectionPosition) != null) {
|
||||
return CombinedHistoryItem.ItemType.SECTION_HEADER;
|
||||
}
|
||||
return CombinedHistoryItem.ItemType.HISTORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (isInChildView()) {
|
||||
if (position == 0) {
|
||||
return ItemType.itemTypeToViewType(ItemType.NAVIGATION_BACK);
|
||||
}
|
||||
return ItemType.itemTypeToViewType(ItemType.CHILD);
|
||||
} else {
|
||||
final int numClients = remoteClients.size();
|
||||
if (position < numClients) {
|
||||
if (!hiddenClients.isEmpty() && position == numClients - 1) {
|
||||
return ItemType.itemTypeToViewType(ItemType.HIDDEN_DEVICES);
|
||||
}
|
||||
return ItemType.itemTypeToViewType(ItemType.CLIENT);
|
||||
}
|
||||
|
||||
final int sectionPosition = transformAdapterPositionForDataStructure(ItemType.SECTION_HEADER, position);
|
||||
if (sectionHeaders.get(sectionPosition) != null) {
|
||||
return ItemType.itemTypeToViewType(ItemType.SECTION_HEADER);
|
||||
}
|
||||
|
||||
return ItemType.itemTypeToViewType(ItemType.HISTORY);
|
||||
}
|
||||
return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (isInChildView()) {
|
||||
if (clientChildren == null) {
|
||||
clientChildren = new ArrayList<>();
|
||||
clientChildren.add(null);
|
||||
clientChildren.addAll(remoteClients.get(remoteClientIndexOfParent).tabs);
|
||||
}
|
||||
return clientChildren.size();
|
||||
} else {
|
||||
final int historySize = historyCursor == null ? 0 : historyCursor.getCount();
|
||||
return remoteClients.size() + historySize + sectionHeaders.size();
|
||||
}
|
||||
final int historySize = historyCursor == null ? 0 : historyCursor.getCount();
|
||||
return historySize + sectionHeaders.size() + CombinedHistoryPanel.NUM_SMART_FOLDERS;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -335,11 +179,11 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
|
||||
do {
|
||||
final int historyPosition = c.getPosition();
|
||||
final long visitTime = c.getLong(c.getColumnIndexOrThrow(BrowserContract.History.DATE_LAST_VISITED));
|
||||
final SectionHeader itemSection = CombinedHistoryPanel.getSectionFromTime(visitTime);
|
||||
final SectionHeader itemSection = getSectionFromTime(visitTime);
|
||||
|
||||
if (section != itemSection) {
|
||||
section = itemSection;
|
||||
sparseArray.append(historyPosition + sparseArray.size(), section);
|
||||
sparseArray.append(historyPosition + sparseArray.size() + CombinedHistoryPanel.NUM_SMART_FOLDERS, section);
|
||||
}
|
||||
|
||||
if (section == SectionHeader.OLDER_THAN_SIX_MONTHS) {
|
||||
@ -348,48 +192,18 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
|
||||
} while (c.moveToNext());
|
||||
}
|
||||
|
||||
|
||||
public boolean containsHistory() {
|
||||
if (historyCursor == null) {
|
||||
return false;
|
||||
}
|
||||
return (historyCursor.getCount() > 0);
|
||||
private static String getSectionHeaderTitle(SectionHeader section) {
|
||||
return sectionDateRangeArray[section.ordinal()].displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(CombinedHistoryItem viewHolder, int position) {
|
||||
final ItemType itemType = getItemTypeForPosition(position);
|
||||
final int localPosition = transformAdapterPositionForDataStructure(itemType, position);
|
||||
|
||||
switch (itemType) {
|
||||
case CLIENT:
|
||||
final CombinedHistoryItem.ClientItem clientItem = (CombinedHistoryItem.ClientItem) viewHolder;
|
||||
final RemoteClient client = remoteClients.get(localPosition);
|
||||
clientItem.bind(client, context);
|
||||
break;
|
||||
|
||||
case HIDDEN_DEVICES:
|
||||
final String hiddenDevicesLabel = context.getResources().getString(R.string.home_remote_tabs_many_hidden_devices, hiddenClients.size());
|
||||
((TextView) viewHolder.itemView).setText(hiddenDevicesLabel);
|
||||
break;
|
||||
|
||||
case CHILD:
|
||||
RemoteTab remoteTab = clientChildren.get(position);
|
||||
((CombinedHistoryItem.HistoryItem) viewHolder).bind(remoteTab);
|
||||
break;
|
||||
|
||||
case SECTION_HEADER:
|
||||
((TextView) viewHolder.itemView).setText(CombinedHistoryPanel.getSectionHeaderTitle(sectionHeaders.get(localPosition)));
|
||||
break;
|
||||
|
||||
|
||||
case HISTORY:
|
||||
if (historyCursor == null || !historyCursor.moveToPosition(localPosition)) {
|
||||
throw new IllegalStateException("Couldn't move cursor to position " + localPosition);
|
||||
}
|
||||
((CombinedHistoryItem.HistoryItem) viewHolder).bind(historyCursor);
|
||||
break;
|
||||
private static SectionHeader getSectionFromTime(long time) {
|
||||
for (int i = 0; i < SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal(); i++) {
|
||||
if (time > sectionDateRangeArray[i].start) {
|
||||
return SectionHeader.values()[i];
|
||||
}
|
||||
}
|
||||
|
||||
return SectionHeader.OLDER_THAN_SIX_MONTHS;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -397,15 +211,42 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
|
||||
* @param position position in the adapter
|
||||
*/
|
||||
private int getHeadersBefore(int position) {
|
||||
final int adjustedPosition = position - remoteClients.size();
|
||||
// Skip the first header case because there will always be a header.
|
||||
for (int i = 1; i < sectionHeaders.size(); i++) {
|
||||
// If the position of the header is greater than the history position,
|
||||
// return the number of headers tested.
|
||||
if (sectionHeaders.keyAt(i) > adjustedPosition) {
|
||||
if (sectionHeaders.keyAt(i) > position) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return sectionHeaders.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
|
||||
if (position == 0) {
|
||||
// No context menu for smartfolders.
|
||||
return null;
|
||||
}
|
||||
HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, -1);
|
||||
historyCursor.moveToPosition(transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType.HISTORY, position));
|
||||
return populateHistoryInfoFromCursor(info, historyCursor);
|
||||
}
|
||||
|
||||
protected static HomeContextMenuInfo populateHistoryInfoFromCursor(HomeContextMenuInfo info, Cursor cursor) {
|
||||
info.url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
|
||||
info.title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
|
||||
info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
|
||||
info.itemType = HomeContextMenuInfo.RemoveItemType.HISTORY;
|
||||
final int bookmarkIdCol = cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID);
|
||||
if (cursor.isNull(bookmarkIdCol)) {
|
||||
// If this is a combined cursor, we may get a history item without a
|
||||
// bookmark, in which case the bookmarks ID column value will be null.
|
||||
info.bookmarkId = -1;
|
||||
} else {
|
||||
info.bookmarkId = cursor.getInt(bookmarkIdCol);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,25 +8,64 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.RemoteTabsExpandableListAdapter;
|
||||
import org.mozilla.gecko.db.RemoteClient;
|
||||
import org.mozilla.gecko.db.RemoteTab;
|
||||
|
||||
public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
|
||||
private static final String LOGTAG = "CombinedHistoryItem";
|
||||
|
||||
public CombinedHistoryItem(View view) {
|
||||
super(view);
|
||||
}
|
||||
|
||||
public enum ItemType {
|
||||
CLIENT, HIDDEN_DEVICES, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD, SYNCED_DEVICES;
|
||||
|
||||
public static ItemType viewTypeToItemType(int viewType) {
|
||||
if (viewType >= ItemType.values().length) {
|
||||
Log.e(LOGTAG, "No corresponding ItemType!");
|
||||
}
|
||||
return ItemType.values()[viewType];
|
||||
}
|
||||
|
||||
public static int itemTypeToViewType(ItemType itemType) {
|
||||
return itemType.ordinal();
|
||||
}
|
||||
}
|
||||
|
||||
public static class BasicItem extends CombinedHistoryItem {
|
||||
public BasicItem(View view) {
|
||||
super(view);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SmartFolder extends CombinedHistoryItem {
|
||||
final Context context;
|
||||
final ImageView icon;
|
||||
final TextView title;
|
||||
final TextView subtext;
|
||||
|
||||
public SmartFolder(View view) {
|
||||
super(view);
|
||||
context = view.getContext();
|
||||
|
||||
icon = (ImageView) view.findViewById(R.id.device_type);
|
||||
title = (TextView) view.findViewById(R.id.title);
|
||||
subtext = (TextView) view.findViewById(R.id.subtext);
|
||||
}
|
||||
|
||||
public void bind(int drawableRes, int titleRes, int subtitleRes, int numDevices) {
|
||||
icon.setImageResource(drawableRes);
|
||||
title.setText(titleRes);
|
||||
subtext.setText(context.getString(subtitleRes, numDevices));
|
||||
}
|
||||
}
|
||||
|
||||
public static class HistoryItem extends CombinedHistoryItem {
|
||||
public HistoryItem(View view) {
|
||||
super(view);
|
||||
@ -49,21 +88,31 @@ public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
|
||||
final TextView nameView;
|
||||
final ImageView deviceTypeView;
|
||||
final TextView lastModifiedView;
|
||||
final ImageView deviceExpanded;
|
||||
|
||||
public ClientItem(View view) {
|
||||
super(view);
|
||||
nameView = (TextView) view.findViewById(R.id.client);
|
||||
deviceTypeView = (ImageView) view.findViewById(R.id.device_type);
|
||||
lastModifiedView = (TextView) view.findViewById(R.id.last_synced);
|
||||
deviceExpanded = (ImageView) view.findViewById(R.id.device_expanded);
|
||||
}
|
||||
|
||||
public void bind(RemoteClient client, Context context) {
|
||||
this.nameView.setText(client.name);
|
||||
this.nameView.setTextColor(ContextCompat.getColor(context, R.color.placeholder_active_grey));
|
||||
this.deviceTypeView.setImageResource("desktop".equals(client.deviceType) ? R.drawable.sync_desktop : R.drawable.sync_mobile);
|
||||
public void bind(Context context, RemoteClient client, boolean isCollapsed) {
|
||||
this.nameView.setText(client.name);
|
||||
final long now = System.currentTimeMillis();
|
||||
this.lastModifiedView.setText(ClientsAdapter.getLastSyncedString(context, now, client.lastModified));
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
this.lastModifiedView.setText(RemoteTabsExpandableListAdapter.getLastSyncedString(context, now, client.lastModified));
|
||||
if (client.isDesktop()) {
|
||||
deviceTypeView.setImageResource(isCollapsed ? R.drawable.sync_desktop_inactive : R.drawable.sync_desktop);
|
||||
} else {
|
||||
deviceTypeView.setImageResource(isCollapsed ? R.drawable.sync_mobile_inactive : R.drawable.sync_mobile);
|
||||
}
|
||||
|
||||
nameView.setTextColor(ContextCompat.getColor(context, isCollapsed ? R.color.tabs_tray_icon_grey : R.color.placeholder_active_grey));
|
||||
if (client.tabs.size() > 0) {
|
||||
deviceExpanded.setImageResource(isCollapsed ? R.drawable.home_group_collapsed : R.drawable.arrow_down);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,16 +5,19 @@
|
||||
|
||||
package org.mozilla.gecko.home;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.DefaultItemAnimator;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextPaint;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
@ -27,26 +30,24 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewStub;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONArray;
|
||||
import org.mozilla.gecko.EventDispatcher;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.GeckoProfile;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.RemoteClientsDialogFragment;
|
||||
import org.mozilla.gecko.fxa.FirefoxAccounts;
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
import org.mozilla.gecko.fxa.SyncStatusListener;
|
||||
import org.mozilla.gecko.restrictions.Restrictions;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.db.RemoteClient;
|
||||
import org.mozilla.gecko.db.RemoteTab;
|
||||
import org.mozilla.gecko.home.HistorySectionsHelper.SectionDateRange;
|
||||
import org.mozilla.gecko.restrictions.Restrictable;
|
||||
import org.mozilla.gecko.widget.DividerItemDecoration;
|
||||
|
||||
@ -56,46 +57,58 @@ import java.util.List;
|
||||
|
||||
public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsDialogFragment.RemoteClientsListener {
|
||||
private static final String LOGTAG = "GeckoCombinedHistoryPnl";
|
||||
|
||||
private static final String[] STAGES_TO_SYNC_ON_REFRESH = new String[] { "clients", "tabs" };
|
||||
private final int LOADER_ID_HISTORY = 0;
|
||||
private final int LOADER_ID_REMOTE = 1;
|
||||
|
||||
// Semantic names for the time covered by each section
|
||||
public enum SectionHeader {
|
||||
TODAY,
|
||||
YESTERDAY,
|
||||
WEEK,
|
||||
THIS_MONTH,
|
||||
MONTH_AGO,
|
||||
TWO_MONTHS_AGO,
|
||||
THREE_MONTHS_AGO,
|
||||
FOUR_MONTHS_AGO,
|
||||
FIVE_MONTHS_AGO,
|
||||
OLDER_THAN_SIX_MONTHS
|
||||
}
|
||||
|
||||
// Array for the time ranges in milliseconds covered by each section.
|
||||
private static final SectionDateRange[] sectionDateRangeArray = new SectionDateRange[SectionHeader.values().length];
|
||||
|
||||
// String placeholders to mark formatting.
|
||||
private final static String FORMAT_S1 = "%1$s";
|
||||
private final static String FORMAT_S2 = "%2$s";
|
||||
|
||||
private CombinedHistoryRecyclerView mRecyclerView;
|
||||
private CombinedHistoryAdapter mAdapter;
|
||||
private CursorLoaderCallbacks mCursorLoaderCallbacks;
|
||||
private int mSavedParentIndex = -1;
|
||||
// Number of smart folders for determining practical empty state.
|
||||
public static final int NUM_SMART_FOLDERS = 1;
|
||||
|
||||
private OnPanelLevelChangeListener.PanelLevel mPanelLevel = OnPanelLevelChangeListener.PanelLevel.PARENT;
|
||||
private CombinedHistoryRecyclerView mRecyclerView;
|
||||
private CombinedHistoryAdapter mHistoryAdapter;
|
||||
private ClientsAdapter mClientsAdapter;
|
||||
private CursorLoaderCallbacks mCursorLoaderCallbacks;
|
||||
|
||||
private OnPanelLevelChangeListener.PanelLevel mPanelLevel;
|
||||
private Button mPanelFooterButton;
|
||||
|
||||
// Child refresh layout view.
|
||||
protected SwipeRefreshLayout mRefreshLayout;
|
||||
|
||||
// Sync listener that stops refreshing when a sync is completed.
|
||||
protected RemoteTabsSyncListener mSyncStatusListener;
|
||||
|
||||
// Reference to the View to display when there are no results.
|
||||
private View mEmptyView;
|
||||
private View mHistoryEmptyView;
|
||||
private View mClientsEmptyView;
|
||||
|
||||
public interface OnPanelLevelChangeListener {
|
||||
enum PanelLevel {
|
||||
PARENT, CHILD
|
||||
}
|
||||
}
|
||||
|
||||
void onPanelLevelChange(PanelLevel level);
|
||||
/**
|
||||
* Propagates level changes.
|
||||
* @param level
|
||||
* @return true if level changed, false otherwise.
|
||||
*/
|
||||
boolean changeLevel(PanelLevel level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstance) {
|
||||
super.onCreate(savedInstance);
|
||||
|
||||
mHistoryAdapter = new CombinedHistoryAdapter(getResources());
|
||||
mClientsAdapter = new ClientsAdapter(getContext());
|
||||
|
||||
mSyncStatusListener = new RemoteTabsSyncListener();
|
||||
FirefoxAccounts.addSyncStatusListener(mSyncStatusListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -108,17 +121,76 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
mRecyclerView = (CombinedHistoryRecyclerView) view.findViewById(R.id.combined_recycler_view);
|
||||
mAdapter = new CombinedHistoryAdapter(getContext(), mSavedParentIndex);
|
||||
mRecyclerView.setAdapter(mAdapter);
|
||||
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
|
||||
setUpRecyclerView();
|
||||
|
||||
mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
|
||||
setUpRefreshLayout();
|
||||
|
||||
mClientsEmptyView = view.findViewById(R.id.home_clients_empty_view);
|
||||
mHistoryEmptyView = view.findViewById(R.id.home_history_empty_view);
|
||||
setUpEmptyViews();
|
||||
|
||||
mPanelFooterButton = (Button) view.findViewById(R.id.clear_history_button);
|
||||
mPanelFooterButton.setOnClickListener(new OnFooterButtonClickListener());
|
||||
}
|
||||
|
||||
private void setUpRecyclerView() {
|
||||
if (mPanelLevel == null) {
|
||||
mPanelLevel = OnPanelLevelChangeListener.PanelLevel.PARENT;
|
||||
}
|
||||
|
||||
mRecyclerView.setAdapter(mPanelLevel == OnPanelLevelChangeListener.PanelLevel.PARENT ? mHistoryAdapter : mClientsAdapter);
|
||||
|
||||
final RecyclerView.ItemAnimator animator = new DefaultItemAnimator();
|
||||
animator.setAddDuration(100);
|
||||
animator.setChangeDuration(100);
|
||||
animator.setMoveDuration(100);
|
||||
animator.setRemoveDuration(100);
|
||||
mRecyclerView.setItemAnimator(animator);
|
||||
mRecyclerView.addItemDecoration(new DividerItemDecoration(getContext()));
|
||||
mRecyclerView.setOnHistoryClickedListener(mUrlOpenListener);
|
||||
mRecyclerView.setOnPanelLevelChangeListener(new OnLevelChangeListener());
|
||||
mRecyclerView.setHiddenClientsDialogBuilder(new HiddenClientsHelper());
|
||||
registerForContextMenu(mRecyclerView);
|
||||
}
|
||||
|
||||
mPanelFooterButton = (Button) view.findViewById(R.id.clear_history_button);
|
||||
mPanelFooterButton.setOnClickListener(new OnFooterButtonClickListener());
|
||||
private void setUpRefreshLayout() {
|
||||
mRefreshLayout.setColorSchemeResources(R.color.fennec_ui_orange, R.color.action_orange);
|
||||
mRefreshLayout.setOnRefreshListener(new RemoteTabsRefreshListener());
|
||||
}
|
||||
|
||||
private void setUpEmptyViews() {
|
||||
// Set up history empty view.
|
||||
final ImageView emptyIcon = (ImageView) mHistoryEmptyView.findViewById(R.id.home_empty_image);
|
||||
emptyIcon.setVisibility(View.GONE);
|
||||
|
||||
final TextView emptyText = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_text);
|
||||
emptyText.setText(R.string.home_most_recent_empty);
|
||||
|
||||
final TextView emptyHint = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_hint);
|
||||
|
||||
if (!Restrictions.isAllowed(getActivity(), Restrictable.PRIVATE_BROWSING)) {
|
||||
emptyHint.setVisibility(View.GONE);
|
||||
} else {
|
||||
final String hintText = getResources().getString(R.string.home_most_recent_emptyhint);
|
||||
final SpannableStringBuilder hintBuilder = formatHintText(hintText);
|
||||
if (hintBuilder != null) {
|
||||
emptyHint.setText(hintBuilder);
|
||||
emptyHint.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
emptyHint.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up Clients empty view.
|
||||
final Button syncSetupButton = (Button) mClientsEmptyView.findViewById(R.id.sync_setup_button);
|
||||
syncSetupButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
// This Activity will redirect to the correct Activity as needed.
|
||||
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -127,17 +199,6 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
|
||||
mCursorLoaderCallbacks = new CursorLoaderCallbacks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if (isVisible()) {
|
||||
// The parent stack is saved just so that the folder state can be
|
||||
// restored on rotation.
|
||||
mSavedParentIndex = mAdapter.getParentIndex();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load() {
|
||||
getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks);
|
||||
@ -171,25 +232,10 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
|
||||
@Override
|
||||
public Cursor loadCursor() {
|
||||
final ContentResolver cr = getContext().getContentResolver();
|
||||
HistorySectionsHelper.updateRecentSectionOffset(getContext().getResources(), sectionDateRangeArray);
|
||||
return mDB.getRecentHistory(cr, HISTORY_LIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
protected static String getSectionHeaderTitle(SectionHeader section) {
|
||||
return sectionDateRangeArray[section.ordinal()].displayName;
|
||||
}
|
||||
|
||||
protected static SectionHeader getSectionFromTime(long time) {
|
||||
for (int i = 0; i < SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal(); i++) {
|
||||
if (time > sectionDateRangeArray[i].start) {
|
||||
return SectionHeader.values()[i];
|
||||
}
|
||||
}
|
||||
|
||||
return SectionHeader.OLDER_THAN_SIX_MONTHS;
|
||||
}
|
||||
|
||||
private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
private BrowserDB mDB; // Pseudo-final: set in onCreateLoader.
|
||||
|
||||
@ -215,140 +261,122 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
|
||||
final int loaderId = loader.getId();
|
||||
switch (loaderId) {
|
||||
case LOADER_ID_HISTORY:
|
||||
mAdapter.setHistory(c);
|
||||
mHistoryAdapter.setHistory(c);
|
||||
break;
|
||||
|
||||
case LOADER_ID_REMOTE:
|
||||
final List<RemoteClient> clients = mDB.getTabsAccessor().getClientsFromCursor(c);
|
||||
|
||||
mAdapter.setClients(clients);
|
||||
mHistoryAdapter.getDeviceUpdateHandler().onDeviceCountUpdated(clients.size());
|
||||
mClientsAdapter.setClients(clients);
|
||||
mRefreshLayout.setEnabled(clients.size() > 0);
|
||||
break;
|
||||
}
|
||||
|
||||
// Check and set empty state.
|
||||
updateButtonFromLevel(mPanelLevel);
|
||||
updateEmptyView(mAdapter.getItemCount() == 0);
|
||||
updateEmptyView();
|
||||
updateButtonFromLevel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
mAdapter.setClients(Collections.<RemoteClient>emptyList());
|
||||
mAdapter.setHistory(null);
|
||||
mClientsAdapter.setClients(Collections.<RemoteClient>emptyList());
|
||||
mHistoryAdapter.setHistory(null);
|
||||
}
|
||||
}
|
||||
|
||||
protected class OnLevelChangeListener implements OnPanelLevelChangeListener {
|
||||
@Override
|
||||
public void onPanelLevelChange(PanelLevel level) {
|
||||
updateButtonFromLevel(level);
|
||||
public boolean changeLevel(PanelLevel level) {
|
||||
if (level == mPanelLevel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mPanelLevel = level;
|
||||
switch (level) {
|
||||
case PARENT:
|
||||
mRecyclerView.swapAdapter(mHistoryAdapter, true);
|
||||
break;
|
||||
case CHILD:
|
||||
mRecyclerView.swapAdapter(mClientsAdapter, true);
|
||||
break;
|
||||
}
|
||||
|
||||
updateEmptyView();
|
||||
updateButtonFromLevel();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateButtonFromLevel(OnPanelLevelChangeListener.PanelLevel level) {
|
||||
mPanelLevel = level;
|
||||
switch (level) {
|
||||
case CHILD:
|
||||
mPanelFooterButton.setVisibility(View.VISIBLE);
|
||||
mPanelFooterButton.setText(R.string.home_open_all);
|
||||
break;
|
||||
private void updateButtonFromLevel() {
|
||||
switch (mPanelLevel) {
|
||||
case PARENT:
|
||||
final boolean historyRestricted = !Restrictions.isAllowed(getActivity(), Restrictable.CLEAR_HISTORY);
|
||||
if (historyRestricted || !mAdapter.containsHistory()) {
|
||||
if (historyRestricted || mHistoryAdapter.getItemCount() == NUM_SMART_FOLDERS) {
|
||||
mPanelFooterButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
mPanelFooterButton.setVisibility(View.VISIBLE);
|
||||
mPanelFooterButton.setText(R.string.home_clear_history_button);
|
||||
}
|
||||
break;
|
||||
case CHILD:
|
||||
mPanelFooterButton.setVisibility(View.GONE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private class OnFooterButtonClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
switch (mPanelLevel) {
|
||||
case PARENT:
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
|
||||
dialogBuilder.setMessage(R.string.home_clear_history_confirm);
|
||||
dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int which) {
|
||||
dialog.dismiss();
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
|
||||
dialogBuilder.setMessage(R.string.home_clear_history_confirm);
|
||||
dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
// Send message to Java to clear history.
|
||||
final JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("history", true);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "JSON error", e);
|
||||
}
|
||||
dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int which) {
|
||||
dialog.dismiss();
|
||||
|
||||
GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
|
||||
}
|
||||
});
|
||||
|
||||
dialogBuilder.show();
|
||||
break;
|
||||
|
||||
case CHILD:
|
||||
final JSONArray tabUrls = mAdapter.getCurrentChildTabs();
|
||||
if (tabUrls != null) {
|
||||
final JSONObject message = new JSONObject();
|
||||
try {
|
||||
message.put("urls", tabUrls);
|
||||
message.put("shouldNotifyTabsOpenedToJava", false);
|
||||
GeckoAppShell.notifyObservers("Tabs:OpenMultiple", message.toString());
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error making JSON message to open tabs");
|
||||
}
|
||||
// Send message to Java to clear history.
|
||||
final JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("history", true);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "JSON error", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
|
||||
}
|
||||
});
|
||||
|
||||
dialogBuilder.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateEmptyView(boolean isEmpty) {
|
||||
if (isEmpty) {
|
||||
if (mEmptyView == null) {
|
||||
// Set empty panel view if it needs to be shown and hasn't been inflated.
|
||||
final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
|
||||
mEmptyView = emptyViewStub.inflate();
|
||||
private void updateEmptyView() {
|
||||
boolean showEmptyHistoryView = false;
|
||||
boolean showEmptyClientsView = false;
|
||||
switch (mPanelLevel) {
|
||||
case PARENT:
|
||||
showEmptyHistoryView = mHistoryAdapter.getItemCount() == NUM_SMART_FOLDERS;
|
||||
break;
|
||||
|
||||
final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image);
|
||||
emptyIcon.setImageResource(R.drawable.icon_most_recent_empty);
|
||||
|
||||
final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text);
|
||||
emptyText.setText(R.string.home_most_recent_empty);
|
||||
|
||||
final TextView emptyHint = (TextView) mEmptyView.findViewById(R.id.home_empty_hint);
|
||||
final String hintText = getResources().getString(R.string.home_most_recent_emptyhint);
|
||||
|
||||
final SpannableStringBuilder hintBuilder = formatHintText(hintText);
|
||||
if (hintBuilder != null) {
|
||||
emptyHint.setText(hintBuilder);
|
||||
emptyHint.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
emptyHint.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (!Restrictions.isAllowed(getActivity(), Restrictable.PRIVATE_BROWSING)) {
|
||||
emptyHint.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
mEmptyView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
if (mEmptyView != null) {
|
||||
mEmptyView.setVisibility(View.GONE);
|
||||
}
|
||||
case CHILD:
|
||||
showEmptyClientsView = mClientsAdapter.getItemCount() == 1;
|
||||
break;
|
||||
}
|
||||
|
||||
final boolean showEmptyView = showEmptyClientsView || showEmptyHistoryView;
|
||||
mRecyclerView.setOverScrollMode(showEmptyView? View.OVER_SCROLL_NEVER : View.OVER_SCROLL_IF_CONTENT_SCROLLS);
|
||||
|
||||
mClientsEmptyView.setVisibility(showEmptyClientsView ? View.VISIBLE : View.GONE);
|
||||
mHistoryEmptyView.setVisibility(showEmptyHistoryView ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make Span that is clickable, and underlined
|
||||
* between the string markers <code>FORMAT_S1</code> and
|
||||
@ -439,7 +467,7 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
|
||||
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == R.id.home_remote_tabs_hide_client) {
|
||||
mAdapter.removeItem(info.position);
|
||||
mClientsAdapter.removeItem(info.position);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -453,20 +481,18 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
|
||||
protected class HiddenClientsHelper implements DialogBuilder<RemoteClient> {
|
||||
@Override
|
||||
public void createAndShowDialog(List<RemoteClient> clientsList) {
|
||||
final RemoteClientsDialogFragment dialog = RemoteClientsDialogFragment.newInstance(
|
||||
final RemoteClientsDialogFragment dialog = RemoteClientsDialogFragment.newInstance(
|
||||
getResources().getString(R.string.home_remote_tabs_hidden_devices_title),
|
||||
getResources().getString(R.string.home_remote_tabs_unhide_selected_devices),
|
||||
RemoteClientsDialogFragment.ChoiceMode.MULTIPLE, new ArrayList<>(clientsList));
|
||||
dialog.setTargetFragment(CombinedHistoryPanel.this, 0);
|
||||
dialog.show(getActivity().getSupportFragmentManager(), "show-clients");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClients(List<RemoteClient> clients) {
|
||||
mAdapter.unhideClients(clients);
|
||||
mClientsAdapter.unhideClients(clients);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -481,25 +507,46 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
|
||||
}
|
||||
}
|
||||
|
||||
protected static HomeContextMenuInfo populateHistoryInfoFromCursor(HomeContextMenuInfo info, Cursor cursor) {
|
||||
info.url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
|
||||
info.title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
|
||||
info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
|
||||
info.itemType = HomeContextMenuInfo.RemoveItemType.HISTORY;
|
||||
final int bookmarkIdCol = cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID);
|
||||
if (cursor.isNull(bookmarkIdCol)) {
|
||||
// If this is a combined cursor, we may get a history item without a
|
||||
// bookmark, in which case the bookmarks ID column value will be null.
|
||||
info.bookmarkId = -1;
|
||||
} else {
|
||||
info.bookmarkId = cursor.getInt(bookmarkIdCol);
|
||||
protected class RemoteTabsRefreshListener implements SwipeRefreshLayout.OnRefreshListener {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
if (FirefoxAccounts.firefoxAccountsExist(getActivity())) {
|
||||
final Account account = FirefoxAccounts.getFirefoxAccount(getActivity());
|
||||
FirefoxAccounts.requestImmediateSync(account, STAGES_TO_SYNC_ON_REFRESH, null);
|
||||
} else {
|
||||
Log.wtf(LOGTAG, "No Firefox Account found; this should never happen. Ignoring.");
|
||||
mRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, RemoteTab tab) {
|
||||
info.url = tab.url;
|
||||
info.title = tab.title;
|
||||
return info;
|
||||
protected class RemoteTabsSyncListener implements SyncStatusListener {
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account getAccount() {
|
||||
return FirefoxAccounts.getFirefoxAccount(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSyncStarted() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSyncFinished() {
|
||||
mRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (mSyncStatusListener != null) {
|
||||
FirefoxAccounts.removeSyncStatusListener(mSyncStatusListener);
|
||||
mSyncStatusListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,17 +13,22 @@ import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import org.mozilla.gecko.db.RemoteClient;
|
||||
import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener;
|
||||
import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.widget.RecyclerViewClickSupport;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD;
|
||||
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.PARENT;
|
||||
|
||||
public class CombinedHistoryRecyclerView extends RecyclerView
|
||||
implements RecyclerViewClickSupport.OnItemClickListener, RecyclerViewClickSupport.OnItemLongClickListener {
|
||||
public static String LOGTAG = "CombinedHistoryRecycView";
|
||||
|
||||
protected interface AdapterContextMenuBuilder {
|
||||
HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position);
|
||||
}
|
||||
|
||||
protected HomePager.OnUrlOpenListener mOnUrlOpenListener;
|
||||
protected OnPanelLevelChangeListener mOnPanelLevelChangeListener;
|
||||
@ -61,8 +66,7 @@ public class CombinedHistoryRecyclerView extends RecyclerView
|
||||
|
||||
// If the user hit the BACK key, try to move to the parent folder.
|
||||
if (action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
mOnPanelLevelChangeListener.onPanelLevelChange(PARENT);
|
||||
return ((CombinedHistoryAdapter) getAdapter()).exitChildView();
|
||||
return mOnPanelLevelChangeListener.changeLevel(PARENT);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -84,23 +88,27 @@ public class CombinedHistoryRecyclerView extends RecyclerView
|
||||
@Override
|
||||
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
|
||||
final int viewType = getAdapter().getItemViewType(position);
|
||||
final CombinedHistoryAdapter.ItemType itemType = CombinedHistoryAdapter.ItemType.viewTypeToItemType(viewType);
|
||||
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
|
||||
|
||||
switch (itemType) {
|
||||
case CLIENT:
|
||||
mOnPanelLevelChangeListener.onPanelLevelChange(PanelLevel.CHILD);
|
||||
((CombinedHistoryAdapter) getAdapter()).showChildView(position);
|
||||
case SYNCED_DEVICES:
|
||||
mOnPanelLevelChangeListener.changeLevel(CHILD);
|
||||
break;
|
||||
|
||||
case CLIENT:
|
||||
((ClientsAdapter) getAdapter()).toggleClient(position);
|
||||
break;
|
||||
|
||||
case HIDDEN_DEVICES:
|
||||
if (mDialogBuilder != null) {
|
||||
mDialogBuilder.createAndShowDialog(((CombinedHistoryAdapter) getAdapter()).getHiddenClients());
|
||||
mDialogBuilder.createAndShowDialog(((ClientsAdapter) getAdapter()).getHiddenClients());
|
||||
}
|
||||
break;
|
||||
|
||||
case NAVIGATION_BACK:
|
||||
mOnPanelLevelChangeListener.onPanelLevelChange(PARENT);
|
||||
((CombinedHistoryAdapter) getAdapter()).exitChildView();
|
||||
mOnPanelLevelChangeListener.changeLevel(PARENT);
|
||||
break;
|
||||
|
||||
case CHILD:
|
||||
case HISTORY:
|
||||
if (mOnUrlOpenListener != null) {
|
||||
@ -114,7 +122,7 @@ public class CombinedHistoryRecyclerView extends RecyclerView
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {
|
||||
mContextMenuInfo = ((CombinedHistoryAdapter) getAdapter()).makeContextMenuInfoFromPosition(v, position);
|
||||
mContextMenuInfo = ((AdapterContextMenuBuilder) getAdapter()).makeContextMenuInfoFromPosition(v, position);
|
||||
return showContextMenuForChild(this);
|
||||
}
|
||||
|
||||
|
@ -7,12 +7,13 @@ package org.mozilla.gecko.home;
|
||||
|
||||
import android.content.res.Resources;
|
||||
|
||||
import org.mozilla.gecko.home.CombinedHistoryPanel.SectionHeader;
|
||||
import org.mozilla.gecko.home.CombinedHistoryAdapter.SectionHeader;
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
public class HistorySectionsHelper {
|
||||
|
||||
// Constants for different time sections.
|
||||
|
@ -44,7 +44,6 @@ public class TwoLinePageRow extends LinearLayout
|
||||
private int mSwitchToTabIconId;
|
||||
|
||||
private final FaviconView mFavicon;
|
||||
private final View mReaderCached;
|
||||
|
||||
private boolean mShowIcons;
|
||||
private int mLoadFaviconJobId = Favicons.NOT_LOADING;
|
||||
@ -109,8 +108,6 @@ public class TwoLinePageRow extends LinearLayout
|
||||
|
||||
mFavicon = (FaviconView) findViewById(R.id.icon);
|
||||
mFaviconListener = new UpdateViewFaviconLoadedListener(mFavicon);
|
||||
|
||||
mReaderCached = findViewById(R.id.is_reader_cached);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -201,9 +198,23 @@ public class TwoLinePageRow extends LinearLayout
|
||||
mUrl.setCompoundDrawablesWithIntrinsicBounds(mSwitchToTabIconId, 0, 0, 0);
|
||||
}
|
||||
|
||||
private void showBookmarkIcon(boolean toShow) {
|
||||
final int visibility = toShow ? VISIBLE : GONE;
|
||||
mStatusIcon.setVisibility(visibility);
|
||||
private void updateStatusIcon(boolean isBookmark, boolean isReaderItem) {
|
||||
if (isReaderItem) {
|
||||
mStatusIcon.setImageResource(R.drawable.status_icon_readercache);
|
||||
} else if (isBookmark) {
|
||||
mStatusIcon.setImageResource(R.drawable.star_blue);
|
||||
}
|
||||
|
||||
if (mShowIcons && (isBookmark || isReaderItem)) {
|
||||
mStatusIcon.setVisibility(View.VISIBLE);
|
||||
} else if (mShowIcons) {
|
||||
// We use INVISIBLE to have consistent padding for our items. This means text/URLs
|
||||
// fade consistently in the same location, regardless of them being bookmarked.
|
||||
mStatusIcon.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
mStatusIcon.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -261,9 +272,10 @@ public class TwoLinePageRow extends LinearLayout
|
||||
// The bookmark id will be 0 (null in database) when the url
|
||||
// is not a bookmark.
|
||||
final boolean isBookmark = bookmarkId != 0;
|
||||
showBookmarkIcon(isBookmark);
|
||||
|
||||
updateStatusIcon(isBookmark, hasReaderCacheItem);
|
||||
} else {
|
||||
showBookmarkIcon(false);
|
||||
updateStatusIcon(false, false);
|
||||
}
|
||||
|
||||
// Use the URL instead of an empty title for consistency with the normal URL
|
||||
@ -286,8 +298,6 @@ public class TwoLinePageRow extends LinearLayout
|
||||
mLoadFaviconJobId = Favicons.getSizedFaviconForPageFromLocal(getContext(), pageURL, mFaviconListener);
|
||||
|
||||
updateDisplayedUrl(url, hasReaderCacheItem);
|
||||
|
||||
mReaderCached.setVisibility(hasReaderCacheItem ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -130,10 +130,7 @@ public class GeckoMenuInflater extends MenuInflater {
|
||||
item.enabled = a.getBoolean(R.styleable.MenuItem_android_enabled, true);
|
||||
item.hasSubMenu = false;
|
||||
item.iconRes = a.getResourceId(R.styleable.MenuItem_android_icon, 0);
|
||||
|
||||
if (Versions.feature11Plus) {
|
||||
item.showAsAction = a.getInt(R.styleable.MenuItem_android_showAsAction, 0);
|
||||
}
|
||||
item.showAsAction = a.getInt(R.styleable.MenuItem_android_showAsAction, 0);
|
||||
|
||||
a.recycle();
|
||||
}
|
||||
|
@ -17,8 +17,7 @@ import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
|
||||
public class GeckoMenuItem implements MenuItem {
|
||||
private static final int SECONDARY_ACTION_BAR_HISTORY_SIZE = 0;
|
||||
private static final int QUICK_SHARE_ACTION_BAR_HISTORY_SIZE = 3;
|
||||
private static final int SHARE_BAR_HISTORY_SIZE = 2;
|
||||
|
||||
// These values mirror MenuItem values that are only available on API >= 11.
|
||||
public static final int SHOW_AS_ACTION_NEVER = 0;
|
||||
@ -135,13 +134,8 @@ public class GeckoMenuItem implements MenuItem {
|
||||
@Override
|
||||
public View getActionView() {
|
||||
if (mActionProvider != null) {
|
||||
if (getActionEnum() == MenuItem.SHOW_AS_ACTION_IF_ROOM) {
|
||||
return mActionProvider.onCreateActionView(SECONDARY_ACTION_BAR_HISTORY_SIZE,
|
||||
GeckoActionProvider.ActionViewType.DEFAULT);
|
||||
} else {
|
||||
return mActionProvider.onCreateActionView(QUICK_SHARE_ACTION_BAR_HISTORY_SIZE,
|
||||
GeckoActionProvider.ActionViewType.QUICK_SHARE_ICON);
|
||||
}
|
||||
return mActionProvider.onCreateActionView(SHARE_BAR_HISTORY_SIZE,
|
||||
GeckoActionProvider.ActionViewType.DEFAULT);
|
||||
}
|
||||
|
||||
return mActionView;
|
||||
|
@ -8,7 +8,6 @@ package org.mozilla.gecko.menu;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.mozilla.gecko.AppConstants.Versions;
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
@ -156,9 +155,9 @@ public class MenuItemSwitcherLayout extends LinearLayout
|
||||
params.weight = 1.0f;
|
||||
button.setLayoutParams(params);
|
||||
|
||||
// Fill in the action-buttons to the left of the actual menu button.
|
||||
// Place action buttons to the right of the actual menu item
|
||||
mActionButtons.add(button);
|
||||
addView(button, count);
|
||||
addView(button, count + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,36 +0,0 @@
|
||||
/* -*- 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.menu;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
/**
|
||||
* A MenuItemActionView without the default child views.
|
||||
*
|
||||
* A better implementation would have the non-child view implementation as the parent of
|
||||
* MenuItemActionView, but this is simpler and faster to implement for something intended to be
|
||||
* uplifted, and this implementation will soon be replaced with the old implementation for adding
|
||||
* the share plane to the menu (see https://bug1122302.bugzilla.mozilla.org/attachment.cgi?id=8572126).
|
||||
*/
|
||||
public class QuickShareBarActionView extends MenuItemSwitcherLayout {
|
||||
|
||||
public QuickShareBarActionView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.menuItemSwitcherLayoutStyle);
|
||||
}
|
||||
|
||||
@TargetApi(14)
|
||||
public QuickShareBarActionView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
// We remove the views so they are visible, but note that
|
||||
// the child still does some computation on them.
|
||||
removeAllViews();
|
||||
}
|
||||
}
|
@ -0,0 +1,239 @@
|
||||
/* -*- 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.promotion;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.AboutPages;
|
||||
import org.mozilla.gecko.BrowserApp;
|
||||
import org.mozilla.gecko.BrowserAppDelegate;
|
||||
import org.mozilla.gecko.EditBookmarkDialog;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.SnackbarHelper;
|
||||
import org.mozilla.gecko.Tab;
|
||||
import org.mozilla.gecko.Tabs;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.home.HomeConfig;
|
||||
import org.mozilla.gecko.prompts.Prompt;
|
||||
import org.mozilla.gecko.prompts.PromptListItem;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* Delegate to watch for bookmark state changes.
|
||||
*
|
||||
* This is responsible for showing snackbars and helper UIs related to the addition/removal
|
||||
* of bookmarks, or reader view bookmarks.
|
||||
*/
|
||||
public class BookmarkStateChangeDelegate extends BrowserAppDelegate implements Tabs.OnTabsChangedListener {
|
||||
private static final String LOGTAG = "BookmarkDelegate";
|
||||
|
||||
private WeakReference<BrowserApp> mBrowserApp;
|
||||
|
||||
@Override
|
||||
public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
|
||||
mBrowserApp = new WeakReference<>(browserApp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume(BrowserApp browserApp) {
|
||||
Tabs.registerOnTabsChangedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause(BrowserApp browserApp) {
|
||||
Tabs.unregisterOnTabsChangedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
|
||||
switch (msg) {
|
||||
case BOOKMARK_ADDED:
|
||||
// We always show the special offline snackbar whenever we bookmark a reader page.
|
||||
// It's possible that the page is already stored offline, however this is highly
|
||||
// unlikely, and even so it is probably nicer to show the same offline notification
|
||||
// every time we bookmark an about:reader page.
|
||||
if (!AboutPages.isAboutReader(tab.getURL())) {
|
||||
showBookmarkAddedSnackbar();
|
||||
} else {
|
||||
if (!promoteReaderViewBookmarkAdded()) {
|
||||
showReaderModeBookmarkAddedSnackbar();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case BOOKMARK_REMOVED:
|
||||
showBookmarkRemovedSnackbar();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == BrowserApp.ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK) {
|
||||
if (resultCode == BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS) {
|
||||
browserApp.openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS));
|
||||
} else if (resultCode == BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE) {
|
||||
showReaderModeBookmarkAddedSnackbar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean promoteReaderViewBookmarkAdded() {
|
||||
final BrowserApp browserApp = mBrowserApp.get();
|
||||
if (browserApp == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(browserApp);
|
||||
|
||||
final boolean hasFirstReaderViewPromptBeenShownBefore = prefs.getBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, false);
|
||||
|
||||
if (hasFirstReaderViewPromptBeenShownBefore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SimpleHelperUI.show(browserApp,
|
||||
SimpleHelperUI.FIRST_RVBP_SHOWN_TELEMETRYEXTRA,
|
||||
BrowserApp.ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK,
|
||||
R.string.helper_first_offline_bookmark_title, R.string.helper_first_offline_bookmark_message,
|
||||
R.drawable.helper_readerview_bookmark, R.string.helper_first_offline_bookmark_button,
|
||||
BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS,
|
||||
BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE);
|
||||
|
||||
GeckoSharedPrefs.forProfile(browserApp)
|
||||
.edit()
|
||||
.putBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, true)
|
||||
.apply();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showBookmarkAddedSnackbar() {
|
||||
final BrowserApp browserApp = mBrowserApp.get();
|
||||
if (browserApp == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This flow is from the option menu which has check to see if a bookmark was already added.
|
||||
// So, it is safe here to show the snackbar that bookmark_added without any checks.
|
||||
final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "bookmark_options");
|
||||
showBookmarkDialog(browserApp);
|
||||
}
|
||||
};
|
||||
|
||||
SnackbarHelper.showSnackbarWithAction(browserApp,
|
||||
browserApp.getResources().getString(R.string.bookmark_added),
|
||||
Snackbar.LENGTH_LONG,
|
||||
browserApp.getResources().getString(R.string.bookmark_options),
|
||||
callback);
|
||||
}
|
||||
|
||||
private void showBookmarkRemovedSnackbar() {
|
||||
final BrowserApp browserApp = mBrowserApp.get();
|
||||
if (browserApp == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
SnackbarHelper.showSnackbar(browserApp, browserApp.getResources().getString(R.string.bookmark_removed), Snackbar.LENGTH_LONG);
|
||||
}
|
||||
|
||||
private static void showBookmarkDialog(final BrowserApp browserApp) {
|
||||
final Resources res = browserApp.getResources();
|
||||
final Tab tab = Tabs.getInstance().getSelectedTab();
|
||||
|
||||
final Prompt ps = new Prompt(browserApp, new Prompt.PromptCallback() {
|
||||
@Override
|
||||
public void onPromptFinished(String result) {
|
||||
int itemId = -1;
|
||||
try {
|
||||
itemId = new JSONObject(result).getInt("button");
|
||||
} catch (JSONException ex) {
|
||||
Log.e(LOGTAG, "Exception reading bookmark prompt result", ex);
|
||||
}
|
||||
|
||||
if (tab == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemId == 0) {
|
||||
final String extrasId = res.getResourceEntryName(R.string.contextmenu_edit_bookmark);
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
|
||||
TelemetryContract.Method.DIALOG, extrasId);
|
||||
|
||||
new EditBookmarkDialog(browserApp).show(tab.getURL());
|
||||
} else if (itemId == 1) {
|
||||
final String extrasId = res.getResourceEntryName(R.string.contextmenu_add_to_launcher);
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
|
||||
TelemetryContract.Method.DIALOG, extrasId);
|
||||
|
||||
final String url = tab.getURL();
|
||||
final String title = tab.getDisplayTitle();
|
||||
|
||||
if (url != null && title != null) {
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GeckoAppShell.createShortcut(title, url);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final PromptListItem[] items = new PromptListItem[2];
|
||||
items[0] = new PromptListItem(res.getString(R.string.contextmenu_edit_bookmark));
|
||||
items[1] = new PromptListItem(res.getString(R.string.contextmenu_add_to_launcher));
|
||||
|
||||
ps.show("", "", items, ListView.CHOICE_MODE_NONE);
|
||||
}
|
||||
|
||||
private void showReaderModeBookmarkAddedSnackbar() {
|
||||
final BrowserApp browserApp = mBrowserApp.get();
|
||||
if (browserApp == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Drawable iconDownloaded = DrawableUtil.tintDrawable(browserApp, R.drawable.status_icon_readercache, Color.WHITE);
|
||||
|
||||
final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
browserApp.openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS));
|
||||
}
|
||||
};
|
||||
|
||||
SnackbarHelper.showSnackbarWithActionAndColors(browserApp,
|
||||
browserApp.getResources().getString(R.string.reader_saved_offline),
|
||||
Snackbar.LENGTH_LONG,
|
||||
browserApp.getResources().getString(R.string.reader_switch_to_bookmarks),
|
||||
callback,
|
||||
iconDownloaded,
|
||||
ContextCompat.getColor(browserApp, R.color.link_blue),
|
||||
Color.WHITE);
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/* -*- 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.promotion;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.keepsafe.switchboard.SwitchBoard;
|
||||
|
||||
import org.mozilla.gecko.BrowserApp;
|
||||
import org.mozilla.gecko.BrowserAppDelegate;
|
||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Tab;
|
||||
import org.mozilla.gecko.Tabs;
|
||||
import org.mozilla.gecko.reader.ReaderModeUtils;
|
||||
import org.mozilla.gecko.util.Experiments;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class ReaderViewBookmarkPromotion extends BrowserAppDelegate implements Tabs.OnTabsChangedListener {
|
||||
private WeakReference<BrowserApp> mBrowserApp;
|
||||
|
||||
private int mTimesEnteredReaderMode;
|
||||
private boolean mExperimentEnabled;
|
||||
|
||||
@Override
|
||||
public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
|
||||
mBrowserApp = new WeakReference<>(browserApp);
|
||||
|
||||
mExperimentEnabled = SwitchBoard.isInExperiment(browserApp, Experiments.TRIPLE_READERVIEW_BOOKMARK_PROMPT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume(BrowserApp browserApp) {
|
||||
Tabs.registerOnTabsChangedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause(BrowserApp browserApp) {
|
||||
Tabs.unregisterOnTabsChangedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
|
||||
switch (msg) {
|
||||
case LOCATION_CHANGE:
|
||||
// old url: data
|
||||
// new url: tab.getURL()
|
||||
final boolean enteringReaderMode = ReaderModeUtils.isEnteringReaderMode(tab.getURL(), data);
|
||||
|
||||
if (mTimesEnteredReaderMode < 4 && enteringReaderMode) {
|
||||
mTimesEnteredReaderMode++;
|
||||
}
|
||||
|
||||
if (mTimesEnteredReaderMode == 3) {
|
||||
promoteBookmarking();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case BrowserApp.ACTIVITY_REQUEST_TRIPLE_READERVIEW:
|
||||
if (resultCode == BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK) {
|
||||
final Tab tab = Tabs.getInstance().getSelectedTab();
|
||||
if (tab != null) {
|
||||
tab.addBookmark();
|
||||
}
|
||||
} else if (resultCode == BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE) {
|
||||
// Nothing to do: we won't show this promotion again either way.
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void promoteBookmarking() {
|
||||
final BrowserApp browserApp = mBrowserApp.get();
|
||||
if (browserApp == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(browserApp);
|
||||
|
||||
// We reuse the same preference as for the first offline reader view bookmark
|
||||
// as we only want to show one of the two UIs (they both explain the same
|
||||
// functionality).
|
||||
if (!mExperimentEnabled || prefs.getBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SimpleHelperUI.show(browserApp,
|
||||
SimpleHelperUI.TRIPLE_READERVIEW_OPEN_TELEMETRYEXTRA,
|
||||
BrowserApp.ACTIVITY_REQUEST_TRIPLE_READERVIEW,
|
||||
R.string.helper_triple_readerview_open_title,
|
||||
R.string.helper_triple_readerview_open_message,
|
||||
R.drawable.helper_readerview_bookmark, // We share the icon with the usual helper UI
|
||||
R.string.helper_triple_readerview_open_button,
|
||||
BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK,
|
||||
BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE);
|
||||
|
||||
GeckoSharedPrefs.forProfile(browserApp)
|
||||
.edit()
|
||||
.putBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, true)
|
||||
.apply();
|
||||
}
|
||||
|
||||
}
|
@ -33,6 +33,7 @@ import org.mozilla.gecko.TelemetryContract;
|
||||
public class SimpleHelperUI extends Locales.LocaleAwareActivity {
|
||||
public static final String PREF_FIRST_RVBP_SHOWN = "first_reader_view_bookmark_prompt_shown";
|
||||
public static final String FIRST_RVBP_SHOWN_TELEMETRYEXTRA = "first_readerview_bookmark_prompt";
|
||||
public static final String TRIPLE_READERVIEW_OPEN_TELEMETRYEXTRA = "third_readerview_open_prompt";
|
||||
|
||||
private View containerView;
|
||||
|
||||
|
@ -35,7 +35,7 @@ import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TimePicker;
|
||||
|
||||
public class PromptInput {
|
||||
public abstract class PromptInput {
|
||||
protected final String mLabel;
|
||||
protected final String mType;
|
||||
protected final String mId;
|
||||
@ -47,7 +47,7 @@ public class PromptInput {
|
||||
public static final String LOGTAG = "GeckoPromptInput";
|
||||
|
||||
public interface OnChangeListener {
|
||||
public void onChange(PromptInput input);
|
||||
void onChange(PromptInput input);
|
||||
}
|
||||
|
||||
public void setListener(OnChangeListener listener) {
|
||||
@ -372,9 +372,7 @@ public class PromptInput {
|
||||
return null;
|
||||
}
|
||||
|
||||
public View getView(Context context) throws UnsupportedOperationException {
|
||||
return null;
|
||||
}
|
||||
public abstract View getView(Context context) throws UnsupportedOperationException;
|
||||
|
||||
public String getId() {
|
||||
return mId;
|
||||
|
@ -7,6 +7,7 @@
|
||||
package org.mozilla.gecko.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
public class ContextUtils {
|
||||
@ -17,11 +18,11 @@ public class ContextUtils {
|
||||
* @throws PackageManager.NameNotFoundException Unexpected - we get the package name from the context so
|
||||
* it's expected to be found.
|
||||
*/
|
||||
public static long getPackageInstallTime(final Context context) {
|
||||
public static PackageInfo getCurrentPackageInfo(final Context context) {
|
||||
try {
|
||||
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).firstInstallTime;
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
throw new AssertionError("Should not happen: could not get package info for own package");
|
||||
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new AssertionError("Should not happen: Can't get package info of own package");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,9 @@ public class Experiments {
|
||||
|
||||
public static final String PREF_ONBOARDING_VERSION = "onboarding_version";
|
||||
|
||||
// Promotion to bookmark reader-view items after entering reader view three times (Bug 1247689)
|
||||
public static final String TRIPLE_READERVIEW_BOOKMARK_PROMPT = "triple-readerview-bookmark-prompt";
|
||||
|
||||
private static volatile Boolean disabled = null;
|
||||
|
||||
/**
|
||||
|
@ -33,7 +33,8 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration {
|
||||
}
|
||||
for (int i = 0; i < parent.getChildCount(); i++) {
|
||||
final View child = parent.getChildAt(i);
|
||||
c.drawRect(0, child.getBottom(), parent.getWidth(), child.getBottom() + mDividerHeight, mDividerPaint);
|
||||
final float bottom = child.getBottom() + child.getTranslationY();
|
||||
c.drawRect(0, bottom, parent.getWidth(), bottom + mDividerHeight, mDividerPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.SnackbarHelper;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.menu.QuickShareBarActionView;
|
||||
import org.mozilla.gecko.mozglue.ContextUtils;
|
||||
import org.mozilla.gecko.overlays.ui.ShareDialog;
|
||||
import org.mozilla.gecko.menu.MenuItemSwitcherLayout;
|
||||
@ -119,10 +118,6 @@ public class GeckoActionProvider {
|
||||
view = new MenuItemSwitcherLayout(mContext, null);
|
||||
break;
|
||||
|
||||
case QUICK_SHARE_ICON:
|
||||
view = new QuickShareBarActionView(mContext, null);
|
||||
break;
|
||||
|
||||
case CONTEXT_MENU:
|
||||
view = new MenuItemSwitcherLayout(mContext, null);
|
||||
view.initContextMenuStyles();
|
||||
@ -283,7 +278,6 @@ public class GeckoActionProvider {
|
||||
|
||||
public enum ActionViewType {
|
||||
DEFAULT,
|
||||
QUICK_SHARE_ICON,
|
||||
CONTEXT_MENU,
|
||||
}
|
||||
|
||||
|
@ -536,7 +536,9 @@ size. -->
|
||||
with the page title defined in aboutHome.dtd -->
|
||||
<!ENTITY home_title "&brandShortName; Home">
|
||||
<!ENTITY home_history_title "History">
|
||||
<!ENTITY home_history_back_to "Go back to all history">
|
||||
<!ENTITY home_synced_devices_smartfolder "Synced devices">
|
||||
<!ENTITY home_synced_devices_number "&formatD; devices">
|
||||
<!ENTITY home_history_back_to2 "Back to full History">
|
||||
<!ENTITY home_clear_history_button "Clear browsing history">
|
||||
<!ENTITY home_clear_history_confirm "Are you sure you want to clear your history?">
|
||||
<!ENTITY home_bookmarks_empty "Bookmarks you save show up here.">
|
||||
@ -778,3 +780,8 @@ just addresses the organization to follow, e.g. "This site is run by " -->
|
||||
<!ENTITY helper_first_offline_bookmark_title "Read offline">
|
||||
<!ENTITY helper_first_offline_bookmark_message "Find your Reader View items in Bookmarks, even offline.">
|
||||
<!ENTITY helper_first_offline_bookmark_button "Go to Bookmarks">
|
||||
|
||||
<!ENTITY helper_triple_readerview_open_title "Available offline">
|
||||
<!ENTITY helper_triple_readerview_open_message "Bookmark Reader View items to read them offline.">
|
||||
<!ENTITY helper_triple_readerview_open_button "Add to Bookmarks">
|
||||
|
||||
|
@ -385,6 +385,7 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
|
||||
'home/BookmarksListView.java',
|
||||
'home/BookmarksPanel.java',
|
||||
'home/BrowserSearch.java',
|
||||
'home/ClientsAdapter.java',
|
||||
'home/CombinedHistoryAdapter.java',
|
||||
'home/CombinedHistoryItem.java',
|
||||
'home/CombinedHistoryPanel.java',
|
||||
@ -456,7 +457,6 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
|
||||
'menu/MenuItemSwitcherLayout.java',
|
||||
'menu/MenuPanel.java',
|
||||
'menu/MenuPopup.java',
|
||||
'menu/QuickShareBarActionView.java',
|
||||
'MotionEventInterceptor.java',
|
||||
'NotificationClient.java',
|
||||
'NotificationHandler.java',
|
||||
@ -508,7 +508,9 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
|
||||
'PrintHelper.java',
|
||||
'PrivateTab.java',
|
||||
'promotion/AddToHomeScreenPromotion.java',
|
||||
'promotion/BookmarkStateChangeDelegate.java',
|
||||
'promotion/HomeScreenPrompt.java',
|
||||
'promotion/ReaderViewBookmarkPromotion.java',
|
||||
'promotion/SimpleHelperUI.java',
|
||||
'prompts/ColorPickerInput.java',
|
||||
'prompts/IconGridInput.java',
|
||||
@ -524,7 +526,6 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
|
||||
'reader/ReadingListHelper.java',
|
||||
'reader/SavedReaderViewHelper.java',
|
||||
'RemoteClientsDialogFragment.java',
|
||||
'RemoteTabsExpandableListAdapter.java',
|
||||
'Restarter.java',
|
||||
'restrictions/DefaultConfiguration.java',
|
||||
'restrictions/GuestProfileConfiguration.java',
|
||||
|
Before Width: | Height: | Size: 888 B After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 754 B After Width: | Height: | Size: 779 B |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
BIN
mobile/android/base/resources/drawable-nodpi/cloud.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1000 B After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.4 KiB |