diff -r 000000000000 -r 6474c204b198 browser/components/tabview/modules/utils.jsm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/components/tabview/modules/utils.jsm Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,823 @@ +/* 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"; + +// ********** +// Title: utils.js + +this.EXPORTED_SYMBOLS = ["Point", "Rect", "Range", "Subscribable", "Utils", "MRUList"]; + +// ######### +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +// ########## +// Class: Point +// A simple point. +// +// Constructor: Point +// If a is a Point, creates a copy of it. Otherwise, expects a to be x, +// and creates a Point with it along with y. If either a or y are omitted, +// 0 is used in their place. +this.Point = function Point(a, y) { + if (Utils.isPoint(a)) { + this.x = a.x; + this.y = a.y; + } else { + this.x = (Utils.isNumber(a) ? a : 0); + this.y = (Utils.isNumber(y) ? y : 0); + } +}; + +Point.prototype = { + // ---------- + // Function: toString + // Prints [Point (x,y)] for debug use + toString: function Point_toString() { + return "[Point (" + this.x + "," + this.y + ")]"; + }, + + // ---------- + // Function: distance + // Returns the distance from this point to the given . + distance: function Point_distance(point) { + var ax = this.x - point.x; + var ay = this.y - point.y; + return Math.sqrt((ax * ax) + (ay * ay)); + } +}; + +// ########## +// Class: Rect +// A simple rectangle. Note that in addition to the left and width, it also has +// a right property; changing one affects the others appropriately. Same for the +// vertical properties. +// +// Constructor: Rect +// If a is a Rect, creates a copy of it. Otherwise, expects a to be left, +// and creates a Rect with it along with top, width, and height. +this.Rect = function Rect(a, top, width, height) { + // Note: perhaps 'a' should really be called 'rectOrLeft' + if (Utils.isRect(a)) { + this.left = a.left; + this.top = a.top; + this.width = a.width; + this.height = a.height; + } else { + this.left = a; + this.top = top; + this.width = width; + this.height = height; + } +}; + +Rect.prototype = { + // ---------- + // Function: toString + // Prints [Rect (left,top,width,height)] for debug use + toString: function Rect_toString() { + return "[Rect (" + this.left + "," + this.top + "," + + this.width + "," + this.height + ")]"; + }, + + get right() this.left + this.width, + set right(value) { + this.width = value - this.left; + }, + + get bottom() this.top + this.height, + set bottom(value) { + this.height = value - this.top; + }, + + // ---------- + // Variable: xRange + // Gives you a new for the horizontal dimension. + get xRange() new Range(this.left, this.right), + + // ---------- + // Variable: yRange + // Gives you a new for the vertical dimension. + get yRange() new Range(this.top, this.bottom), + + // ---------- + // Function: intersects + // Returns true if this rectangle intersects the given . + intersects: function Rect_intersects(rect) { + return (rect.right > this.left && + rect.left < this.right && + rect.bottom > this.top && + rect.top < this.bottom); + }, + + // ---------- + // Function: intersection + // Returns a new with the intersection of this rectangle and the give , + // or null if they don't intersect. + intersection: function Rect_intersection(rect) { + var box = new Rect(Math.max(rect.left, this.left), Math.max(rect.top, this.top), 0, 0); + box.right = Math.min(rect.right, this.right); + box.bottom = Math.min(rect.bottom, this.bottom); + if (box.width > 0 && box.height > 0) + return box; + + return null; + }, + + // ---------- + // Function: contains + // Returns a boolean denoting if the or is contained inside + // this rectangle. + // + // Parameters + // - A or a + contains: function Rect_contains(a) { + if (Utils.isPoint(a)) + return (a.x > this.left && + a.x < this.right && + a.y > this.top && + a.y < this.bottom); + + return (a.left >= this.left && + a.right <= this.right && + a.top >= this.top && + a.bottom <= this.bottom); + }, + + // ---------- + // Function: center + // Returns a new with the center location of this rectangle. + center: function Rect_center() { + return new Point(this.left + (this.width / 2), this.top + (this.height / 2)); + }, + + // ---------- + // Function: size + // Returns a new with the dimensions of this rectangle. + size: function Rect_size() { + return new Point(this.width, this.height); + }, + + // ---------- + // Function: position + // Returns a new with the top left of this rectangle. + position: function Rect_position() { + return new Point(this.left, this.top); + }, + + // ---------- + // Function: area + // Returns the area of this rectangle. + area: function Rect_area() { + return this.width * this.height; + }, + + // ---------- + // Function: inset + // Makes the rect smaller (if the arguments are positive) as if a margin is added all around + // the initial rect, with the margin widths (symmetric) being specified by the arguments. + // + // Paramaters + // - A or two arguments: x and y + inset: function Rect_inset(a, b) { + if (Utils.isPoint(a)) { + b = a.y; + a = a.x; + } + + this.left += a; + this.width -= a * 2; + this.top += b; + this.height -= b * 2; + }, + + // ---------- + // Function: offset + // Moves (translates) the rect by the given vector. + // + // Paramaters + // - A or two arguments: x and y + offset: function Rect_offset(a, b) { + if (Utils.isPoint(a)) { + this.left += a.x; + this.top += a.y; + } else { + this.left += a; + this.top += b; + } + }, + + // ---------- + // Function: equals + // Returns true if this rectangle is identical to the given . + equals: function Rect_equals(rect) { + return (rect.left == this.left && + rect.top == this.top && + rect.width == this.width && + rect.height == this.height); + }, + + // ---------- + // Function: union + // Returns a new with the union of this rectangle and the given . + union: function Rect_union(a) { + var newLeft = Math.min(a.left, this.left); + var newTop = Math.min(a.top, this.top); + var newWidth = Math.max(a.right, this.right) - newLeft; + var newHeight = Math.max(a.bottom, this.bottom) - newTop; + var newRect = new Rect(newLeft, newTop, newWidth, newHeight); + + return newRect; + }, + + // ---------- + // Function: copy + // Copies the values of the given into this rectangle. + copy: function Rect_copy(a) { + this.left = a.left; + this.top = a.top; + this.width = a.width; + this.height = a.height; + } +}; + +// ########## +// Class: Range +// A physical interval, with a min and max. +// +// Constructor: Range +// Creates a Range with the given min and max +this.Range = function Range(min, max) { + if (Utils.isRange(min) && !max) { // if the one variable given is a range, copy it. + this.min = min.min; + this.max = min.max; + } else { + this.min = min || 0; + this.max = max || 0; + } +}; + +Range.prototype = { + // ---------- + // Function: toString + // Prints [Range (min,max)] for debug use + toString: function Range_toString() { + return "[Range (" + this.min + "," + this.max + ")]"; + }, + + // Variable: extent + // Equivalent to max-min + get extent() { + return (this.max - this.min); + }, + + set extent(extent) { + this.max = extent - this.min; + }, + + // ---------- + // Function: contains + // Whether the contains the given or value or not. + // + // Parameters + // - a number or + contains: function Range_contains(value) { + if (Utils.isNumber(value)) + return value >= this.min && value <= this.max; + if (Utils.isRange(value)) + return value.min >= this.min && value.max <= this.max; + return false; + }, + + // ---------- + // Function: overlaps + // Whether the overlaps with the given value or not. + // + // Parameters + // - a number or + overlaps: function Range_overlaps(value) { + if (Utils.isNumber(value)) + return this.contains(value); + if (Utils.isRange(value)) + return !(value.max < this.min || this.max < value.min); + return false; + }, + + // ---------- + // Function: proportion + // Maps the given value to the range [0,1], so that it returns 0 if the value is <= the min, + // returns 1 if the value >= the max, and returns an interpolated "proportion" in (min, max). + // + // Parameters + // - a number + // - (bool) smooth? If true, a smooth tanh-based function will be used instead of the linear. + proportion: function Range_proportion(value, smooth) { + if (value <= this.min) + return 0; + if (this.max <= value) + return 1; + + var proportion = (value - this.min) / this.extent; + + if (smooth) { + // The ease function ".5+.5*Math.tanh(4*x-2)" is a pretty + // little graph. It goes from near 0 at x=0 to near 1 at x=1 + // smoothly and beautifully. + // http://www.wolframalpha.com/input/?i=.5+%2B+.5+*+tanh%28%284+*+x%29+-+2%29 + let tanh = function tanh(x) { + var e = Math.exp(x); + return (e - 1/e) / (e + 1/e); + }; + + return .5 - .5 * tanh(2 - 4 * proportion); + } + + return proportion; + }, + + // ---------- + // Function: scale + // Takes the given value in [0,1] and maps it to the associated value on the Range. + // + // Parameters + // - a number in [0,1] + scale: function Range_scale(value) { + if (value > 1) + value = 1; + if (value < 0) + value = 0; + return this.min + this.extent * value; + } +}; + +// ########## +// Class: Subscribable +// A mix-in for allowing objects to collect subscribers for custom events. +this.Subscribable = function Subscribable() { + this.subscribers = null; +}; + +Subscribable.prototype = { + // ---------- + // Function: addSubscriber + // The given callback will be called when the Subscribable fires the given event. + addSubscriber: function Subscribable_addSubscriber(eventName, callback) { + try { + Utils.assertThrow(typeof callback == "function", "callback must be a function"); + Utils.assertThrow(eventName && typeof eventName == "string", + "eventName must be a non-empty string"); + } catch(e) { + Utils.log(e); + return; + } + + if (!this.subscribers) + this.subscribers = {}; + + if (!this.subscribers[eventName]) + this.subscribers[eventName] = []; + + let subscribers = this.subscribers[eventName]; + if (subscribers.indexOf(callback) == -1) + subscribers.push(callback); + }, + + // ---------- + // Function: removeSubscriber + // Removes the subscriber associated with the event for the given callback. + removeSubscriber: function Subscribable_removeSubscriber(eventName, callback) { + try { + Utils.assertThrow(typeof callback == "function", "callback must be a function"); + Utils.assertThrow(eventName && typeof eventName == "string", + "eventName must be a non-empty string"); + } catch(e) { + Utils.log(e); + return; + } + + if (!this.subscribers || !this.subscribers[eventName]) + return; + + let subscribers = this.subscribers[eventName]; + let index = subscribers.indexOf(callback); + + if (index > -1) + subscribers.splice(index, 1); + }, + + // ---------- + // Function: _sendToSubscribers + // Internal routine. Used by the Subscribable to fire events. + _sendToSubscribers: function Subscribable__sendToSubscribers(eventName, eventInfo) { + try { + Utils.assertThrow(eventName && typeof eventName == "string", + "eventName must be a non-empty string"); + } catch(e) { + Utils.log(e); + return; + } + + if (!this.subscribers || !this.subscribers[eventName]) + return; + + let subsCopy = this.subscribers[eventName].concat(); + subsCopy.forEach(function (callback) { + try { + callback(this, eventInfo); + } catch(e) { + Utils.log(e); + } + }, this); + } +}; + +// ########## +// Class: Utils +// Singelton with common utility functions. +this.Utils = { + // ---------- + // Function: toString + // Prints [Utils] for debug use + toString: function Utils_toString() { + return "[Utils]"; + }, + + // ___ Logging + useConsole: true, // as opposed to dump + showTime: false, + + // ---------- + // Function: log + // Prints the given arguments to the JavaScript error console as a message. + // Pass as many arguments as you want, it'll print them all. + log: function Utils_log() { + var text = this.expandArgumentsForLog(arguments); + var prefix = this.showTime ? Date.now() + ': ' : ''; + if (this.useConsole) + Services.console.logStringMessage(prefix + text); + else + dump(prefix + text + '\n'); + }, + + // ---------- + // Function: error + // Prints the given arguments to the JavaScript error console as an error. + // Pass as many arguments as you want, it'll print them all. + error: function Utils_error() { + var text = this.expandArgumentsForLog(arguments); + var prefix = this.showTime ? Date.now() + ': ' : ''; + if (this.useConsole) + Cu.reportError(prefix + "tabview error: " + text); + else + dump(prefix + "TABVIEW ERROR: " + text + '\n'); + }, + + // ---------- + // Function: trace + // Prints the given arguments to the JavaScript error console as a message, + // along with a full stack trace. + // Pass as many arguments as you want, it'll print them all. + trace: function Utils_trace() { + var text = this.expandArgumentsForLog(arguments); + + // cut off the first line of the stack trace, because that's just this function. + let stack = Error().stack.split("\n").slice(1); + + // if the caller was assert, cut out the line for the assert function as well. + if (stack[0].indexOf("Utils_assert(") == 0) + stack.splice(0, 1); + + this.log('trace: ' + text + '\n' + stack.join("\n")); + }, + + // ---------- + // Function: assert + // Prints a stack trace along with label (as a console message) if condition is false. + assert: function Utils_assert(condition, label) { + if (!condition) { + let text; + if (typeof label != 'string') + text = 'badly formed assert'; + else + text = "tabview assert: " + label; + + this.trace(text); + } + }, + + // ---------- + // Function: assertThrow + // Throws label as an exception if condition is false. + assertThrow: function Utils_assertThrow(condition, label) { + if (!condition) { + let text; + if (typeof label != 'string') + text = 'badly formed assert'; + else + text = "tabview assert: " + label; + + // cut off the first line of the stack trace, because that's just this function. + let stack = Error().stack.split("\n").slice(1); + + throw text + "\n" + stack.join("\n"); + } + }, + + // ---------- + // Function: expandObject + // Prints the given object to a string, including all of its properties. + expandObject: function Utils_expandObject(obj) { + var s = obj + ' = {'; + for (let prop in obj) { + let value; + try { + value = obj[prop]; + } catch(e) { + value = '[!!error retrieving property]'; + } + + s += prop + ': '; + if (typeof value == 'string') + s += '\'' + value + '\''; + else if (typeof value == 'function') + s += 'function'; + else + s += value; + + s += ', '; + } + return s + '}'; + }, + + // ---------- + // Function: expandArgumentsForLog + // Expands all of the given args (an array) into a single string. + expandArgumentsForLog: function Utils_expandArgumentsForLog(args) { + var that = this; + return Array.map(args, function(arg) { + return typeof arg == 'object' ? that.expandObject(arg) : arg; + }).join('; '); + }, + + // ___ Misc + + // ---------- + // Function: isLeftClick + // Given a DOM mouse event, returns true if it was for the left mouse button. + isLeftClick: function Utils_isLeftClick(event) { + return event.button == 0; + }, + + // ---------- + // Function: isMiddleClick + // Given a DOM mouse event, returns true if it was for the middle mouse button. + isMiddleClick: function Utils_isMiddleClick(event) { + return event.button == 1; + }, + + // ---------- + // Function: isRightClick + // Given a DOM mouse event, returns true if it was for the right mouse button. + isRightClick: function Utils_isRightClick(event) { + return event.button == 2; + }, + + // ---------- + // Function: isDOMElement + // Returns true if the given object is a DOM element. + isDOMElement: function Utils_isDOMElement(object) { + return object instanceof Ci.nsIDOMElement; + }, + + // ---------- + // Function: isValidXULTab + // A xulTab is valid if it has not been closed, + // and it has not been removed from the DOM + // Returns true if the tab is valid. + isValidXULTab: function Utils_isValidXULTab(xulTab) { + return !xulTab.closing && xulTab.parentNode; + }, + + // ---------- + // Function: isNumber + // Returns true if the argument is a valid number. + isNumber: function Utils_isNumber(n) { + return typeof n == 'number' && !isNaN(n); + }, + + // ---------- + // Function: isRect + // Returns true if the given object (r) looks like a . + isRect: function Utils_isRect(r) { + return (r && + this.isNumber(r.left) && + this.isNumber(r.top) && + this.isNumber(r.width) && + this.isNumber(r.height)); + }, + + // ---------- + // Function: isRange + // Returns true if the given object (r) looks like a . + isRange: function Utils_isRange(r) { + return (r && + this.isNumber(r.min) && + this.isNumber(r.max)); + }, + + // ---------- + // Function: isPoint + // Returns true if the given object (p) looks like a . + isPoint: function Utils_isPoint(p) { + return (p && this.isNumber(p.x) && this.isNumber(p.y)); + }, + + // ---------- + // Function: isPlainObject + // Check to see if an object is a plain object (created using "{}" or "new Object"). + isPlainObject: function Utils_isPlainObject(obj) { + // Must be an Object. + // Make sure that DOM nodes and window objects don't pass through, as well + if (!obj || Object.prototype.toString.call(obj) !== "[object Object]" || + obj.nodeType || obj.setInterval) { + return false; + } + + // Not own constructor property must be Object + const hasOwnProperty = Object.prototype.hasOwnProperty; + + if (obj.constructor && + !hasOwnProperty.call(obj, "constructor") && + !hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf")) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for (key in obj) {} + + return key === undefined || hasOwnProperty.call(obj, key); + }, + + // ---------- + // Function: isEmptyObject + // Returns true if the given object has no members. + isEmptyObject: function Utils_isEmptyObject(obj) { + for (let name in obj) + return false; + return true; + }, + + // ---------- + // Function: copy + // Returns a copy of the argument. Note that this is a shallow copy; if the argument + // has properties that are themselves objects, those properties will be copied by reference. + copy: function Utils_copy(value) { + if (value && typeof value == 'object') { + if (Array.isArray(value)) + return this.extend([], value); + return this.extend({}, value); + } + return value; + }, + + // ---------- + // Function: merge + // Merge two array-like objects into the first and return it. + merge: function Utils_merge(first, second) { + Array.forEach(second, function(el) Array.push(first, el)); + return first; + }, + + // ---------- + // Function: extend + // Pass several objects in and it will combine them all into the first object and return it. + extend: function Utils_extend() { + + // copy reference to target object + let target = arguments[0] || {}; + // Deep copy is not supported + if (typeof target === "boolean") { + this.assert(false, "The first argument of extend cannot be a boolean." + + "Deep copy is not supported."); + return target; + } + + // Back when this was in iQ + iQ.fn, so you could extend iQ objects with it. + // This is no longer supported. + let length = arguments.length; + if (length === 1) { + this.assert(false, "Extending the iQ prototype using extend is not supported."); + return target; + } + + // Handle case when target is a string or something + if (typeof target != "object" && typeof target != "function") { + target = {}; + } + + for (let i = 1; i < length; i++) { + // Only deal with non-null/undefined values + let options = arguments[i]; + if (options != null) { + // Extend the base object + for (let name in options) { + let copy = options[name]; + + // Prevent never-ending loop + if (target === copy) + continue; + + if (copy !== undefined) + target[name] = copy; + } + } + } + + // Return the modified object + return target; + }, + + // ---------- + // Function: attempt + // Tries to execute a number of functions. Returns immediately the return + // value of the first non-failed function without executing successive + // functions, or null. + attempt: function Utils_attempt() { + let args = arguments; + + for (let i = 0; i < args.length; i++) { + try { + return args[i](); + } catch (e) {} + } + + return null; + } +}; + +// ########## +// Class: MRUList +// A most recently used list. +// +// Constructor: MRUList +// If a is an array of entries, creates a copy of it. +this.MRUList = function MRUList(a) { + if (Array.isArray(a)) + this._list = a.concat(); + else + this._list = []; +}; + +MRUList.prototype = { + // ---------- + // Function: toString + // Prints [List (entry1, entry2, ...)] for debug use + toString: function MRUList_toString() { + return "[List (" + this._list.join(", ") + ")]"; + }, + + // ---------- + // Function: update + // Updates/inserts the given entry as the most recently used one in the list. + update: function MRUList_update(entry) { + this.remove(entry); + this._list.unshift(entry); + }, + + // ---------- + // Function: remove + // Removes the given entry from the list. + remove: function MRUList_remove(entry) { + let index = this._list.indexOf(entry); + if (index > -1) + this._list.splice(index, 1); + }, + + // ---------- + // Function: peek + // Returns the most recently used entry. If a filter exists, gets the most + // recently used entry which matches the filter. + peek: function MRUList_peek(filter) { + let match = null; + if (filter && typeof filter == "function") + this._list.some(function MRUList_peek_getEntry(entry) { + if (filter(entry)) { + match = entry + return true; + } + return false; + }); + else + match = this._list.length > 0 ? this._list[0] : null; + + return match; + }, +}; +