Skip to content

Commit

Permalink
Add an option for showing indicators for animated GIFs
Browse files Browse the repository at this point in the history
Closes #16.
  • Loading branch information
simonlindholm committed Jul 8, 2015
1 parent 74917c4 commit 6574dc9
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 35 deletions.
1 change: 1 addition & 0 deletions bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ function initPrefs() {
var defaults = {
defaultPaused: false,
showOverlays: true,
indicatorStyle: 0,
toggleOnClick: false,
originalAnimationMode: "undefined",
playOnHover: false,
Expand Down
1 change: 1 addition & 0 deletions content/android-options.xul
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
<setting type="bool" pref="extensions.togglegifs.hoverPlayOnLoad" title="– as soon as the GIF loads" />
<setting type="boolint" pref="extensions.togglegifs.hoverPauseWhen" title="– and stop when tapping another GIF" on="1" off="0" />
<setting type="bool" pref="extensions.togglegifs.showOverlays" title="Display GIF controls on tap" />
<setting type="boolint" pref="extensions.togglegifs.indicatorStyle" title="Show an indicator for animated GIFs" on="1" off="0" />
<setting type="bool" pref="extensions.togglegifs.toggleOnClick" title="Toggle GIFs on click" />
</vbox>
220 changes: 186 additions & 34 deletions content/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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++) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -330,7 +349,7 @@ function updateKeyListeners(forceRemove) {
removeEventListener("keydown", onKeydown);
}

// ==== Exception lists ====
// ==== Load listeners ====

var exceptionList = [];
function locationOnExceptionList(loc) {
Expand All @@ -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
Expand All @@ -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 ====
Expand All @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 + ":";
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions content/options.xul
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</radiogroup>
</setting>
<setting type="bool" pref="extensions.togglegifs.showOverlays" title="Display GIF controls on hover" />
<setting type="boolint" pref="extensions.togglegifs.indicatorStyle" title="Show an indicator for animated GIFs" on="1" off="0" />
<setting type="bool" pref="extensions.togglegifs.toggleOnClick" title="Toggle GIFs on click" />
<setting type="string" pref="extensions.togglegifs.shortcutToggle" title="Shortcut for toggling GIFs" class="togglegifs-shortcut" />
<setting type="string" pref="extensions.togglegifs.shortcutReset" title="Shortcut for resetting GIFs" class="togglegifs-shortcut" />
Expand Down
2 changes: 1 addition & 1 deletion install.rdf
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<em:id>[email protected]</em:id>
<em:type>2</em:type>
<em:name>Toggle animated GIFs</em:name>
<em:version>1.2.2</em:version>
<em:version>1.2.3</em:version>
<em:bootstrap>true</em:bootstrap>
<em:multiprocessCompatible>true</em:multiprocessCompatible>
<em:creator>Simon Lindholm</em:creator>
Expand Down

0 comments on commit 6574dc9

Please sign in to comment.