diff --git a/bootstrap.js b/bootstrap.js
index fa1c267..56eccf2 100644
--- a/bootstrap.js
+++ b/bootstrap.js
@@ -112,6 +112,7 @@ function initPrefs() {
var defaults = {
defaultPaused: false,
showOverlays: true,
+ indicatorStyle: 0,
toggleOnClick: false,
originalAnimationMode: "undefined",
playOnHover: false,
diff --git a/content/android-options.xul b/content/android-options.xul
index 43129e9..69d66fb 100644
--- a/content/android-options.xul
+++ b/content/android-options.xul
@@ -6,5 +6,6 @@
+
diff --git a/content/content.js b/content/content.js
index ce70ab2..5b0d82b 100644
--- a/content/content.js
+++ b/content/content.js
@@ -98,12 +98,16 @@ var OverlayCss = [
// It would be okay to have this per document, but I'm lazy.
var CurrentHover = null;
var individuallyToggledImages = new WeakMap();
+var animationIndicators = new WeakMap();
var AddonIsEnabled = true;
var Prefs = null;
// === Expando symbols ===
+var eNoMoreGifIndicators = "toggleGifs-noMoreGifIndicators";
+var eHasHovered = "toggleGifs-hasHovered";
var eAttachedLoadWaiter = "toggleGifs-attachedLoadWaiter";
+var eInitedGif = "toggleGifs-initedGif";
var eRelatedTo = "toggleGifs-relatedTo";
var eFakeImage = "toggleGifs-fakeImage";
var eHandledPinterest = "toggleGifs-handledPinterest";
@@ -165,6 +169,14 @@ function resetImageAnimation(el) {
}
}
+function forEachAnimationIndicator(doc, callback) {
+ var indicators = animationIndicators.get(doc);
+ if (!indicators)
+ return;
+ for (var el of indicators)
+ callback(el);
+}
+
function hasEventListenerOfType(el, type) {
var listeners = elService.getListenerInfoFor(el, {});
for (var i = 0; i < listeners.length; i++) {
@@ -203,6 +215,12 @@ function isGifv(el) {
return el.hasAttribute("muted") && el.hasAttribute("loop") && el.hasAttribute("autoplay");
}
+function imageTooSmall(el) {
+ if (el.width && el.height)
+ return (el.width < ButtonsMinWidth || el.height < ButtonsMinHeight);
+ return (el.offsetWidth < ButtonsMinWidth || el.offsetHeight < ButtonsMinHeight);
+}
+
function isEditable(node) {
var doc = node.ownerDocument;
if (doc.designMode === "on" || node.isContentEditable)
@@ -276,6 +294,7 @@ function toggleGifsInWindow(win) {
setGifStateForWindow(win, curState === 1);
if (CurrentHover)
CurrentHover.refresh();
+ forEachAnimationIndicator(win.document, showAnimationIndicator);
} catch (ex) {
// Some invisible iframes don't have presContexts, which breaks
// the imageAnimationMode getter.
@@ -330,7 +349,7 @@ function updateKeyListeners(forceRemove) {
removeEventListener("keydown", onKeydown);
}
-// ==== Exception lists ====
+// ==== Load listeners ====
var exceptionList = [];
function locationOnExceptionList(loc) {
@@ -352,23 +371,28 @@ function locationOnExceptionList(loc) {
function handleContentDocumentLoad(doc) {
var win = doc && doc.defaultView, loc = doc && doc.location;
+
if (loc && win && win.document === doc && loc.protocol !== "data:" &&
locationOnExceptionList(loc)) {
// This site is on the exception list. Play gifs iff the default is to pause them.
var play = Prefs.defaultPaused;
setGifStateForWindow(win, play);
}
+ if (Prefs.indicatorStyle > 0)
+ showIndicatorsForDocument(doc, true);
}
function onDOMWindowCreated(ev) {
handleContentDocumentLoad(ev.target);
}
+var shownIndicator = false;
function maybeHookContentWindows(forceUnhook, initial) {
function id(x) { return x; }
function addDot(x) { return "." + x; }
exceptionList = Prefs.pauseExceptions.split(/[ ,]+/).filter(id).map(addDot);
- var shouldHook = exceptionList.length > 0 && !forceUnhook;
+ var shouldShowIndicator = Prefs.indicatorStyle > 0 && !forceUnhook;
+ var shouldHook = (Prefs.indicatorStyle > 0 || exceptionList.length > 0) && !forceUnhook;
if (shouldHook)
addEventListener("DOMWindowCreated", onDOMWindowCreated);
else
@@ -380,6 +404,99 @@ function maybeHookContentWindows(forceUnhook, initial) {
handleContentDocumentLoad(w.document);
});
}
+
+ if (shownIndicator && !shouldShowIndicator) {
+ iterateFrames(content, function(w) {
+ showIndicatorsForDocument(w.document, false);
+ });
+ }
+ shownIndicator = shouldShowIndicator;
+}
+
+function showIndicatorsForDocument(doc, show) {
+ if (show) {
+ doc.addEventListener("load", onLoad, true);
+ }
+ else {
+ doc.removeEventListener("load", onLoad, true);
+ forEachAnimationIndicator(doc, function(el) {
+ el.style.filter = el.opacity = "";
+ });
+ animationIndicators.delete(doc);
+ doc[eNoMoreGifIndicators] = true;
+ }
+}
+
+function initGifState(el, noIndicator) {
+ var doc = el.ownerDocument;
+ if (el[eInitedGif])
+ return;
+ el[eInitedGif] = true;
+ if (isGifv(el)) {
+ if (Prefs.defaultPaused)
+ getIc(el).animationMode = 1;
+ }
+ if (Prefs.indicatorStyle > 0 && !noIndicator && !imageTooSmall(el)) {
+ var ic = getIc(el), shouldShow = false;
+ try {
+ shouldShow = (ic && ic.animated);
+ }
+ catch (ex) {
+ // Image not yet decoded, so we don't know the animation state.
+ // Set up an observer and wait for a decode signal.
+ // Removing the observers is somewhat annoying, so we don't.
+ var ilc = el.QueryInterface(Ci.nsIImageLoadingContent);
+ if (!ilc)
+ return;
+ var observer = null;
+ var jsObserver = {
+ sizeAvailable: function() {},
+ frameComplete: function() {},
+ decodeComplete: function() {},
+ loadComplete: function() {},
+ frameUpdate: function() {},
+ discard: function() {},
+ isAnimated: function() {},
+ };
+ jsObserver.decodeComplete = function() {
+ ilc.removeObserver(observer);
+ // Set a zero-length timeout, because I'm not certain that it's
+ // safe to run arbitrary scripts off observer notifications.
+ doc.defaultView.setTimeout(function() {
+ if (!AddonIsEnabled || Prefs.indicatorStyle === 0 || doc[eNoMoreGifIndicators])
+ return;
+ var ic = getIc(el);
+ if (ic && ic.animated && !el[eHasHovered])
+ showAnimationIndicator(el);
+ });
+ };
+ observer = Cc["@mozilla.org/image/tools;1"]
+ .getService(Ci.imgITools).createScriptedObserver(jsObserver);
+ ilc.addObserver(observer);
+ }
+ if (shouldShow)
+ showAnimationIndicator(el);
+ }
+}
+
+function onLoadedMetadata(event) {
+ if (isGifv(event.target))
+ initGifState(event.target);
+}
+
+function onLoad(event) {
+ if (!AddonIsEnabled)
+ return;
+ var el = event.target;
+ if (el.localName === "img")
+ initGifState(el);
+}
+
+function updateLoadListeners(forceRemove) {
+ if (!forceRemove)
+ addEventListener("loadedmetadata", onLoadedMetadata, true);
+ else
+ removeEventListener("loadedmetadata", onLoadedMetadata, true);
}
// ==== Hovering ====
@@ -399,6 +516,56 @@ function onImageMouseDown(event) {
CurrentHover.toggleImageAnimation();
}
+function showAnimationIndicator(el) {
+ var doc = el.ownerDocument;
+ var indicators = animationIndicators.get(doc);
+ if (!indicators) {
+ indicators = new Set();
+ animationIndicators.set(doc, indicators);
+
+ var cr = function(name, attrs) {
+ var ret = doc.createElementNS("http://www.w3.org/2000/svg", name);
+ for (var a in attrs)
+ ret.setAttribute(a, attrs[a]);
+ return ret;
+ };
+ var crFilter = function(id, img) {
+ var ret = cr("filter", {id: id, x: "0", y: "0", width: "100%", height: "100%"});
+ var feImage = cr("feImage", {preserveAspectRatio: "xMaxYMin meet", width: "100%", height: "19", y: "3", result: "img"});
+ feImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", img);
+ var feComposite = cr("feComposite", {operator: "over", in: "img", in2: "SourceGraphic"});
+ ret.appendChild(feImage);
+ ret.appendChild(feComposite);
+ return ret;
+ };
+ var svg = cr("svg", {width: "0", height: "0"});
+ svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
+ var defs = cr("defs", {});
+ defs.appendChild(crFilter("toggleGifsIndicatorFilterPlay", PlayIcon));
+ defs.appendChild(crFilter("toggleGifsIndicatorFilterPause", PauseIcon));
+ svg.appendChild(defs);
+ doc.documentElement.appendChild(svg);
+ }
+
+ try {
+ // TODO: This isn't necessarily accurate if the image is display: none.
+ var playing = (getIc(el).animationMode === 0);
+ if (Prefs.indicatorStyle === 1) {
+ el.style.filter = "url(#toggleGifsIndicatorFilter" + (playing ? "Pause" : "Play") + ")";
+ }
+ else if (Prefs.indicatorStyle === 2) {
+ if (playing) {
+ el.style.opacity = "1";
+ indicators.delete(el);
+ return;
+ }
+ el.style.transition = "opacity 0.4s";
+ el.style.opacity = "0.2";
+ }
+ indicators.add(el);
+ } catch (ex) {} // no longer visible, or so
+}
+
function clearHoverEffect() {
if (!CurrentHover)
return;
@@ -422,7 +589,8 @@ function clearHoverEffect() {
function applyHoverEffect(el) {
var doc = el.ownerDocument, win = doc.defaultView;
var ic = getIc(el);
- initGifvState(el);
+ initGifState(el, true);
+ el[eHasHovered] = true;
CurrentHover = {
element: el,
@@ -485,11 +653,19 @@ function applyHoverEffect(el) {
}
}
+ var indicators = animationIndicators.get(doc);
+ if (indicators && indicators.has(el)) {
+ if (Prefs.indicatorStyle === 1)
+ el.style.filter = "";
+ else if (Prefs.indicatorStyle === 2)
+ el.style.opacity = "1";
+ indicators.delete(el);
+ }
+
if (!Prefs.showOverlays)
return;
- var offsetBase = el[ePositionAsIf] || el;
- if (offsetBase.offsetWidth < ButtonsMinWidth || offsetBase.offsetHeight < ButtonsMinHeight)
+ if (imageTooSmall(el))
return;
var overlay = doc.createElement("div");
@@ -531,6 +707,7 @@ function applyHoverEffect(el) {
content.appendChild(pauseButton);
overlay.appendChild(content);
+ var offsetBase = el[ePositionAsIf] || el;
var reposition = function() {
var par = offsetBase.offsetParent;
var x = offsetBase.offsetLeft, y = offsetBase.offsetTop;
@@ -707,28 +884,6 @@ function updateHoverListeners(forceRemove) {
}
}
-// ==== Load listeners ====
-
-function initGifvState(el) {
- if (!isGifv(el) || el.initedGifv)
- return;
- el.initedGifv = true;
-
- if (Prefs.defaultPaused)
- getIc(el).animationMode = 1;
-}
-
-function onLoadedMetadata(event) {
- initGifvState(event.target);
-}
-
-function updateLoadListeners(forceRemove) {
- if (!forceRemove)
- addEventListener("loadedmetadata", onLoadedMetadata, true);
- else
- removeEventListener("loadedmetadata", onLoadedMetadata, true);
-}
-
// === Signals ===
var MMPrefix = Components.stack.filename + ":";
@@ -745,20 +900,17 @@ addSignal("set-gif-state", function(msg) {
addSignal("update-pref", function(msg) {
var key = msg.data.key, value = msg.data.value;
Prefs[key] = value;
- if (key === "showOverlays" || key === "toggleOnClick") {
+ if (key === "showOverlays" || key === "toggleOnClick")
updateHoverListeners();
- }
- else if (key === "hoverPauseWhen") {
+ else if (key === "hoverPauseWhen")
updateClickListeners();
- }
- else if (key === "pauseExceptions") {
+ else if (key === "pauseExceptions" || key === "indicatorStyle")
maybeHookContentWindows();
- }
});
addSignal("destroy", function() {
AddonIsEnabled = false;
- for (let sig of signals)
+ for (var sig of signals)
removeMessageListener(MMPrefix + sig.signal, sig.callback);
updateKeyListeners(true);
updateClickListeners(true);
diff --git a/content/options.xul b/content/options.xul
index 926d90d..f5c0dcb 100644
--- a/content/options.xul
+++ b/content/options.xul
@@ -13,6 +13,7 @@
+
diff --git a/install.rdf b/install.rdf
index 5e4adf0..217abe3 100644
--- a/install.rdf
+++ b/install.rdf
@@ -4,7 +4,7 @@
giftoggle@simonsoftware.se
2
Toggle animated GIFs
- 1.2.2
+ 1.2.3
true
true
Simon Lindholm