Bug 771826: implement Social toolbar button UI, including notification icons, notification panel, and "profile" panel. Styling only for Windows/Mac for the moment, r=gavin

--HG--
extra : rebase_source : 62924e7f08b182d4ac9bd19521794d0e8eb5ebd9
This commit is contained in:
Shane Caraveo 2012-07-15 16:12:13 -07:00
parent 062c9d4909
commit ea8c7a94ec
16 changed files with 592 additions and 8 deletions

View File

@ -1184,4 +1184,4 @@ pref("pdfjs.previousHandler.alwaysAskBeforeHandling", false);
pref("image.mem.max_decoded_image_kb", 256000);
// Example social provider
pref("social.manifest.motown", "{\"origin\":\"https://motown-dev.mozillalabs.com\",\"name\":\"MoTown\",\"workerURL\":\"https://motown-dev.mozillalabs.com/social/worker.js\"}");
pref("social.manifest.motown", "{\"origin\":\"https://motown-dev.mozillalabs.com\",\"name\":\"MoTown\",\"workerURL\":\"https://motown-dev.mozillalabs.com/social/worker.js\",\"iconURL\":\"https://motown-dev.mozillalabs.com/images/motown-icon.png\"}");

View File

@ -6,21 +6,42 @@ let SocialUI = {
// Called on delayed startup to initialize UI
init: function SocialUI_init() {
Services.obs.addObserver(this, "social:pref-changed", false);
Services.obs.addObserver(this, "social:ambient-notification-changed", false);
Services.obs.addObserver(this, "social:profile-changed", false);
Social.init(this._providerReady.bind(this));
},
// Called on window unload
uninit: function SocialUI_uninit() {
Services.obs.removeObserver(this, "social:pref-changed");
Services.obs.removeObserver(this, "social:ambient-notification-changed");
Services.obs.removeObserver(this, "social:profile-changed");
},
// Called when the social.enabled pref is changed
observe: function SocialUI_observe(aSubject, aTopic, aData) {
SocialShareButton.updateButtonEnabledState();
showProfile: function SocialUI_showProfile() {
if (Social.provider)
openUILink(Social.provider.profile.profileURL);
},
observe: function SocialUI_observe(subject, topic, data) {
switch (topic) {
case "social:pref-changed":
SocialShareButton.updateButtonHiddenState();
SocialToolbar.updateButtonHiddenState();
break;
case "social:ambient-notification-changed":
SocialToolbar.updateButton();
break;
case "social:profile-changed":
SocialToolbar.updateProfile();
break;
}
},
// Called once Social.jsm's provider has been set
_providerReady: function SocialUI_providerReady() {
SocialToolbar.init();
SocialShareButton.init();
}
}
@ -28,7 +49,7 @@ let SocialUI = {
let SocialShareButton = {
init: function SSB_init() {
this.sharePopup.hidden = false;
this.updateButtonEnabledState();
this.updateButtonHiddenState();
},
get shareButton() {
@ -42,11 +63,10 @@ let SocialShareButton = {
this.sharePopup.hidePopup();
},
updateButtonEnabledState: function SSB_updateButtonEnabledState() {
updateButtonHiddenState: function SSB_updateButtonHiddenState() {
let shareButton = this.shareButton;
if (shareButton)
shareButton.hidden = !Social.provider || !Social.provider.enabled ||
!Social.provider.port;
shareButton.hidden = !Social.uiVisible;
},
onClick: function SSB_onClick(aEvent) {
@ -106,3 +126,119 @@ let SocialShareButton = {
}
}
};
var SocialToolbar = {
// Called once, after window load, when the Social.provider object is initialized
init: function SocialToolbar_init() {
document.getElementById("social-provider-image").setAttribute("image", Social.provider.iconURL);
// handle button state
document.getElementById("social-statusarea-popup").addEventListener("popupshowing", function(e) {
document.getElementById("social-toolbar-button").setAttribute("open", "true");
}, false);
document.getElementById("social-statusarea-popup").addEventListener("popuphiding", function(e) {
document.getElementById("social-toolbar-button").removeAttribute("open");
}, false);
this.updateButton();
this.updateProfile();
},
updateButtonHiddenState: function SocialToolbar_updateButtonHiddenState() {
let toolbarbutton = document.getElementById("social-toolbar-button");
toolbarbutton.hidden = !Social.uiVisible;
},
updateProfile: function SocialToolbar_updateProfile() {
// Profile may not have been initialized yet, since it depends on a worker
// response. In that case we'll be called again when it's available, via
// social:profile-changed
let profile = Social.provider.profile || {};
let userPortrait = profile.portrait || "chrome://browser/skin/social/social.png";
document.getElementById("social-statusarea-user-portrait").setAttribute("src", userPortrait);
let notLoggedInLabel = document.getElementById("social-statusarea-notloggedin");
let userNameBtn = document.getElementById("social-statusarea-username");
if (profile.userName) {
notLoggedInLabel.hidden = true;
userNameBtn.hidden = false;
userNameBtn.label = profile.userName;
} else {
notLoggedInLabel.hidden = false;
userNameBtn.hidden = true;
}
},
updateButton: function SocialToolbar_updateButton() {
this.updateButtonHiddenState();
let provider = Social.provider;
// if there are no ambient icons, we collapse them in the following loop
let iconNames = Object.keys(provider.ambientNotificationIcons);
let iconBox = document.getElementById("social-status-iconbox");
for (var i = 0; i < iconBox.childNodes.length; i++) {
let iconContainer = iconBox.childNodes[i];
if (i > iconNames.length - 1) {
iconContainer.collapsed = true;
continue;
}
iconContainer.collapsed = false;
let icon = provider.ambientNotificationIcons[iconNames[i]];
let iconImage = iconContainer.firstChild;
let iconCounter = iconImage.nextSibling;
iconImage.setAttribute("contentPanel", icon.contentPanel);
iconImage.setAttribute("src", icon.iconURL);
if (iconCounter.firstChild)
iconCounter.removeChild(iconCounter.firstChild);
if (icon.counter) {
iconCounter.appendChild(document.createTextNode(icon.counter));
iconCounter.collapsed = false;
} else {
iconCounter.collapsed = true;
}
}
},
showAmbientPopup: function SocialToolbar_showAmbientPopup(iconContainer) {
let iconImage = iconContainer.firstChild;
let panel = document.getElementById("social-notification-panel");
let notifBrowser = document.getElementById("social-notification-browser");
panel.hidden = false;
function sizePanelToContent() {
// XXX Maybe we can use nsIDOMWindowUtils.getRootBounds() here?
// XXX need to handle dynamic sizing
let doc = notifBrowser.contentDocument;
// XXX "notif" is an implementation detail that we should get rid of
// eventually
let body = doc.getElementById("notif") || doc.body.firstChild;
if (!body)
return;
let h = body.scrollHeight > 0 ? body.scrollHeight : 300;
notifBrowser.style.width = body.scrollWidth + "px";
notifBrowser.style.height = h + "px";
}
notifBrowser.addEventListener("DOMContentLoaded", function onload() {
notifBrowser.removeEventListener("DOMContentLoaded", onload);
sizePanelToContent();
});
panel.addEventListener("popuphiding", function onpopuphiding() {
panel.removeEventListener("popuphiding", onpopuphiding);
// unload the panel
document.getElementById("social-toolbar-button").removeAttribute("open");
notifBrowser.setAttribute("src", "about:blank");
});
notifBrowser.service = Social.provider;
notifBrowser.setAttribute("src", iconImage.getAttribute("contentPanel"));
document.getElementById("social-toolbar-button").setAttribute("open", "true");
panel.openPopup(iconImage, "bottomcenter topleft", 0, 0, false, false);
}
}

View File

@ -221,6 +221,10 @@
</hbox>
</panel>
<panel id="social-notification-panel" type="arrow" hidden="true" noautofocus="true">
<browser id="social-notification-browser" type="content" flex="1"/>
</panel>
<menupopup id="inspector-node-popup">
<menuitem id="inspectorHTMLCopyInner"
label="&inspectorHTMLCopyInner.label;"
@ -604,6 +608,45 @@
onclick="BrowserGoHome(event);"
aboutHomeOverrideTooltip="&abouthome.pageTitle;"/>
<toolbaritem id="social-toolbar-button"
class="toolbarbutton-1 chromeclass-toolbar-additional"
removable="false"
title="&socialToolbar.title;"
hidden="true">
<hbox id="social-toolbar-button-box" class="social-statusarea-container">
<button id="social-provider-image" type="menu">
<menupopup id="social-statusarea-popup">
<hbox id="social-statusarea-user" pack="left" align="center">
<image id="social-statusarea-user-portrait"/>
<vbox>
<label id="social-statusarea-notloggedin"
value="&social.notLoggedIn.label;"/>
<button id="social-statusarea-username"
oncommand="SocialUI.showProfile(); document.getElementById('social-statusarea-popup').hidePopup();"/>
</vbox>
</hbox>
</menupopup>
</button>
<hbox id="social-status-iconbox" flex="1">
<box class="social-notification-icon-container" collapsed="true"
onclick="SocialToolbar.showAmbientPopup(this);">
<image class="social-notification-icon-image"/>
<box class="social-notification-icon-counter" collapsed="true"/>
</box>
<box class="social-notification-icon-container" collapsed="true"
onclick="SocialToolbar.showAmbientPopup(this);">
<image class="social-notification-icon-image"/>
<box class="social-notification-icon-counter" collapsed="true"/>
</box>
<box class="social-notification-icon-container" collapsed="true"
onclick="SocialToolbar.showAmbientPopup(this);">
<image class="social-notification-icon-image"/>
<box class="social-notification-icon-counter" collapsed="true"/>
</box>
</hbox>
</hbox>
</toolbaritem>
<toolbaritem id="bookmarks-menu-button-container"
class="chromeclass-toolbar-additional"
removable="true"

View File

@ -256,6 +256,7 @@ _BROWSER_FILES = \
browser_tabDrop.js \
browser_lastAccessedTab.js \
browser_bug734076.js \
browser_social_toolbar.js \
$(NULL)
ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT))

View File

@ -0,0 +1,140 @@
/* 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/. */
let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
let gProvider;
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref("social.enabled", true);
registerCleanupFunction(function () {
Services.prefs.clearUserPref("social.enabled");
});
let oldProvider;
function saveOldProviderAndStartTestWith(provider) {
oldProvider = Social.provider;
registerCleanupFunction(function () {
Social.provider = oldProvider;
});
Social.provider = gProvider = provider;
runTests(tests, undefined, undefined, function () {
SocialService.removeProvider(provider.origin, finish);
});
}
let manifest = { // normal provider
name: "provider 1",
origin: "https://example1.com",
workerURL: "https://example1.com/worker.js",
iconURL: "chrome://branding/content/icon48.png"
};
SocialService.addProvider(manifest, function(provider) {
// If the UI is already active, run the test immediately, otherwise wait
// for initialization.
if (Social.provider) {
saveOldProviderAndStartTestWith(provider);
} else {
Services.obs.addObserver(function obs() {
Services.obs.removeObserver(obs, "test-social-ui-ready");
saveOldProviderAndStartTestWith(provider);
}, "test-social-ui-ready", false);
}
});
}
var tests = {
testProfileSet: function(next) {
let profile = {
portrait: "chrome://branding/content/icon48.png",
userName: "trickster",
displayName: "Kuma Lisa",
profileURL: "http://en.wikipedia.org/wiki/Kuma_Lisa"
}
gProvider.updateUserProfile(profile);
// check dom values
let portrait = document.getElementById("social-statusarea-user-portrait").getAttribute("src");
is(portrait, profile.portrait, "portrait is set");
let userButton = document.getElementById("social-statusarea-username");
ok(!userButton.hidden, "username is visible");
is(userButton.label, profile.userName, "username is set");
next();
},
testAmbientNotifications: function(next) {
let ambience = {
name: "testIcon",
iconURL: "chrome://branding/content/icon48.png",
contentPanel: "about:blank",
counter: 42
};
gProvider.setAmbientNotification(ambience);
let statusIcons = document.getElementById("social-status-iconbox");
ok(!statusIcons.firstChild.collapsed, "status icon is visible");
ok(!statusIcons.firstChild.lastChild.collapsed, "status value is visible");
is(statusIcons.firstChild.lastChild.textContent, "42", "status value is correct");
ambience.counter = 0;
gProvider.setAmbientNotification(ambience);
ok(statusIcons.firstChild.lastChild.collapsed, "status value is not visible");
is(statusIcons.firstChild.lastChild.textContent, "", "status value is correct");
next();
},
testProfileUnset: function(next) {
gProvider.updateUserProfile({});
// check dom values
let portrait = document.getElementById("social-statusarea-user-portrait").getAttribute("src");
is(portrait, "chrome://browser/skin/social/social.png", "portrait is generic");
let userButton = document.getElementById("social-statusarea-username");
ok(userButton.hidden, "username is not visible");
let ambience = document.getElementById("social-status-iconbox").firstChild;
while (ambience) {
ok(ambience.collapsed, "ambient icon is collapsed");
ambience = ambience.nextSibling;
}
next();
}
}
function runTests(tests, cbPreTest, cbPostTest, cbFinish) {
let testIter = Iterator(tests);
if (cbPreTest === undefined) {
cbPreTest = function(cb) {cb()};
}
if (cbPostTest === undefined) {
cbPostTest = function(cb) {cb()};
}
function runNextTest() {
let name, func;
try {
[name, func] = testIter.next();
} catch (err if err instanceof StopIteration) {
// out of items:
(cbFinish || finish)();
return;
}
// We run on a timeout as the frameworker also makes use of timeouts, so
// this helps keep the debug messages sane.
executeSoon(function() {
function cleanupAndRunNextTest() {
info("sub-test " + name + " complete");
cbPostTest(runNextTest);
}
cbPreTest(function() {
info("sub-test " + name + " starting");
try {
func.call(tests, cleanupAndRunNextTest);
} catch (ex) {
ok(false, "sub-test " + name + " failed: " + ex.toString() +"\n"+ex.stack);
cleanupAndRunNextTest();
}
})
});
}
runNextTest();
}

View File

@ -653,6 +653,9 @@ doesn't display any label, but exposes a label to screen-readers with "aria-labe
toolbar button -->
<!ENTITY markupButton.accesskey "M">
<!ENTITY socialToolbar.title "Social Toolbar Button">
<!ENTITY social.notLoggedIn.label "Not logged in">
<!ENTITY social.sharePopup.undo.label "Unshare">
<!ENTITY social.sharePopup.undo.accesskey "U">
<!ENTITY social.sharePopup.ok.label "OK">

View File

@ -38,6 +38,10 @@ let Social = {
return SocialService.enabled;
},
get uiVisible() {
return this.provider && this.provider.enabled && this.provider.port;
},
sendWorkerMessage: function Social_sendWorkerMessage(message) {
// Responses aren't handled yet because there is no actions to perform
// based on the response from the provider at this point.

View File

@ -3343,3 +3343,123 @@ stack[anonid=browserStack][responsivemode] {
background-image: -moz-linear-gradient(top, #B4211B, #8A1915);
border-radius: 1px;
}
/* === social toolbar button === */
/* button icon for the service */
#social-provider-image {
-moz-appearance: none;
margin: 0;
padding: 2px;
min-width: 0;
max-height: 20px;
list-style-image: url("chrome://browser/skin/social/social.png");
}
/* hbox that surrounds an image and its counter */
.social-notification-icon-container {
cursor: pointer;
padding: 0px;
margin: 0px;
position: relative;
}
/* notification counter box */
.social-notification-icon-counter {
background-color: rgb(240,61,37);
border: 1px solid rgb(216,55,34);
box-shadow: 0px 1px 0px rgba(0,39,121,0.77);
padding-right: 1px;
padding-left: 1px;
color: white;
font-size: 9px;
font-weight: bold;
position: absolute;
right: -3px;
top: -4px;
z-index: 1;
text-align: center;
}
/* notification image */
.social-notification-icon-image {
padding: 2px;
margin: 0px;
min-width: 20px;
max-width: 32px;
max-height: 20px;
list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
}
/* === end of social toolbar button === */
/* === social toolbar provider menu === */
#social-statusarea-user {
background-color: white;
color: black;
cursor: default;
font-family: "lucida grande",tahoma,verdana,arial,sans-serif;
font-size: 12px;
}
#social-statusarea-user-portrait {
width: 32px;
height: 32px;
margin: 10px;
list-style-image: url("chrome://browser/skin/social/social.png");
}
#social-statusarea-username {
-moz-appearance: none;
color: -moz-nativehyperlinktext;
cursor: pointer;
min-width: 0;
margin: 0 6px;
}
#social-statusarea-username:hover {
text-decoration: underline;
}
/* === end of social toolbar provider menu === */
/* === start of social toolbar panels === */
#social-notification-panel {
min-height: 100px;
min-width: 240px;
max-height: 600px;
max-width: 400px;
}
#social-notification-panel .panel-arrowcontent {
margin: -4px 0 0 0;
padding: 0;
border-radius: 0px;
}
#social-notification-panel .panel-arrow[side="top"] {
list-style-image: url("chrome://browser/skin/social/panelarrow-up.png");
margin-top: -4px;
margin-bottom: 3px;
height: 21px;
}
#social-notification-panel .panel-arrow[side="bottom"] {
list-style-image: url("chrome://browser/skin/social/panelarrow-down.png");
margin-top: -5px;
}
#social-notification-panel .panel-arrow[side="left"] {
list-style-image: url("chrome://browser/skin/social/panelarrow-horiz.png");
margin-right: -1px;
-moz-transform: scaleX(-1);
}
#social-notification-panel .panel-arrow[side="right"] {
list-style-image: url("chrome://browser/skin/social/panelarrow-horiz.png");
margin-left: -1px;
}
/* === end of social toolbar panels === */

