trackEditor.js
Go to the documentation of this file.
1 /*
2  *=BEGIN SONGBIRD GPL
3  *
4  * This file is part of the Songbird web player.
5  *
6  * Copyright(c) 2005-2010 POTI, Inc.
7  * http://www.songbirdnest.com
8  *
9  * This file may be licensed under the terms of of the
10  * GNU General Public License Version 2 (the ``GPL'').
11  *
12  * Software distributed under the License is distributed
13  * on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either
14  * express or implied. See the GPL for the specific language
15  * governing rights and limitations.
16  *
17  * You should have received a copy of the GPL along with this
18  * program. If not, go to http://www.gnu.org/licenses/gpl.html
19  * or write to the Free Software Foundation, Inc.,
20  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21  *
22  *=END SONGBIRD GPL
23  */
24 
25 if (typeof(Ci) == "undefined")
26  var Ci = Components.interfaces;
27 if (typeof(Cc) == "undefined")
28  var Cc = Components.classes;
29 if (typeof(Cr) == "undefined")
30  var Cr = Components.results;
31 if (typeof(Cu) == "undefined")
32  var Cu = Components.utils;
33 
34 Cu.import("resource://app/jsmodules/ArrayConverter.jsm");
35 Cu.import("resource://app/jsmodules/sbCoverHelper.jsm");
36 Cu.import("resource://app/jsmodules/SBJobUtils.jsm");
37 Cu.import("resource://app/jsmodules/sbLibraryUtils.jsm");
38 Cu.import("resource://app/jsmodules/sbMetadataUtils.jsm");
39 Cu.import("resource://app/jsmodules/sbProperties.jsm");
40 Cu.import("resource://app/jsmodules/StringUtils.jsm");
41 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
42 
43 /******************************************************************************
44  *
45  * \class TrackEditor
46  * \brief Base controller for track editor windows, including trackEditor.xul.
47  *
48  * Responsible for setting up default UI elements and maintaining the
49  * state object
50  *
51  *****************************************************************************/
52 var TrackEditor = {
53 
54  _propertyManager: Cc["@songbirdnest.com/Songbird/Properties/PropertyManager;1"]
55  .getService(Ci.sbIPropertyManager),
56 
57  // TrackEditorState object
58  state: null,
59 
60  // Hash of ID to widget objects
61  _elements: {},
62 
63  // TODO consolidate?
64  _browser: null,
65  mediaListView: null,
66 
70  onLoadTrackEditor: function TrackEditor_onLoadTrackEditor() {
71 
72  // Get the main window.
73  var windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]
74  .getService(Ci.nsIWindowMediator);
75 
76  var songbirdWindow = windowMediator.getMostRecentWindow("Songbird:Main");
77  this._browser = songbirdWindow.gBrowser;
78 
79  this.state = new TrackEditorState();
80 
81  // Prepare the UI
82  this._setUpDefaultWidgets();
83 
84  // note that this code USED to watch tabContent and selection and library
85  // changes, but we've implemented track editor as a modal dialog, so there's
86  // no need for any of that now. still, the names are there, so it can be
87  // brought back if the desire arises
88  this.onTabContentChange();
89 
90  var initialTab = window.arguments[0];
91  if (initialTab) {
92  var tabBox = document.getElementById("trackeditor-tabbox");
93  var tabs = tabBox.parentNode.getElementsByTagName("tab");
94  for each (var tab in tabs) {
95  if (tab.id == initialTab) {
96  tabBox.selectedTab = tab;
97  }
98  }
99  }
100  },
101 
105  _setUpDefaultWidgets: function TrackEditor__setUpDefaultWidgets() {
106 
107  // load the tabBox and individuals tabs
108  var tabBox = document.getElementById("trackeditor-tabbox");
109  var tabs = tabBox.tabs;
110 
111  // Add an additional layer of control to all elements with a property
112  // attribute. This will cause the elements to be updated when the
113  // selection model changes, and vice versa.
114 
115  this._elements["anonymous"] = []; // Keep elements without ids in a list
116 
117  // Potentially anonymous elements that need wrapping
118  var elements = document.getElementsByAttribute("property", "*");
119  for each (var element in elements) {
120  var wrappedElement = null;
121  if (element.tagName == "label" ||
122  (element.tagName == "textbox" && /\bplain\b/(element.className)))
123  {
124  var property = element.getAttribute("property");
125  var propertyInfo = this._propertyManager.getPropertyInfo(property);
126 
127  if (element.getAttribute("property-type") != "label" &&
128  propertyInfo.type == "uri") {
129  wrappedElement = new TrackEditorURILabel(element);
130  }
131  else if (element.getAttribute("property-type") != "label" &&
132  property == SBProperties.originPage) {
133  // FIXME: originPage should be a uri type, but isn't!
134  wrappedElement = new TrackEditorOriginLabel(element);
135  }
136  else {
137  wrappedElement = new TrackEditorLabel(element);
138  }
139  } else if (element.tagName == "textbox") {
140  wrappedElement = new TrackEditorTextbox(element);
141  } else if (element.tagName == "sb-rating") {
142  wrappedElement = new TrackEditorRating(element);
143  } else if (element.localName == "svg") {
144  // Setup a TrackEditorArtwork wrappedElement
145  wrappedElement = new TrackEditorArtwork(element);
146  }
147 
148  if (wrappedElement) {
149  if (element.id) {
150  this._elements[element.id] = wrappedElement;
151  } else {
152  this._elements["anonymous"].push(wrappedElement);
153  }
154  }
155  }
156 
157  // load the advanced tab
158  // get ngales property manager
159  var propMan = Cc["@songbirdnest.com/Songbird/Properties/PropertyManager;1"]
160  .getService(Ci.sbIPropertyManager);
161  // get all row elements from the advanced tab
162  var advancedRowItemsToLoad = document.getElementsByAttribute("class", "advTabRowElements");
163 
164  // get the menupopup element from the advanced tab
165  var advancedMenupopup = document.getElementById("menupopup");
166 
167  // Populate the advanced tab's menupopup
168  for (var i = 0; i < advancedRowItemsToLoad.length; i++){
169  var singleRowItem = advancedRowItemsToLoad[i];
170  var property = singleRowItem.getAttribute("property");
171  var propInfo = propMan.getPropertyInfo(property);
172  // create menuitems
173  var menuitem = document.createElement("menuitem");
174  menuitem.setAttribute("type", "checkbox");
175  menuitem.setAttribute("class", "advTabMenuItems");
176  menuitem.setAttribute("property", property);
177  menuitem.setAttribute("label", propInfo.displayName);
178  // stop the menuitem(s) from triggering the menupopup to close.
179  menuitem.setAttribute("closemenu", "none");
180  advancedMenupopup.appendChild(menuitem);
181  }
182 
183  // addEventListener to the menupopup
184  advancedMenupopup.addEventListener("popuphidden", function(){TrackEditor.onpopupclose()}, false);
185 
186  // Known elements we're going to want to use.
187  elements = ["notification_box", "notification_text",
188  "prev_button", "next_button", "infotab_trackname_label",
189  "ok_button", "cancel_button"];
190  // I'd love to do this in the previous loop, but getElementsByAttribute
191  // returns an HTMLCollection, not an array.
192  for each (var elementName in elements) {
193  var element = document.getElementById(elementName);
194  if (element) {
195  this._elements[elementName] = element;
196  }
197  }
198 
199  this._elements["notification_box"].hidden = true;
200 
201  var trackeditorTabs = document.getElementById("trackeditor-tabs");
202  var tabCount = 0;
203  for (var tab = trackeditorTabs.firstChild; tab; tab = tab.nextSibling) {
204  if (("hidden" in tab) && (!tab.hidden)) {
205  ++tabCount;
206  }
207  }
208  if (tabCount > 1) {
209  trackeditorTabs.hidden = false;
210  }
211 
212  // Monitor all changes in order to update the dialog controls
213  this.state.addPropertyListener("all", this);
214  },
215 
219  updateNotificationBox: function TrackEditor__updateNotificationBox() {
220 
221  var itemCount = this.state.selectedItems.length;
222  var writableCount = this.state.writableItemCount;
223 
224  var message;
225  var notificationClass = "dialog-notification notification-warning";
226 
227  if (itemCount > 1) {
228  if (writableCount == itemCount) {
229  message = SBFormattedString("trackeditor.notification.editingmultiple", [itemCount]);
230  notificationClass = "dialog-notification notification-info";
231  } else if (writableCount >= 1) {
232  message = SBFormattedString("trackeditor.notification.somereadonly",
233  [(itemCount - writableCount), itemCount]);
234  } else {
235  message = SBString("trackeditor.notification.multiplereadonly");
236  }
237  } else if (writableCount == 0) {
238  message = SBString("trackeditor.notification.singlereadonly");
239  }
240 
241  if (message) {
242  this._elements["notification_box"].className = notificationClass;
243  this._elements["notification_text"].textContent = message;
244  this._elements["notification_box"].hidden = false;
245  } else {
246  this._elements["notification_box"].hidden = true;
247  }
248  },
249 
253  updateControls: function TrackEditor__updateControls() {
254 
255  // If the user has entered invalid data, disable all
256  // UI that would cause an apply()
257  var hasErrors = this.state.isKnownInvalid();
258 
259  this._elements["ok_button"].disabled = hasErrors;
260 
261  // Disable next/prev at top/bottom of list, and when
262  // editing multiple items
263  var idx = this.mediaListView.selection.currentIndex;
264  var atStart = (idx == 0)
265  var atEnd = (idx == this.mediaListView.length - 1);
266  var hasMultiple = this.mediaListView.selection.count > 1;
267  this._elements["prev_button"].setAttribute("disabled",
268  (atStart || hasErrors || hasMultiple ? "true" : "false"));
269  this._elements["next_button"].setAttribute("disabled",
270  (atEnd || hasErrors || hasMultiple ? "true" : "false"));
271  },
272 
277  onTabContentChange: function TrackEditor_onTabContentChange() {
278  // We don't listen to nobody.
279  //if(this.mediaListView) {
280  //this.mediaListView.mediaList.removeListener(this);
281  //}
282 
283  this.mediaListView = this._browser.currentMediaListView;
284 
285  //this.mediaListView.mediaList.addListener(this);
286  //we're assuming a modal dialog for now, so don't reflect changes.
287 
288  this.onSelectionChanged();
289  },
290 
291  // TODO this isn't hooked up to anything!
292  // TODO should be fixed, as playback may modify metadata while
293  // we are editing it
294  /*
295  onItemUpdated: function(aMediaList, aMediaItem, aOldPropertiesArray) {
296  // we have to treat this just like a selection change
297  // so that multiple-selection values are correctly updated
298  // this might actually be a punch in the face because there are a
299  // a bunch of user-uneditable properties that change when playback starts
300  // hmm
301  for (var i = 0; i < aOldPropertiesArray.length; i++) {
302  // this is weak
303  var property = aOldPropertiesArray.getPropertyAt(i);
304  var propInfo = this._propertyManager.getPropertyInfo(property.id);
305 
306  //dump("Property change logged for "+aMediaItem.getProperty(SBProperties.trackName)+ ":\n"
307  // + property.value + "\n" + aMediaItem.getProperty(property.id)
308  // + "\neditable: " + propInfo.userEditable);
309  if (property.value != aMediaItem.getProperty(property.id) && propInfo.userEditable) {
310  this.onSelectionChanged();
311  return;
312  }
313  }
314  },*/
315 
319  onSelectionChanged: function TrackEditor_onSelectionChanged() {
320 
321  // get the new item's content type
322  var itemContentType = this.mediaListView.selection.currentMediaItem.contentType;
323 
324  if (itemContentType == "audio"){
325  // load the users audio preference
326  var userPreferenceProperty = JSON.parse(Application.prefs.getValue(
327  "songbird.trackeditor.audioAdvancedTags", ""));
328 
329  // update the advanced tab with the proper elements
330  TrackEditor.updateAdvancedTab(userPreferenceProperty);
331  }
332  else if (itemContentType == "video"){
333  // load the users audio preference
334  var userPreferenceProperty = JSON.parse(Application.prefs.getValue(
335  "songbird.trackeditor.videoAdvancedTags", ""));
336 
337  // update the advanced tab with the proper elements
338  TrackEditor.updateAdvancedTab(userPreferenceProperty);
339  }
340 
341  this.state.setSelection(this.mediaListView.selection);
342 
343  // Disable summary page if multiple items are selected.
344  // TODO summary page is commented out.
345  /*
346  var tabBox = document.getElementById("trackeditor-tabbox");
347  var tabs = tabBox.tabs;
348 
349  if (this.state.selectedItems.length > 1) {
350  // warning: assumes summary page is at tabs[0]
351  if (tabBox.selectedIndex == 0) {
352  tabBox.selectedIndex = 1;
353  }
354  tabs.getItemAtIndex(0).setAttribute("disabled", "true");
355  }
356  */
357 
358  this.updateNotificationBox();
359  this.updateControls();
360 
361  // Hacky special case to hide the title when editing multiple items.
362  // We assume you don't want to set multiple titles at once.
363  var hidden = this.state.selectedItems.length > 1;
364  this._elements["infotab_trackname_textbox"].hidden = hidden;
365  this._elements["infotab_trackname_label"].hidden = hidden;
366  },
367 
371  onTrackEditorPropertyChange: function TrackEditor_onTrackEditorPropertyChange(property) {
372  // Update dialog controls, since the validation state may have changed.
373  // TODO: this may hurt performance, since it is executed on every keystroke!
374  this.updateControls();
375  },
376 
381  onUnloadTrackEditor: function() {
382  // break the cycles
383  //this._browser.removeEventListener("TabContentChange", this, false);
384  this.mediaListView.selection.removeListener(this);
385 
386  //this.mediaListView.mediaList.removeListener(this);
387  //we're assuming a modal dialog for now, so don't reflect changes.
388 
389  this.mediaListView = null;
390  },
391 
396  next: function() {
397  if (!this.apply()) {
398  return;
399  }
400 
401  var idx = this.mediaListView.selection.currentIndex;
402 
403  if (idx == null || idx == undefined) { return; }
404 
405  idx = idx+1;
406  if (idx >= this.mediaListView.length) {
407  //idx = 0; // wrap around
408  idx = this.mediaListView.length - 1 // bump
409  }
410 
411  this.mediaListView.selection.selectOnly(idx);
412  // called explicitly since we aren't watching
413  this.onSelectionChanged();
414  },
415 
420  prev: function() {
421  if (!this.apply()) {
422  return;
423  }
424 
425  var idx = this.mediaListView.selection.currentIndex;
426 
427  if (idx == null || idx == undefined) { return; }
428 
429  idx = idx-1;
430  if (idx < 0) {
431  //idx = this.mediaListView.length - 1; // wrap around
432  idx = 0; // bump
433  }
434 
435  this.mediaListView.selection.selectOnly(idx);
436  // called explicitly since we aren't watching
437  this.onSelectionChanged();
438  },
439 
444  reset: function() {
445  // Reiniting with the existing selection will
446  // refresh all UI
447  onSelectionChanged();
448  },
449 
453  closeAndApply: function() {
454  if (this.apply()) {
455  window.close();
456  }
457  },
458 
463  apply: function() {
464 
465  if (this.state.isKnownInvalid()) {
466  Components.utils.reportError("TrackEditor: attempt to call apply() with known invalid state");
467  return false;
468  }
469 
470  var properties = this.state.getEnabledProperties();
471  var items = this.state.selectedItems;
472  if (items.length == 0 || properties.length == 0) {
473  return true;
474  }
475 
476  var enableRatingWrite = Application.prefs.getValue("songbird.metadata.ratings.enableWriting", false);
477 
478  // Properties we want to write
479  var writeProperties = [];
480 
481  // Apply each modified property back onto the selected items,
482  // keeping track of which items have been modified
483  var needsWriting = new Array(items.length);
484 
485  var batchSetter = {
486  runBatched: function(aUserData) {
487  for each (property in properties) {
488  if (!TrackEditor.state.isPropertyEdited(property)) {
489  continue;
490  }
491  // Add the property to our list so only the changed ones get written
492  writeProperties.push(property);
493 
494  for (var i = 0; i < items.length; i++) {
495  var value = TrackEditor.state.getPropertyValue(property);
496  var item = items[i];
497  // don't modify values for non-user-editable items (except rating)
498  if (!LibraryUtils.canEditMetadata(item)
499  && property != SBProperties.rating) {
500  continue;
501  }
502 
503  if (value != item.getProperty(property)) {
504  // Completely remove empty properties
505  // HACK for 0.7: primaryImageURL likes to be set to ""
506  // this says "i am scanned and empty"
507  // setting it to null says "rescan me"
508  if (value == "" && property != SBProperties.primaryImageURL) {
509  value = null;
510  }
511 
512  item.setProperty(property, value);
513 
514  // Flag the item as needing a metadata-write job.
515  // Do not start a write-job if all that has changed is the
516  // rating, and rating-write isn't enabled.
517  if (property != SBProperties.rating || enableRatingWrite) {
518  needsWriting[i] = true;
519  }
520  }
521  }
522  }
523  }
524  };
525 
526  this.mediaListView.mediaList.runInBatchMode(batchSetter, null);
527 
528  /* TODO: finish or nix this
529  // isPartOfCompilation gets special treatment because
530  // this is our only user-exposed boolean property right now
531  // TODO: generalize this to be more like the textboxes above
532  // boy, it would be really nice if it were really boolean instead of
533  // a 1/0 clamped number...
534  var property = SBProperties.isPartOfCompilation;
535  var compilation = document.getElementsByAttribute("property", property)[0];
536  if (compilation.checked) {
537  // go through the list setting properties and queuing items
538  var sIMI = this.mediaListView.selection.selectedIndexedMediaItems;
539  var j = 0;
540  while (sIMI.hasMoreElements()) {
541  j++;
542  var mI = sIMI.getNext()
543  .QueryInterface(Ci.sbIIndexedMediaItem)
544  .mediaItem;
545 
546  if (mI.getProperty(property) != tb.value) {
547  mI.setProperty(property, (tb.value ? "1" : "0"));
548  needsWriting[j] = true; // keep track of these suckers
549  }
550  }
551  }
552  */
553 
554  // Add all items that need writing into an array
555  var mediaItemArray = [];
556  for (var i = 0; i < items.length; i++) {
557  if (needsWriting[i] && LibraryUtils.canEditMetadata(items[i])) {
558  mediaItemArray.push(items[i]);
559  }
560  }
561  if (mediaItemArray.length > 0 && writeProperties.length > 0) {
562  sbMetadataUtils.writeMetadata(mediaItemArray,
563  writeProperties,
564  window,
565  this.mediaListView.mediaList);
566  }
567 
568  return true;
569  },
570 
579  onpopupclose: function() {
580  // get all menuitems from the advanced tab
581  var advancedMenuItems = document.getElementsByAttribute("class", "advTabMenuItems");
582  var checkedPropertyStrings = [];
583  for (var i = 0; i < advancedMenuItems.length; i++){
584  if (advancedMenuItems[i].getAttribute("checked", "true")){
585  var propertyString = advancedMenuItems[i].getAttribute("property");
586  checkedPropertyStrings.push(propertyString);
587  }
588  }
589 
590  // update the advanced tab
591  TrackEditor.updateAdvancedTab(checkedPropertyStrings);
592 
593  // get current items content type
594  var itemContentType = this._browser.currentMediaListView.selection.currentMediaItem.contentType;
595 
596  // write the list of property strings to the users preferences
597  if (itemContentType == "audio"){
598  Application.prefs.setValue("songbird.trackeditor.audioAdvancedTags",
599  JSON.stringify(checkedPropertyStrings));
600  }
601  else if (itemContentType == "video"){
602  Application.prefs.setValue("songbird.trackeditor.videoAdvancedTags",
603  JSON.stringify(checkedPropertyStrings));
604  }
605  },
606 
614  updateAdvancedTab: function(anArrayOfPropertyStrings){
615  // get all menuitems from the advanced tab
616  var advancedMenuItems = document.getElementsByAttribute("class", "advTabMenuItems");
617  // get all row elements from the advanced tab
618  var rowElementsAdvancedTab = document.getElementsByAttribute("class", "advTabRowElements");
619 
626  for (var i = 0; i < rowElementsAdvancedTab.length; i++){
627  var property = rowElementsAdvancedTab[i].getAttribute("property");
628  var matchStringProperty = anArrayOfPropertyStrings.indexOf(property,0);
629  if(matchStringProperty > -1){
630  rowElementsAdvancedTab[i].removeAttribute("hidden");
631  } else {
632  rowElementsAdvancedTab[i].setAttribute("hidden", true);
633  }
634  }
635 
642  for (var i = 0; i < advancedMenuItems.length; i++){
643  var property = advancedMenuItems[i].getAttribute("property");
644  var matchStringProperty = anArrayOfPropertyStrings.indexOf(property,0);
645  if(matchStringProperty > -1){
646  advancedMenuItems[i].setAttribute("checked", "true");
647  } else {
648  advancedMenuItems[i].removeAttribute("checked");
649  }
650  }
651  },
652 };
653 
654 
const Cu
function TrackEditorOriginLabel(element)
const Cc
var TrackEditor
Definition: trackEditor.js:52
var Application
Definition: sbAboutDRM.js:37
function SBFormattedString(aKey, aParams, aDefault, aStringBundle)
var tab
function SBString(aKey, aDefault, aStringBundle)
Definition: StringUtils.jsm:93
let window
var tabs
function TrackEditorState()
GstMessage * message
return ret concat apply([], ret)
return null
Definition: FeedWriter.js:1143
var sbMetadataUtils
countRef value
Definition: FeedWriter.js:1423
return aWindow document documentElement getAttribute(aAttribute)||dimension
const Cr
const Ci
var JSON
Javascript wrappers for common library tasks.
var hidden
var _browser
_getSelectedPageStyle s i
function next()
function TrackEditorArtwork(element)