trackEditorState.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 
45 /******************************************************************************
46  *
47  * \class TrackEditorState
48  * \brief Wraps an sbIMediaListViewSelection interface, adding accessors
49  * and listener support.
50  *
51  * This class maintains the state of the track editor. UI widgets read from,
52  * write to, and observe the instance of this class available at TrackEditor.state.
53  *
54  * Since the track editor has no knowledge of the widgets extension authors
55  * are free to customize the UI.
56  *
57  *****************************************************************************/
58 function TrackEditorState() {
59  this._propertyListeners = {};
60  this._selectionListeners = [];
61  this._properties = {};
62 }
63 TrackEditorState.prototype = {
64 
65  _propertyManager: Cc["@songbirdnest.com/Songbird/Properties/PropertyManager;1"]
66  .getService(Ci.sbIPropertyManager),
67 
68  // Array of media items that the track editor is operating on
69  _selectedItems: null,
70 
71  // Internal data structure
72  // Example:
73  // {
74  // propertyName: {
75  // hasMultiple: false, (multiple values between selected items)
76  // edited: false,
77  // enabled: false, (allowed to be saved)
78  // value: "",
79  // originalValue: "".
80  // knownInvalid: false (verified to be ivalid)
81  // }
82  // }
83  _properties: null,
84 
85  // Cached count of items with userEditable == true
86  _writableItemCount: null,
87 
88  // Map of properties to listener arrays
89  _propertyListeners: null,
90 
91  // Array of listeners to be notified only when the selection is reinited
92  _selectionListeners: null,
93 
98  setSelection: function TrackEditorState_setSelection(mediaListSelection) {
99  this._selectedItems = [];
100  var items = mediaListSelection.selectedIndexedMediaItems;
101  while (items.hasMoreElements()) {
102  var item = items.getNext()
103  .QueryInterface(Ci.sbIIndexedMediaItem)
104  .mediaItem;
105  this._selectedItems.push(item);
106  }
107 
108  this._properties = {};
109  this._writableItemCount = null;
110 
111  this._notifySelectionListeners();
112  this._notifyPropertyListeners();
113  },
114 
115 
119  get isDisabled() {
120  return this.writableItemCount == 0;
121  },
122 
126  get writableItemCount() {
127  if (this._writableItemCount == null) {
128  this._writableItemCount = 0;
129  if (this._selectedItems && this._selectedItems.length > 0) {
130 
131  // TODO do we want to handle 200,000 selected tracks?
132  for each (var item in this._selectedItems) {
133  if (LibraryUtils.canEditMetadata(item)) {
134  this._writableItemCount++;
135  }
136  }
137  }
138  }
139  return this._writableItemCount;
140  },
141 
146  get selectedItems() {
147  return this._selectedItems;
148  },
149 
153  getPropertyValue: function(property) {
154  this._ensurePropertyData(property);
155  return this._properties[property].value;
156  },
157 
162  resetPropertyValue: function(property) {
163  if (property in this._properties) {
164  delete this._properties[property];
165  }
166  this._ensurePropertyData(property);
167  this._notifyPropertyListeners(property);
168  },
169 
177  hasMultipleValuesForProperty: function(property) {
178  this._ensurePropertyData(property);
179  return this._properties[property].hasMultiple;
180  },
181 
186  setPropertyValue: function(property, value) {
187  this._ensurePropertyData(property);
188  this._properties[property].value = value;
189  this._properties[property].edited = true;
190 
191  // Multiple values no longer matter
192  this._properties[property].hasMultiple = false;
193 
194  // The property has changed, and may be valid.
195  // We don't want to validate on every change,
196  // so wait until someone calls validatePropertyValue()
197  this.validatePropertyValue(property);
198  },
199 
204  isPropertyEdited: function(property) {
205  this._ensurePropertyData(property);
206  return this._properties[property].edited;
207  },
208 
215  isPropertyEnabled: function(property) {
216  this._ensurePropertyData(property);
217  return this._properties[property].enabled;
218  },
219 
225  setPropertyEnabled: function(property, enabled) {
226  this._ensurePropertyData(property);
227  this._properties[property].enabled = enabled;
228  this._notifyPropertyListeners(property);
229  },
230 
236  isPropertyInvalidated: function(property) {
237  this._ensurePropertyData(property);
238  return this._properties[property].knownInvalid;
239  },
240 
246  validatePropertyValue: function(property) {
247  var needsNotification = true;
248 
249  this._ensurePropertyData(property);
250 
251  // If no changes were made, don't bother invalidating
252  if (this._properties[property].edited) {
253 
254  var value = this._properties[property].value;
255  if (value == "") {
256  value = null;
257  }
258 
259  var valid = this._properties[property].propInfo.validate(value);
260  this._properties[property].knownInvalid = !valid;
261 
262  // XXXAus: Because of bug 9045, we have to tie the totalTracks and
263  // totalDiscs properties to trackNumber and discNumber and ensure
264  // that denominator is not smaller than the numerator.
265  if(valid) {
266  // Track number greater than total tracks, invalid value.
267  if(property == SBProperties.totalTracks ||
268  property == SBProperties.trackNumber) {
269 
270  this._properties[SBProperties.totalTracks].knownInvalid =
271  !isNaN(this._properties[SBProperties.totalTracks].value) &&
272  !isNaN(this._properties[SBProperties.trackNumber].value) &&
273  (parseInt(this._properties[SBProperties.totalTracks].value) <
274  parseInt(this._properties[SBProperties.trackNumber].value))
275 
276  this._properties[SBProperties.trackNumber].knownInvalid =
277  !isNaN(this._properties[SBProperties.totalTracks].value) &&
278  !isNaN(this._properties[SBProperties.trackNumber].value) &&
279  (parseInt(this._properties[SBProperties.trackNumber].value) >
280  parseInt(this._properties[SBProperties.totalTracks].value))
281 
282  needsNotification = false;
283 
284  this._notifyPropertyListeners(SBProperties.totalTracks);
285  this._notifyPropertyListeners(SBProperties.trackNumber);
286  }
287  // Disc number greater than total discs, invalid value.
288  if(property == SBProperties.totalDiscs ||
289  property == SBProperties.discNumber) {
290 
291  this._properties[SBProperties.totalDiscs].knownInvalid =
292  !isNaN(this._properties[SBProperties.totalDiscs].value) &&
293  !isNaN(this._properties[SBProperties.discNumber].value) &&
294  (parseInt(this._properties[SBProperties.discNumber].value) >
295  parseInt(this._properties[SBProperties.totalDiscs].value));
296 
297  this._properties[SBProperties.discNumber].knownInvalid =
298  !isNaN(this._properties[SBProperties.totalDiscs].value) &&
299  !isNaN(this._properties[SBProperties.discNumber].value) &&
300  (parseInt(this._properties[SBProperties.totalDiscs].value) <
301  parseInt(this._properties[SBProperties.discNumber].value));
302 
303  needsNotification = false;
304 
305  this._notifyPropertyListeners(SBProperties.totalDiscs);
306  this._notifyPropertyListeners(SBProperties.discNumber);
307  }
308  }
309  }
310 
311  if(needsNotification) {
312  this._notifyPropertyListeners(property);
313  }
314  },
315 
322  isKnownInvalid: function() {
323  for (var propertyName in this._properties) {
324  if (this._properties[propertyName].knownInvalid &&
325  this._properties[propertyName].enabled) {
326  return true;
327  }
328  }
329  return false;
330  },
331 
337  getEnabledProperties: function() {
338  var enabledList = [];
339  for (var propertyName in this._properties) {
340  if (this._properties[propertyName].enabled) {
341  enabledList.push(propertyName);
342  }
343  }
344  return enabledList;
345  },
346 
351  _ensurePropertyData: function TrackEditorState__ensurePropertyData(property) {
352  // If the data has already been created, nothing to do
353  if (property in this._properties) {
354  return;
355  }
356 
357  // Set up the empty structure
358  this._properties[property] = {
359  edited: false,
360  enabled: false,
361  hasMultiple: false,
362  value: "",
363  originalValue: "",
364  propInfo: this._propertyManager.getPropertyInfo(property),
365  knownInvalid: false
366  };
367 
368  // Populate the structure
369  if (this._selectedItems && this._selectedItems.length > 0) {
370  var value = this._selectedItems[0].getProperty(property);
371 
372  if (property == SBProperties.primaryImageURL) {
373  // See if we need to manually load extra track image data.
374  var MAX_ITEMS_TO_CHECK = 30; // keep this from taking forever.
375  var selectedItems = TrackEditor.state.selectedItems;
376  var numToCheck = Math.min(MAX_ITEMS_TO_CHECK, selectedItems.length);
377 
378  var seedValue;
379  for (var i = 0; i < numToCheck; i++) {
380  value = selectedItems[i].getProperty(property);
381 
382  // Break out early if we determine we don't need to scan any further items.
383  if (i == 0) {
384  seedValue = value;
385  }
386  if (seedValue != value) {
387  value = "";
388  break;
389  }
390  }
391  }
392 
393  if (this._selectedItems.length > 1) {
394  // Look through the selection to see if all items have the same value
395  // for this property
396  for each (var item in this._selectedItems) {
397  if (value != item.getProperty(property)) {
398  this._properties[property].hasMultiple = true;
399  value = "";
400  break;
401  }
402  }
403  }
404 
405  // Format the string if needed
406  if (value)
407  {
408  if (property == SBProperties.primaryImageURL) {
409  // can't format. for an image, format truncates the value to ""!
410  }
411  else if (this._properties[property].hasMultiple) {
412  // we keep multiple values as "".
413  }
414  else {
415  // Formatting can fail. :(
416  try {
417  value = this._properties[property].propInfo.format(value);
418  }
419  catch (e) {
420  Components.utils.reportError("TrackEditor::getPropertyValue("+property+") - "+value+": " + e +"\n");
421  }
422  }
423  }
424 
425  if (value == null) {
426  value = "";
427  }
428 
429  this._properties[property].value = value;
430 
431  // If there are multiple value for this property,
432  // default to edited as "", but disabled.
433  // This way the user can null out the multiple values
434  // simply by enabling the field.
435  if (this._properties[property].hasMultiple) {
436  this._properties[property].edited = true;
437  this._properties[property].originalValue = null;
438  } else {
439  this._properties[property].originalValue = value;
440  }
441  }
442  },
443 
450  addPropertyListener: function(property, listener) {
451  if (!("onTrackEditorPropertyChange" in listener)) {
452  throw new Error("Listener must provide a onTrackEditorPropertyChange function");
453  }
454 
455  if (!(property in this._propertyListeners)) {
456  this._propertyListeners[property] = [listener];
457  } else {
458  this._propertyListeners[property].push(listener);
459  }
460  },
461 
465  removePropertyListener: function(property, listener) {
466  if (property in this._propertyListeners) {
467  var array = this._propertyListeners[property];
468  var index = array.indexOf(listener);
469  if (index > -1) {
470  array.splice(index,1);
471  }
472  }
473  },
474 
481  addSelectionListener: function(listener) {
482  if (!("onTrackEditorSelectionChange" in listener)) {
483  throw new Error("Listener must provide a onTrackEditorSelectionChange function");
484  }
485  this._selectionListeners.push(listener);
486  },
487 
491  removeSelectionListener: function(listener) {
492  var index = this._selectionListeners.indexOf(listener);
493  if (index > -1) {
494  this._selectionListeners.splice(index,1);
495  }
496  },
497 
502  _notifyPropertyListeners: function TrackEditorState__notifyPropertyListeners(property) {
503  // If no property was specified, notify everyone
504  if (property == null) {
505  for each (var listenerArray in this._propertyListeners) {
506  for each (var listener in listenerArray) {
507  listener.onTrackEditorPropertyChange(property);
508  }
509  }
510  } else {
511  // Otherwise just notify listeners interested in
512  // this specific property
513  var listeners = this._propertyListeners[property];
514  if (listeners) {
515  for each (var listener in listeners) {
516  listener.onTrackEditorPropertyChange(property);
517  }
518  }
519 
520  // Notify those who explicitly want to hear about all changes.
521  if (property != "all") {
522  var listeners = this._propertyListeners["all"];
523  if (listeners) {
524  for each (var listener in listeners) {
525  listener.onTrackEditorPropertyChange(property);
526  }
527  }
528  }
529  }
530  },
531 
535  _notifySelectionListeners: function TrackEditorState__notifySelectionListeners() {
536  for each (var listener in this._selectionListeners) {
537  listener.onTrackEditorSelectionChange();
538  }
539  }
540 }
const Cu
const Cc
var TrackEditor
Definition: trackEditor.js:52
inArray array
function TrackEditorState()
return null
Definition: FeedWriter.js:1143
countRef value
Definition: FeedWriter.js:1423
const Cr
const Ci
Javascript wrappers for common library tasks.
_getSelectedPageStyle s i