balloonTip.js
Go to the documentation of this file.
1 /*
2 //
3 // BEGIN SONGBIRD GPL
4 //
5 // This file is part of the Songbird web player.
6 //
7 // Copyright(c) 2005-2008 POTI, Inc.
8 // http://songbirdnest.com
9 //
10 // This file may be licensed under the terms of of the
11 // GNU General Public License Version 2 (the "GPL").
12 //
13 // Software distributed under the License is distributed
14 // on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either
15 // express or implied. See the GPL for the specific language
16 // governing rights and limitations.
17 //
18 // You should have received a copy of the GPL along with this
19 // program. If not, go to http://www.gnu.org/licenses/gpl.html
20 // or write to the Free Software Foundation, Inc.,
21 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 //
23 // END SONGBIRD GPL
24 //
25  */
26 
27 
30 
31 function BalloonTip() {
32 }
33 
34 BalloonTip.prototype.constructor = BalloonTip;
35 
36 BalloonTip.prototype = {
37 
38  anchorPosition: BALLOONTIP_OUTSIDE,
39  autoCloseTimeout: 0,
40  autoCloseOnClick: true,
41  onCloseCallback: null,
42  onInitCallback: null,
43  onClickCallback: null,
44 
45  // Show a balloontip with simple text content
46  //
47  // Sample usage :
48  //
49  // var tip = new BalloonTip;
50  // tip.autoCloseTimeout = 10; // automatically closes the tip after 10 seconds if it is no longer focused (default = 0 = do not autoclose)
51  // tip.autoCloseOnClick = false; // automatically closes the tip when its content is clicked (default = true)
52  // tip.onCloseCallback = myclosecallbackfunction; // get a callback when the tip is closed (default = no callback), params = (tipInstance, checkboxElement)
53  // tip.onInitCallback = myinitcallbackfunction; // get a callback when the tip is inited (default = no callback), params = (tipInstance, checkboxElement)
54  // tip.onClickCallback = myclickcallbackfunction; // get a callback when the tip is clicked (default = no callback), params = (tipInstance, event)
55  // tip.anchorPosition = BALLOONTIP_INSIDE; // anchor tip inside the element, or BALLOONTIP_OUTSIDE to anchor on its side (default = outside)
56  //
57  // tip.showText('Here is a helpful tip.', // autowraps if a width is specified
58  // document.getElementById('some_element_id'), // anchor element
59  // 'Here is something you should know', // title (if none specified, no title is displayed)
60  // 'balloon-icon-songbird', // icon class (if none specified, no icon is displayed)
61  // 'Do not show this again', // text to use on the checkbox (if none specified, checkbox is not visible)
62  // 200); // width (if -1 specified, text is on a single line, if none specified, wraps at 300px)
63 
64  showText: function(aText,
65  aAnchorElement,
66  aTitle,
67  aTitleImageClass,
68  aCheckboxLabel,
69  aWidth) {
70 
71  var width_val;
72  var width_attr;
73  if (aWidth == -1) { width_val = null; width_val = null; }
74  else if (!aWidth) { width_val = '300'; width_attr = 'width'; }
75  else { width_val = aWidth; width_attr = 'width'; }
76 
77  this.showContent('chrome://songbird/content/bindings/balloon.xml#balloon-text',
78  aAnchorElement,
79  aTitle,
80  aTitleImageClass,
81  aCheckboxLabel,
82  ['value', width_attr],
83  [aText, width_val]);
84  },
85 
86  // Show a balloontip with arbitrary content
87  //
88  // Sample usage :
89  //
90  // var tip = new BalloonTip;
91  // tip.autoCloseTimeout = 10; // automatically closes the tip after 10 seconds if it is no longer focused (default = 0 = do not autoclose)
92  // tip.autoCloseOnClick = false; // automatically closes the tip when its content is clicked (default = true)
93  // tip.onCloseCallback = myclosecallbackfunction; // get a callback when the tip is closed (default = no callback), params = (tipInstance, checkboxElement)
94  // tip.onInitCallback = myinitcallbackfunction; // get a callback when the tip is inited (default = no callback), params = (tipInstance, checkboxElement)
95  // tip.onClickCallback = myclickcallbackfunction; // get a callback when the tip is clicked (default = no callback), params = (tipInstance, event)
96  // tip.anchorPosition = BALLOONTIP_INSIDE; // anchor tip inside the element, or BALLOONTIP_OUTSIDE to anchor on its side (default = outside)
97  //
98  // tip.showContent('chrome://songbird/content/bindings/balloon.xml#balloon-text', // a binding URL, or an element tag name
99  // document.getElementById('some_element_id'), // anchor element
100  // 'Here is something you should know', // title (if none specified, no title is displayed)
101  // 'balloon-icon-songbird', // icon class (if none specified, no icon is displayed)
102  // 'Do not show this again', // text to use on the checkbox (if none specified, checkbox is not visible)
103  // ['value', 'width'], // attributes to forward to the binding element (or null)
104  // ['Here is a helpful tip.'], '200'); // values of the attributes to forward to the binding element (or null)
105 
106  showContent: function(aBindingURL, // may also be a simple element name (ie, "label")
107  aAnchorElement,
108  aTitle,
109  aTitleImageClass,
110  aCheckboxLabel,
111  aBindingAttribute,
112  aBindingAttributeValue) {
113  // Init members,
114  if (!aAnchorElement) aAnchorElement = document.documentElement; // no anchor element ? use document, i guess ...
115  this.bindingUrl = aBindingURL;
116  this.bindingAttribute = aBindingAttribute;
117  this.bindingAttributeValue = aBindingAttributeValue;
118  this.titleImageClass = aTitleImageClass;
119  this.titleValue = aTitle;
120  this.anchorElement = aAnchorElement;
121  this.originalDocument = document;
122  this.originalWindow = window;
123  this.checkboxLabel = aCheckboxLabel;
124  // 'alwaysRaised' does not work on Linux or Mac, only use it on Windows.
125  // 'popup' makes the window always on top on linux, which is the next best
126  // way of ensuring that the tip window stays on top (although it makes it
127  // stay on top of other apps as well).
128  // Neither of those two flags work on the Mac, so we use yet another method
129  // (see computePositionAndOrientation function , since that method can only
130  // work after the window has been initialized)
131  var raisedflag;
132  switch (getPlatformString()) {
133  case "Darwin": raisedflag = ""; break;
134  default: raisedflag = ",alwaysRaised"; break;
135  }
136  // Open the window (cloaked)
137  this.tipWindow = window.openDialog("chrome://songbird/content/xul/balloonTip.xul", "_blank", "chrome,modal=no,titlebar=no,resizable=no"+raisedflag, this);
138  this.initTimeStamp = new Date().getTime();
139  if (this.autoCloseTimeout) setTimeout( function(obj) { obj.onAutoCloseTimeout(); }, this.autoCloseTimeout * 1000, this );
140  },
141 
142  bindingUrl: null,
143  bindingAttribute: null,
144  bindingAttributeValue: null,
145  titleImageClass: null,
146  titleValue: null,
147  checkboxLabel: null,
148  checkboxElement: null,
149 
150  tipWindow : null,
151  anchorElement: null,
152  originalDocument: null,
153  originalWindow: null,
154  gotmetrics: false,
155 
156  doneUncloak: false,
157  lastW: -1,
158  lastH: -1,
159  lastX: -32768,
160  lastY: -32768,
161 
162  autoCloseTimeoutElapsed: function() {
163  var now = new Date().getTime();
164  var diff = (now - this.initTimeStamp)/1000;
165  return diff > this.autoCloseTimeout;
166  },
167 
168  onAutoCloseTimeout: function() {
169  if (this.focused) return;
170  this.closeTip();
171  },
172 
173  onTipBlur: function() {
174  this.focused = false;
175  if (!this.autoCloseTimeout) return;
176  if (this.autoCloseTimeoutElapsed()) this.closeTip();
177  },
178 
179  onTipFocus: function() {
180  this.focused = true;
181  },
182 
183  onTipClick: function(clickEvent) {
184  if (this.onClickCallback) this.onClickCallback(this, clickEvent);
185  if (this.autoCloseOnClick) this.closeTip();
186  },
187 
188  // Closed the balloontip
189  closeTip: function() {
190  if (!this.tipWindow) return;
191  // Optional user callback
192  if (this.onCloseCallback) this.onCloseCallback(this, this.checkboxElement);
193  // Cleanup
194  this.tipWindow.document.removeEventListener("focus", this.onfocus, true);
195  this.tipWindow.document.removeEventListener("blur", this.onblur, true);
196  this.tipWindow.document.removeEventListener("click", this.onclick, true);
197  var wnd = this.tipWindow;
198  this.tipWindow = null;
199  wnd.close();
200  },
201 
202  // The balloontip window has been created, but is not initialized yet
203  onCreateTip: function(aWindow) {
204  // Cloak the window as it is created, because we need to wait until it has been initialized
205  // before we can know its size, and therefore, calculate its position. Since we do not
206  // want to see the window pop up and then being moved, we cloak it first, then position
207  // it, and then we uncloak it (see computePositionAndOrientation)
208  var windowCloak =
209  Components.classes["@songbirdnest.com/Songbird/WindowCloak;1"]
210  .getService(Components.interfaces.sbIWindowCloak);
211  windowCloak.cloak(aWindow);
212  },
213 
214  // Compute intersection of two rectangles
215  intersectRect: function(aRectA, aRectB, aIntersection) {
216  aIntersection.x = Math.max(aRectA.x, aRectB.x);
217  aIntersection.y = Math.max(aRectA.y, aRectB.y);
218  aIntersection.w = Math.min(aRectA.x + aRectA.w, aRectB.x + aRectB.w) - aIntersection.x;
219  aIntersection.h = Math.min(aRectA.y + aRectA.h, aRectB.y + aRectB.h) - aIntersection.y;
220 
221  if (!(aIntersection.x < aIntersection.x + aIntersection.w && aIntersection.y < aIntersection.y + aIntersection.h)) {
222  aIntersection.x = aIntersection.y = aIntersection.w = aIntersection.height = 0;
223  return 0;
224  } else {
225  return 1;
226  }
227  },
228 
229  getXULWindowFromWindow: function(win) {
230  var rv;
231  try {
232  var requestor = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
233  var nav = requestor.getInterface(Components.interfaces.nsIWebNavigation);
234  var dsti = nav.QueryInterface(Components.interfaces.nsIDocShellTreeItem);
235  var owner = dsti.treeOwner;
236  requestor = owner.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
237  rv = requestor.getInterface(Components.interfaces.nsIXULWindow);
238  }
239  catch (ex) {
240  rv = null;
241  }
242  return rv;
243  },
244 
245  // Compute the optimal position and orientation of the balloontip window for the requested anchor
246  // Note: at this stage, the window is cloaked. We uncloak it when we're done moving and resizing it.
247  computePositionAndOrientation: function() {
248  try {
249  // If the tip has gone away, abort
250  if (!this.tipWindow) return;
251 
252  if (!this.gotmetrics) {
253  // We may have been called back before the window was initialized, wait some more
254  if (this.tipWindow.document.documentElement.boxObject.width == 0 ||
255  this.tipWindow.document.documentElement.boxObject.height == 0) {
256  setTimeout(function(obj) { obj.computePositionAndOrientation(); }, 0, this);
257  return;
258  }
259 
260  // Optional user callback on init
261  if (this.onInitCallback) this.onInitCallback(this, this.checkboxElement);
262 
263  // Get the tip window's size
264  this._tipX = this.tipWindow.document.documentElement.boxObject.screenX;
265  this._tipY = this.tipWindow.document.documentElement.boxObject.screenY;
266  this._tipWidth = this.tipWindow.document.documentElement.boxObject.width;
267  this._tipHeight = this.tipWindow.document.documentElement.boxObject.height;
268 
269  this.onblur = {
270  _that: null,
271  handleEvent: function( event ) { this._that.onTipBlur(); }
272  }; this.onblur._that = this;
273  this.tipWindow.document.addEventListener("blur", this.onblur, true);
274 
275  this.onfocus = {
276  _that: null,
277  handleEvent: function( event ) { this._that.onTipFocus(); }
278  }; this.onfocus._that = this;
279  this.tipWindow.document.addEventListener("focus", this.onfocus, true);
280 
281  this.onclick = {
282  _that: null,
283  handleEvent: function( event ) { this._that.onTipClick(); }
284  }; this.onclick._that = this;
285  this.tipWindow.document.addEventListener("click", this.onclick, true);
286 
287  if (getPlatformString() == "Darwin") {
288  // This works on MacOSX, but won't stick when another window gets dragged.
289  // There doesn't seem to be any better way though, because neither the
290  // alwaysRaised nor the popup flags work as advertised on the mac.
291  var xulwindow = this.getXULWindowFromWindow(this.tipWindow);
292  if (xulwindow) xulwindow.zLevel = Components.interfaces.nsIXULWindow.highestZ;
293  }
294 
295  // Read offsets from tip window
296  this._anchor_nw = this.tipWindow.document.getElementById("anchor-nw");
297  this._anchor_ne = this.tipWindow.document.getElementById("anchor-ne");
298  this._anchor_sw = this.tipWindow.document.getElementById("anchor-sw");
299  this._anchor_se = this.tipWindow.document.getElementById("anchor-se");
300  this._frame_n = this.tipWindow.document.getElementById("frame-n");
301  this._frame_s = this.tipWindow.document.getElementById("frame-s");
302 
303  // Note: as this stage, all four arrows are visible
304  this._offset_nw = this._anchor_nw.boxObject.screenX - this._tipX;
305  this._offset_ne = (this._tipX + this._tipWidth) - (this._anchor_ne.boxObject.screenX + this._anchor_ne.boxObject.width);
306  this._offset_sw = this._anchor_sw.boxObject.screenX - this._tipX;
307  this._offset_se = (this._tipX + this._tipWidth) - (this._anchor_se.boxObject.screenX + this._anchor_se.boxObject.width);
308  this._margin_n = this._frame_n.boxObject.screenY - this._tipY;
309  this._margin_s = (this._tipY + this._tipHeight) - (this._frame_s.boxObject.screenY + this._frame_s.boxObject.height);
310 
311  // don't re-read the metrics from the window, they'll have changed the next time this
312  // function is called, and we want to keep the original ones for our calculations
313  this.gotmetrics = true;
314  }
315 
316  // We'll record our best orientation and the corresponding position for the window here
317  var best = {
318  orientation: -1,
319  insideArea: -1,
320  x: 0,
321  y: 0,
322  w: 0,
323  h: 0
324  };
325 
326  // Try using the screen coordinates for the window that spawned the tip, and for the tooltip window (in case the original window is
327  // moved, these can be two different ones). Unfortunately this won't check existing monitors that offer a better position for the
328  // tip unless either the tipwindow or the window that spawned it are mostly in it. Ie, if both the tip and the window that spawned
329  // it are mostly in the same monitor, and there is a better possible position in a secondary monitor (both window are partially in
330  // that secondary monitor), then the better position will not be found.
331 
332  // The only way to fix this would be to enumerate all monitors, but there is no service to do that.
333 
334  var basewindows = [this.originalWindow, (this.tipWindow == this.originalWindow) ? null : this.tipWindow];
335 
336  for (var j in basewindows) {
337  if (!basewindows[j]) continue;
338 
339  // Get the screen size (multimonitor aware)
340  var screen = basewindows[j].QueryInterface(Components.interfaces.nsIDOMWindowInternal).screen;
341  if (!screen) continue;
342  var screenRect = {
343  x: screen.availLeft,
344  y: screen.availTop,
345  w: screen.availWidth,
346  h: screen.availHeight
347  };
348 
349 
350  // --- Calculate optimal position for the tooltip based on content, anchor position, and screen size
351 
352  // Cycle all 4 possible orientations (note, NW/NE/SW/SE refer to the direction of the arrow in the tipwindow.
353  // The direction of the window itself, relative to the anchor point, is opposite the arrow.)
354 
355  // These are in order of preference, if all orientations fit in the screen, we'll use SW
356  const ORIENT_SW = 0;
357  const ORIENT_SE = 1;
358  const ORIENT_NW = 2;
359  const ORIENT_NE = 3;
360 
361  var px, py; // anchor position on anchor element
362 
363  for (var i = 0; i <= 3; i++) {
364  // Calculate position of anchor on anchor element
365  if (this.anchorPosition == BALLOONTIP_OUTSIDE) {
366  // If anchored outside, stick to the top or bottom side, and position x coordinate halfway to the center (quarter width) depending on orientation
367  switch (i) {
368  case ORIENT_NW:
369  px = this.anchorElement.boxObject.screenX + (this.anchorElement.boxObject.width / 4);
370  py = this.anchorElement.boxObject.screenY + this.anchorElement.boxObject.height;
371  break;
372  case ORIENT_NE:
373  px = this.anchorElement.boxObject.screenX + (this.anchorElement.boxObject.width / 4)*3;
374  py = this.anchorElement.boxObject.screenY + this.anchorElement.boxObject.height;
375  break;
376  case ORIENT_SW:
377  px = this.anchorElement.boxObject.screenX + (this.anchorElement.boxObject.width / 4);
378  py = this.anchorElement.boxObject.screenY;
379  break;
380  case ORIENT_SE:
381  px = this.anchorElement.boxObject.screenX + (this.anchorElement.boxObject.width / 4)*3;
382  py = this.anchorElement.boxObject.screenY;
383  break;
384  }
385  } else {
386  // if anchored inside, use the center of the element
387  px = this.anchorElement.boxObject.screenX + (this.anchorElement.boxObject.width / 2);
388  py = this.anchorElement.boxObject.screenY + (this.anchorElement.boxObject.height / 2);
389  }
390 
391  // Add or subtract anchor offset, so that the balloontip's arrow points at the anchor position
392  switch (i) {
393  case ORIENT_NW: px -= this._offset_nw; break;
394  case ORIENT_NE: px += this._offset_ne; break;
395  case ORIENT_SW: px -= this._offset_sw; break;
396  case ORIENT_SE: px += this._offset_se; break;
397  }
398 
399  // Calculate actual position of the tip window for this orientation and anchor position
400  var wx, wy, wh = this._tipHeight, ww = this._tipWidth;
401  switch (i) {
402  case ORIENT_NW:
403  wx = px;
404  wy = py;
405  wh -= this._margin_s;
406  break;
407  case ORIENT_NE:
408  wx = px - this._tipWidth;
409  wy = py;
410  wh -= this._margin_s;
411  break;
412  case ORIENT_SW:
413  wx = px;
414  wy = py - this._tipHeight + this._margin_n;
415  wh -= this._margin_n;
416  break;
417  case ORIENT_SE:
418  wx = px - this._tipWidth;
419  wy = py - this._tipHeight + this._margin_n;
420  wh -= this._margin_n;
421  break;
422  }
423 
424  // Calculate area of the window that is going to be inside the screen if we use this orientation
425  var tipRect = {
426  x: wx,
427  y: wy,
428  w: ww,
429  h: wh
430  };
431 
432  var insideRect = { x: 0, y: 0, w: 0, h: 0};
433  this.intersectRect(tipRect, screenRect, insideRect);
434  var insideArea = insideRect.w * insideRect.h;
435 
436  // If this is the first calculation, or we have a better position, record it
437  if (best.insideArea == -1 || insideArea > best.insideArea) {
438  best.orientation = i;
439  best.insideArea = insideArea;
440  best.x = wx;
441  best.y = wy;
442  best.w = ww;
443  best.h = wh;
444  }
445  // Try the next orientation ...
446  }
447  }
448 
449  // Use the best orientation that we found
450 
451  // Show/hide appropriate arrows
452  switch (best.orientation) {
453  case ORIENT_NW:
454  this._anchor_nw.hidden = false;
455  this._anchor_ne.hidden = true;
456  this._anchor_sw.hidden = true;
457  this._anchor_se.hidden = true;
458  break;
459  case ORIENT_NE:
460  this._anchor_nw.hidden = true;
461  this._anchor_ne.hidden = false;
462  this._anchor_sw.hidden = true;
463  this._anchor_se.hidden = true;
464  break;
465  case ORIENT_SW:
466  this._anchor_nw.hidden = true;
467  this._anchor_ne.hidden = true;
468  this._anchor_sw.hidden = false;
469  this._anchor_se.hidden = true;
470  break;
471  case ORIENT_SE:
472  this._anchor_nw.hidden = true;
473  this._anchor_ne.hidden = true;
474  this._anchor_sw.hidden = true;
475  this._anchor_se.hidden = false;
476  break;
477  }
478 
479  if (this.lastW != best.w ||
480  this.lastH != best.h) {
481 
482  // Move the tip window to where it should be
483  this.tipWindow.resizeTo(best.w, best.h);
484 
485  this.lastW = best.w;
486  this.lastH = best.h;
487  }
488 
489  if (this.lastX != best.x ||
490  this.lastY != best.y) {
491 
492  // And resize it to what we computed (this is necessary because removing the unused arrows while keeping the window
493  // the same size makes the content bigger than it needs to be, so we have to shrink the window by the calculated
494  // offset in order for the content size to remain the same as what was automatically determined to be the best
495  // by ui engine)
496  this.tipWindow.moveTo(best.x, best.y);
497 
498  this.lastX = best.x;
499  this.lastY = best.y;
500  }
501 
502  if (!this.doneUncloak) {
503  // Now that the window is correctly positioned and sized, uncloak it
504  var windowCloak =
505  Components.classes["@songbirdnest.com/Songbird/WindowCloak;1"]
506  .getService(Components.interfaces.sbIWindowCloak);
507  windowCloak.uncloak(this.tipWindow);
508  this.doneUncloak = true;
509  }
510 
511  // All done, but follow the position of the anchor element on the screen.
512  setTimeout(function(obj) { obj.computePositionAndOrientation(); }, 100, this);
513  } catch (e) {
514  Components.utils.reportError(e);
515  }
516  }
517 
518 };
519 
520 
const BALLOONTIP_INSIDE
Definition: balloonTip.js:29
function getPlatformString()
Get the name of the platform we are running on.
restoreDimensions aWidth
var event
let window
const BALLOONTIP_OUTSIDE
Definition: balloonTip.js:28
function BalloonTip()
Definition: balloonTip.js:31
aWindow setTimeout(function(){_this.restoreHistory(aWindow, aTabs, aTabData, aIdMap);}, 0)
this _document this
Definition: FeedWriter.js:1085
return null
Definition: FeedWriter.js:1143
BogusChannel prototype owner
this _dialogInput this _pos[1] px
function now()
_getSelectedPageStyle s i