trackEditorWidgetAlbumArtwork.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 if (typeof(Ci) == "undefined")
28  var Ci = Components.interfaces;
29 if (typeof(Cc) == "undefined")
30  var Cc = Components.classes;
31 if (typeof(Cr) == "undefined")
32  var Cr = Components.results;
33 if (typeof(Cu) == "undefined")
34  var Cu = Components.utils;
35 
36 Cu.import("resource://app/jsmodules/ArrayConverter.jsm");
37 Cu.import("resource://app/jsmodules/sbCoverHelper.jsm");
38 Cu.import("resource://app/jsmodules/SBJobUtils.jsm");
39 Cu.import("resource://app/jsmodules/sbLibraryUtils.jsm");
40 Cu.import("resource://app/jsmodules/sbProperties.jsm");
41 Cu.import("resource://app/jsmodules/StringUtils.jsm");
42 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
43 
44 const ARTWORK_NO_COVER = "chrome://songbird/skin/album-art/drop-target.png";
45 
46 /******************************************************************************
47  *
48  * \class TrackEditorArtwork
49  * \brief Extends TrackEditorInputWidget to add an artwork editor
50  *
51  * Binds the given image element
52  *
53  *****************************************************************************/
55 
56  TrackEditorInputWidget.call(this, element);
57 
58  this._replaceLabel = SBString("trackeditor.artwork.replace");
59  this._addLabel = SBString("trackeditor.artwork.add");
60  this._createButton();
61  this._createDragOverlay();
62  this._createContextMenu();
63 
64  var self = this;
65  this._elementStack.addEventListener("click",
66  function(evt) { self.onClick(evt); }, false);
67  this._elementStack.addEventListener("keypress",
68  function(evt) { self.onKeyPress(evt); }, false);
69  this._elementStack.addEventListener("contextmenu",
70  function(evt) { self.onContextMenu(evt); }, false);
71  // Drag and drop for the album art image
72  // We need to have over in order to get the getSupportedFlavours called when
73  // the user drags an item over us.
74  this._elementStack.addEventListener("dragover",
75  function(evt) { nsDragAndDrop.dragOver(evt, self); }, false);
76  this._elementStack.addEventListener("dragdrop",
77  function(evt) { nsDragAndDrop.drop(evt, self); }, false);
78  this._elementStack.addEventListener("draggesture",
79  function(evt) { nsDragAndDrop.startDrag(evt, self); }, false);
80 }
81 TrackEditorArtwork.prototype = {
82  __proto__: TrackEditorInputWidget.prototype,
83  _button: null,
84  _replaceLabel: null,
85  _addLabel: null,
86  _dragoverlay: null,
87  _elementStack: null,
88  // Menu popup and items
89  _menuPopup: null,
90  _menuCut: null,
91  _menuCopy: null,
92  _menuPaste: null,
93  _menuClear: null,
94 
99  _imageSrcChange: function TrackEditorArtwork__imageSrcChange(newValue) {
100  var oldValue = TrackEditor.state.getPropertyValue(this.property);
101 
102  if (newValue != oldValue) {
103 
104  // This will call onTrackEditorPropertyChange
105  TrackEditor.state.setPropertyValue(this.property, newValue);
106 
107  // Auto-enable property write-back
108  if (!TrackEditor.state.isPropertyEnabled(this.property)) {
109  TrackEditor.state.setPropertyEnabled(this.property, true);
110  }
111  }
112  },
113 
118  _createButton: function TrackEditorArtwork__createButton() {
119  this._button = document.createElement("button");
120  var vbox = document.createElement("vbox");
121  this._element.parentNode.replaceChild(vbox, this._element);
122 
123  // In order for tabbing to work in the desired order
124  // we need to apply tabindex to all elements.
125  if (this._element.hasAttribute("tabindex")) {
126  var value = parseInt(this._element.getAttribute("tabindex")) + 1;
127  this._button.setAttribute("tabindex", value);
128  }
129 
130  var self = this;
131  this._button.addEventListener("command",
132  function() { self.onButtonCommand(); }, false);
133 
134  vbox.appendChild(this._element);
135  vbox.appendChild(this._button);
136  },
137 
142  _createDragOverlay: function TrackEditorArtwork__createDragOverlay() {
143  // Outter stack for overlaying the label on the image
144  this._elementStack = document.createElement("stack");
145  this._elementStack.setAttribute("class", "art");
146  if (this._element.hasAttribute("tabindex")) {
147  this._elementStack.setAttribute("tabindex",
148  this._element.getAttribute("tabindex"));
149  this._element.removeAttribute("tabindex");
150  }
151  this._element.parentNode.replaceChild(this._elementStack, this._element);
152 
153  // Label for Drag here text
154  var dragLabel = document.createElement("label");
155  dragLabel.setAttribute("class", "drop-message");
156 
157  // Text value to be added to the label as a child
158  var dragLabelValue
159  dragLabelValue = document.createTextNode(SBString("trackeditor.artwork.drag"));
160  dragLabel.appendChild(dragLabelValue);
161 
162  // Create a wrapper box around the image for the stack
163  var imageVBox = document.createElement("vbox");
164  imageVBox.setAttribute("class", "artWrapperBox");
165  imageVBox.appendChild(this._element);
166 
167  // Create a wrapper box around the label for the stack
168  this._dragoverlay = document.createElement("vbox");
169  this._dragoverlay.setAttribute("class", "artWrapperBox");
170  this._dragoverlay.appendChild(dragLabel);
171 
172  // Append the two boxes
173  this._elementStack.appendChild(imageVBox);
174  this._elementStack.appendChild(this._dragoverlay)
175  },
176 
181  _createContextMenu: function TrackEditorArtwork__createContextMenu() {
182  this._menuPopup = document.createElement("menupopup");
183  this._menuCut = document.createElement("menuitem");
184  this._menuCopy = document.createElement("menuitem");
185  this._menuPaste = document.createElement("menuitem");
186  this._menuClear = document.createElement("menuitem");
187  var menuSeparatorPaste = document.createElement("menuseparator");
188 
189  var self = this;
190  this._menuCut.setAttribute("label", SBString("trackeditor.artwork.menu.cut"));
191  this._menuCut.addEventListener("command",
192  function() { self.onCut();}, false);
193 
194  this._menuCopy.setAttribute("label", SBString("trackeditor.artwork.menu.copy"));
195  this._menuCopy.addEventListener("command",
196  function() { self.onCopy();}, false);
197 
198  this._menuPaste.setAttribute("label", SBString("trackeditor.artwork.menu.paste"));
199  this._menuPaste.addEventListener("command",
200  function() { self.onPaste();}, false);
201 
202  this._menuClear.setAttribute("label", SBString("trackeditor.artwork.menu.clear"));
203  this._menuClear.addEventListener("command",
204  function() { self.onClear();}, false);
205  this._menuPopup.appendChild(this._menuCut);
206  this._menuPopup.appendChild(this._menuCopy);
207  this._menuPopup.appendChild(this._menuPaste);
208  this._menuPopup.appendChild(menuSeparatorPaste);
209  this._menuPopup.appendChild(this._menuClear);
210 
211  this._menuPopup.addEventListener("popupshowing",
212  function(evt) { self.onPopupShowing(evt); }, false);
213 
214  this._element.parentNode.appendChild(this._menuPopup);
215  },
216 
220  onContextMenu: function TrackEditorArtwork_onContextMenu(aEvent) {
221  // Make sure we are focused (could be a right-click that fired this)
222  this._elementStack.focus();
223 
224  // Default to assuming we are invoked by alternative methods to right-click
225  var xPos = 0; // No position needed
226  var yPos = 0;
227  var anchor = "after_start"; // Anchor to the bottom left
228  var anchor_element = this._element; // Anchor to the element
229 
230  // Check if it was a right-click
231  if (aEvent.button == 2) {
232  // Since we were invoked by mouse we do not anchor the menu
233  anchor_element = null;
234  anchor = "";
235  xPos = aEvent.clientX;
236  yPos = aEvent.clientY;
237  }
238 
239  this._menuPopup.openPopup(anchor_element, // Anchor to the art work box
240  anchor, // position it on the bottom
241  xPos, // x position/offset of menu
242  yPos, // y position/offset of menu
243  true, // context menu
244  false); // no attributes override
245  },
246 
252  onPopupShowing: function TrackEditorArtwork_onPopupShowing(aEvent) {
253  var curImageUrl = TrackEditor.state.getPropertyValue(this.property);
254 
255  // Get the clipboard image.
256  var sbClipboard = Cc["@songbirdnest.com/moz/clipboard/helper;1"]
257  .createInstance(Ci.sbIClipboardHelper);
258  var mimeType = {};
259  var imageData = sbClipboard.copyImageFromClipboard(mimeType, {});
260  mimeType = mimeType.value;
261 
262  // Validate image as valid album art.
263  var isValidAlbumArt = false;
264  if (imageData && (imageData.length > 0)) {
265  var artService = Cc["@songbirdnest.com/Songbird/album-art-service;1"]
266  .getService(Ci.sbIAlbumArtService);
267  isValidAlbumArt = artService.imageIsValidAlbumArt(mimeType,
268  imageData,
269  imageData.length);
270  }
271 
272  if (!curImageUrl || curImageUrl == ARTWORK_NO_COVER) {
273  this._menuCut.setAttribute("disabled", true);
274  this._menuCopy.setAttribute("disabled", true);
275  this._menuClear.setAttribute("disabled", true);
276  } else {
277  this._menuCut.removeAttribute("disabled");
278  this._menuCopy.removeAttribute("disabled");
279  this._menuClear.removeAttribute("disabled");
280  }
281 
282  if (!isValidAlbumArt) {
283  this._menuPaste.setAttribute("disabled", true);
284  } else {
285  this._menuPaste.removeAttribute("disabled");
286  }
287 
288  // Disable everything except copy for readonly items.
289  if (TrackEditor.state.isDisabled) {
290  this._menuCut.setAttribute("disabled", true);
291  this._menuClear.setAttribute("disabled", true);
292  this._menuPaste.setAttribute("disabled", true);
293  }
294  },
295 
299  onClick: function TrackEditorArtwork_onClick(aEvent) {
300  // Focus the element so we can respond to context menus and commands
301  this._elementStack.focus();
302  },
303 
308  onKeyPress: function TrackEditorArtwork_onKeyPress(aEvent) {
309  var validMetaKeys = false;
310 
311  if (aEvent.keyCode == 46 || aEvent.keyCode == 8) {
312  this.onClear();
313  }
314 
315  if (getPlatformString() == "Darwin") {
316  // Mac (Uses cmd)
317  validMetaKeys = (aEvent.metaKey && !aEvent.ctrlKey && !aEvent.altKey);
318  } else {
319  // Windows, Linux (Use ctrl)
320  validMetaKeys = (!aEvent.metaKey && aEvent.ctrlKey && !aEvent.altKey);
321  }
322 
323  if (validMetaKeys) {
324  switch (aEvent.charCode) {
325  case 120: // CUT
326  // Relies on copy
327  if (!TrackEditor.state.isDisabled)
328  this.onCut();
329  break;
330  case 99: // COPY
331  this.onCopy();
332  break;
333  case 118: // PASTE
334  if (!TrackEditor.state.isDisabled)
335  this.onPaste();
336  break;
337  }
338  }
339  },
340 
345  onPaste: function TrackEditorArtwork_onPaste() {
346  var sbClipboard = Cc["@songbirdnest.com/moz/clipboard/helper;1"]
347  .createInstance(Ci.sbIClipboardHelper);
348  var mimeType = {};
349  var imageData = sbClipboard.copyImageFromClipboard(mimeType, {});
350  if (sbCoverHelper.isImageSizeValid(null, imageData.length)) {
351 
352  var artService =
353  Cc["@songbirdnest.com/Songbird/album-art-service;1"]
354  .getService(Ci.sbIAlbumArtService);
355 
356  var newURI = artService.cacheImage(mimeType.value,
357  imageData,
358  imageData.length);
359  if (newURI) {
360  this._imageSrcChange(newURI.spec);
361  }
362  }
363  },
364 
369  onCopy: function TrackEditorArtwork_onCopy() {
370  var sbClipboard = Cc["@songbirdnest.com/moz/clipboard/helper;1"]
371  .createInstance(Ci.sbIClipboardHelper);
372 
373  // Load up the file (Properties are stored as URL Strings)
374  var imageFilePath = TrackEditor.state.getPropertyValue(this.property);
375  var ioService = Cc["@mozilla.org/network/io-service;1"]
376  .getService(Ci.nsIIOService);
377 
378  // Convert the URL to a URI
379  var imageURI = null;
380  try {
381  imageURI = ioService.newURI(imageFilePath, null, null);
382  } catch (err) {
383  Cu.reportError("trackEditor: Unable to convert to URI: [" +
384  imageFilePath + "] - " + err);
385  return false;
386  }
387 
388  // Check if this is a local file
389  if (!(imageURI instanceof Ci.nsIFileURL)) {
390  Cu.reportError("trackEditor: Not a local file [" +
391  imageFilePath + "]");
392  return false;
393  }
394 
395  imageFile = imageURI.file; // The .file is a nsIFile
396  var imageData;
397  var mimetype;
398  [imageData, mimeType] = sbCoverHelper.readImageData(imageFile);
399 
400  try {
401  sbClipboard.pasteImageToClipboard(mimeType,
402  imageData,
403  imageData.length);
404  } catch (err) {
405  Cu.reportError("trackEditor: Unable to copy from clipboard - " + err);
406  return false;
407  }
408 
409  return true;
410  },
411 
416  onCut: function TrackEditorArtwork_onCut() {
417  if (this.onCopy()) {
418  this.onClear();
419  }
420  },
421 
425  onClear: function TrackEditorArtwork_onClear() {
426  this._imageSrcChange("");
427  },
428 
432  getSupportedFlavours : function TrackEditorArtwork_getSupportedFlavours() {
433  var flavours = new FlavourSet();
434  return sbCoverHelper.getFlavours(flavours);
435  },
436 
437  onDragOver: function(event, flavour, session) {
438  // No need to do anything here, for UI we should set the
439  // .art:-moz-drag-over style.
440  },
441 
442  onDrop: function TrackEditorArtwork_onDrop(aEvent, aDropData, aSession) {
443  if (TrackEditor.state.isDisabled) {
444  return;
445  }
446  var self = this;
447  sbCoverHelper.handleDrop(function (newFile) {
448  if (newFile) {
449  self._imageSrcChange(newFile);
450  }
451  }, aDropData);
452  },
453 
454  onDragStart: function TrackEditorArtwork_onDragStart(aEvent,
455  aTransferData,
456  aAction) {
457  var imageURL = TrackEditor.state.getPropertyValue(this.property);
458  aTransferData.data = new TransferData();
459  sbCoverHelper.setupDragTransferData(aTransferData, imageURL);
460  },
461 
462 
467  onButtonCommand: function TrackEditorArtwork_onButtonCommand() {
468  this._elementStack.focus();
469 
470  // Open the file picker
471  var filePicker = Cc["@mozilla.org/filepicker;1"]
472  .createInstance(Ci.nsIFilePicker);
473  var windowTitle = SBString("trackeditor.filepicker.title");
474  filePicker.init( window, windowTitle, Ci.nsIFilePicker.modeOpen);
475  filePicker.appendFilters(Ci.nsIFilePicker.filterImages);
476  var fileResult = filePicker.show();
477  if (fileResult == Ci.nsIFilePicker.returnOK) {
478  var ioService = Cc["@mozilla.org/network/io-service;1"]
479  .getService(Ci.nsIIOService);
480  var fileURL = ioService.newFileURI(filePicker.file).spec;
481  if (sbCoverHelper.isImageSizeValid(fileURL)) {
482  this._imageSrcChange(fileURL);
483  }
484  }
485  },
486 
487  onTrackEditorPropertyChange: function TrackEditorArtwork_onTrackEditorPropertyChange() {
488  var value = TrackEditor.state.getPropertyValue(this.property);
489 
490  var XLINK_NS = "http://www.w3.org/1999/xlink";
491  var SVG_NS = "http://www.w3.org/2000/svg";
492  // The SVG image is somewhat unique in that it requires an SVG element to wrap it.
493  // Thus, we put the property on the wrapper, and reach in to grab the image and set its
494  // href from the outside.
495  var imageElement = this._element.getElementsByTagNameNS(SVG_NS, "image")[0];
496  if (value && value == imageElement.getAttributeNS(XLINK_NS, "href")) {
497  // Nothing has changed so leave it as is.
498  return;
499  }
500 
501  // Check if we have multiple values for this property
502  var allMatch = true;
503  if (TrackEditor.state.selectedItems.length > 1) {
504  allMatch = !TrackEditor.state.hasMultipleValuesForProperty(this.property);
505  }
506 
507  // Check if we can edit all the items in the list
508  var canEdit = (TrackEditor.state.writableItemCount ==
509  TrackEditor.state.selectedItems.length);
510 
511  // check if we should hide the drag here label
512  if (value && value != "") {
513  // There is an image so hide the label
514  this._dragoverlay.hidden = true;
515  } else {
516  this._dragoverlay.hidden = false;
517  }
518 
519  // Lets check if this item is missing a cover
520  if( (!value || value == "") && allMatch ) {
521  value = ARTWORK_NO_COVER;
522  }
523 
524  // Update the button to the correct text
525  this._button.label = (value == ARTWORK_NO_COVER ? this._addLabel : this._replaceLabel);
526 
527  // Update the image depending on if we have multiple items or not.
528  imageElement.setAttributeNS(XLINK_NS, "href", (allMatch ? value : ""));
529 
530  // Indicate if this property has been edited
531  if (TrackEditor.state.isPropertyEdited(this.property)) {
532  this._elementStack.setAttribute("edited", true);
533  } else {
534  this._elementStack.removeAttribute("edited");
535  }
536 
537  // update the check box to indicate we haved edited the property
538  this._checkbox.checked = TrackEditor.state.isPropertyEdited(this.property);
539 
540  // Toggle the "Add..." button depending on the read-only state.
541  if (TrackEditor.state.isDisabled) {
542  this._button.setAttribute("disabled", "true");
543  }
544  else {
545  this._button.removeAttribute("disabled");
546  }
547  }
548 }
549 
const Cu
const ARTWORK_NO_COVER
const Cc
var TrackEditor
Definition: trackEditor.js:52
function getPlatformString()
Get the name of the platform we are running on.
dndDefaultHandler_module onDragOver
var event
var ioService
function SBString(aKey, aDefault, aStringBundle)
Definition: StringUtils.jsm:93
let window
return null
Definition: FeedWriter.js:1143
function newURI(aURLString)
countRef value
Definition: FeedWriter.js:1423
oState session
const Cr
const Ci
function TrackEditorArtwork(element)