View File

@ -113,6 +113,10 @@ browser.jar:
* skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
skin/classic/browser/preferences/applications.css (preferences/applications.css)
skin/classic/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css)
skin/classic/browser/social/social.png (social/social.png)
skin/classic/browser/social/panelarrow-down.png (social/panelarrow-down.png)
skin/classic/browser/social/panelarrow-horiz.png (social/panelarrow-horiz.png)
skin/classic/browser/social/panelarrow-up.png (social/panelarrow-up.png)
skin/classic/browser/tabbrowser/alltabs-box-bkgnd-icon.png (tabbrowser/alltabs-box-bkgnd-icon.png)
skin/classic/browser/tabbrowser/newtab.png (tabbrowser/newtab.png)
skin/classic/browser/tabbrowser/connecting.png (tabbrowser/connecting.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -3270,3 +3270,134 @@ stack[anonid=browserStack][responsivemode] {
background-image: -moz-linear-gradient(top, #B4211B, #8A1915);
border-radius: 1px;
}
/* === social toolbar button === */
.social-statusarea-container {
-moz-appearance: toolbarbutton;
margin: 0 2px;
padding: 3px;
}
.social-statusarea-container {
-moz-appearance: none;
margin: 2px; /* make sure we have the correct platform spacing*/
padding: 1px;
}
/* aero look for hover */
#social-toolbar-button[open="true"] > .social-statusarea-container,
.social-statusarea-container:hover {
background-image: -moz-linear-gradient(hsla(0,0%,100%,.6), hsla(0,0%,100%,.1));
border-color: hsla(210,54%,20%,.3) hsla(210,54%,20%,.35) hsla(210,54%,20%,.4);
box-shadow: 0 1px hsla(0,0%,100%,.85) inset,
0 1px hsla(210,54%,20%,.1),
0 0 2px hsla(210,54%,20%,.4);
}
/* favicon for the service */
#social-provider-image {
width: 20px;
height: 20px;
max-height: 20px;
max-width: 20px;
padding: 2px;
list-style-image: url("chrome://browser/skin/social/social.png");
}
/* hbox that hold notification icons */
#social-status-iconbox {
margin: 0px;
padding: 0px;
}
/* hbox that surrounds an image and its counter */
.social-notification-icon-container {
cursor: pointer;
padding: 0px;
margin: 0px;
position: relative;
}
/* notification counter box */
.social-notification-icon-counter {
background-color: rgb(240,61,37);
border: 1px solid rgb(216,55,34);
box-shadow: 0px 1px 0px rgba(0,39,121,0.77);
padding-right: 1px;
padding-left: 1px;
color: white;
font-size: 9px;
font-weight: bold;
position: absolute;
right: -3px;
top: -4px;
z-index: 1;
text-align: center;
}
/* notification image */
.social-notification-icon-image {
padding: 2px;
margin: 0px;
min-width: 20px;
max-width: 32px;
max-height: 20px;
list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
}
/* === end of social toolbar button === */
/* === social toolbar provider menu === */
/* popups that hang off our toolbaritem */
#social-statusarea-popup {
margin-top:0px;
margin-left: -12px;
margin-right: -12px;
}
#social-statusarea-user {
border-bottom:1px solid rgb(221,221,221);
background-color:-moz-Dialog;
color:black;
cursor:default;
position:relative;
font-family: "lucida grande",tahoma,verdana,arial,sans-serif;
font-size:12px;
}
#social-statusarea-user-portrait {
width:32px;
height:32px;
border-radius:2px;
margin:10px;
list-style-image: url("chrome://browser/skin/social/social.png");
}
#social-statusarea-username {
-moz-appearance: none;
background: transparent;
border: none;
color: -moz-nativehyperlinktext;
cursor: pointer;
min-width: 0;
margin: 0 6px;
}
#social-statusarea-username:hover {
text-decoration: underline;
}
/* === end of social toolbar provider menu === */
/* === end of social toolbar panels === */
#social-notification-panel {
min-height: 100px;
min-width: 100px;
max-height: 600px;
max-width: 400px;
}
/* === end of social toolbar panels === */

View File

@ -106,6 +106,7 @@ browser.jar:
skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
skin/classic/browser/preferences/applications.css (preferences/applications.css)
skin/classic/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css)
skin/classic/browser/social/social.png (social/social.png)
skin/classic/browser/tabbrowser/alltabs.png (tabbrowser/alltabs.png)
skin/classic/browser/tabbrowser/newtab.png (tabbrowser/newtab.png)
skin/classic/browser/tabbrowser/newtab-inverted.png (tabbrowser/newtab-inverted.png)
@ -307,6 +308,7 @@ browser.jar:
skin/classic/aero/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
skin/classic/aero/browser/preferences/applications.css (preferences/applications.css)
skin/classic/aero/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css)
skin/classic/aero/browser/social/social.png (social/social.png)
skin/classic/aero/browser/tabbrowser/alltabs.png (tabbrowser/alltabs.png)
skin/classic/aero/browser/tabbrowser/newtab.png (tabbrowser/newtab.png)
skin/classic/aero/browser/tabbrowser/newtab-inverted.png (tabbrowser/newtab-inverted.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB