1 /*
  2  * ModelMaterialsComponent.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <[email protected]>
  5  *
  6  * This program is free software; you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation; either version 2 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * This program is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with this program; if not, write to the Free Software
 18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 19  */
 20 
 21 // Requires CoreTools.js
 22 // Requires toolkit.js
 23 
 24 /**
 25  * Component giving access to materials editor. When the user clicks
 26  * on this button a dialog appears to let him choose materials.
 27  * @param {UserPreferences} preferences user preferences
 28  * @param {ModelMaterialsController} controller modelMaterials choice controller
 29  * @constructor
 30  * @author Louis Grignon
 31  * @author Emmanuel Puybaret
 32  */
 33 function ModelMaterialsComponent(preferences, controller) {
 34   JSComponent.call(this, preferences, document.createElement("span"), true);
 35 
 36   this.controller = controller;
 37 
 38   this.getHTMLElement().innerHTML = '<button class="model-materials-button">' + this.getLocalizedLabelText('ModelMaterialsComponent', "modifyButton.text") + '</button>';
 39   this.button = this.findElement(".model-materials-button");
 40   this.button.disabled = true;
 41 
 42   var component = this;
 43   this.registerEventListener(this.button, "click", function(ev) { 
 44       component.openModelMaterialsSelectorDialog(); 
 45     });
 46 }
 47 ModelMaterialsComponent.prototype = Object.create(JSComponent.prototype);
 48 ModelMaterialsComponent.prototype.constructor = ModelMaterialsComponent;
 49 
 50 /**
 51  * Enables or disables this component.
 52  * @param {boolean} enabled  
 53  */
 54 ModelMaterialsComponent.prototype.setEnabled = function(enabled) {
 55   this.button.disabled = !enabled;
 56 }
 57 
 58 /**
 59  * @private
 60  */
 61 ModelMaterialsComponent.prototype.openModelMaterialsSelectorDialog = function() {
 62   var dialog = new JSModelMaterialsSelectorDialog(this.getUserPreferences(), this.controller);
 63   dialog.displayView();
 64 }
 65 
 66 ModelMaterialsComponent.prototype.dispose = function() {
 67   JSComponent.prototype.dispose.call(this);
 68 }
 69 
 70 /**
 71  * The modelMaterials selector dialog class.
 72  * @param {UserPreferences} preferences the current user preferences
 73  * @param {ModelMaterialsController} controller modelMaterials choice controller
 74  * @extends JSDialog
 75  * @constructor
 76  * @private
 77  */
 78 function JSModelMaterialsSelectorDialog(preferences, controller) {
 79   this.controller = controller;
 80 
 81   var html = 
 82     '<div data-name="preview-panel">' +
 83     '  <span>@{ModelMaterialsComponent.previewLabel.text}</span><br/>' +
 84     '  <canvas id="model-preview-canvas"></canvas>' +
 85     '</div>' + 
 86     '<div data-name="edit-panel">' +
 87     '  <div>' +
 88     '    <span>@{ModelMaterialsComponent.materialsLabel.text}</span>' +
 89     '    <div data-name="materials-list">' +
 90     '    </div>' +
 91     '  </div>' +
 92     '  <div class="color-texture-shininess-panel">' +
 93     '    <span>@{ModelMaterialsComponent.colorAndTextureLabel.text}</span><br/>' +
 94     '    <div class="label-input-grid">' +
 95     '      <label class="whole-line"><input type="radio" name="color-and-texture-checkbox" value="DEFAULT">@{ModelMaterialsComponent.defaultColorAndTextureRadioButton.text}</label>' +
 96     '      <label class="whole-line"><input type="radio" name="color-and-texture-checkbox" value="INVISIBLE">@{ModelMaterialsComponent.invisibleRadioButton.text}</label>' +
 97     '      <label><input type="radio" name="color-and-texture-checkbox" value="COLOR">@{ModelMaterialsComponent.colorRadioButton.text}</label>' +
 98     '      <span data-name="color-button"></span>' +
 99     '      <label><input type="radio" name="color-and-texture-checkbox" value="TEXTURE">@{ModelMaterialsComponent.textureRadioButton.text}</label>' +
100     '      <span data-name="texture-component"></span>' +
101     '    </div>' +
102     '    <br />' +
103     '    <span>@{ModelMaterialsComponent.shininessLabel.text}</span><br/>' +
104     '    <input type="range" name="shininess-slider" min="0" max="128" list="model-materials-shininess-list" /> ' +
105     '    <datalist id="model-materials-shininess-list"></datalist> ' +
106     '    <div class="slider-labels"><div>@{ModelMaterialsComponent.mattLabel.text}</div><div>@{ModelMaterialsComponent.shinyLabel.text}</div></div>' +
107     '  </div>' +
108     '</div>';
109 
110   JSDialog.call(this, preferences, "@{HomeFurnitureController.modelMaterialsTitle}", html, 
111       {
112         size: "medium",
113         applier: function(dialog) {
114           controller.setMaterials(dialog.materialsList.getMaterials());
115         },
116         disposer: function(dialog) {
117           dialog.selectedMaterialBlinker.stop();
118           dialog.colorButton.dispose();
119           dialog.textureComponent.dispose();
120         }
121       });
122 
123   this.getHTMLElement().classList.add("model-materials-chooser-dialog");
124   
125   this.initMaterialsList();
126   this.initPreviewPanel();
127   this.initColorAndTexturePanel();
128   this.initShininessPanel();
129 
130   var dialog = this;
131   if (this.materialsList.size() > 0) {
132     this.materialsList.addSelectionInterval(0, 0);
133   } else {
134     // Add a listener that will select first row as soon as the list contains some data
135     var selectFirstMaterialOnContentAvailable = function() {
136         if (dialog.materialsList.size() > 0) {
137           dialog.materialsList.addSelectionInterval(0, 0);
138           dialog.materialsList.removeListDataListener(selectFirstMaterialOnContentAvailable);
139         }
140       };
141     this.materialsList.addListDataListener(selectFirstMaterialOnContentAvailable);
142   }
143 
144   this.enableComponents();
145 
146   var selectedMaterialBlinker = {
147       delay: 500,
148       animationTask: null,
149       start: function() {
150         var materialsList = dialog.materialsList;
151         var toggleBlinkingState = function() {
152             if (materialsList.size() > 1) {
153               var materials = materialsList.getMaterials();
154               if (selectedMaterialBlinker.delay !== 100) {
155                 selectedMaterialBlinker.delay = 100;
156                 if (materials == null) {
157                   materials = dialog.newArray(materialsList.size());
158                 } else {
159                   materials = materials.slice(0);
160                 }
161                 var selectedIndices = materialsList.getSelectedIndices();
162                 for (var i = 0; i < selectedIndices.length; i++) {
163                   var index = selectedIndices[i];
164                   var defaultMaterial = materialsList.getDefaultMaterialAt(index);
165                   var selectedMaterial = materials [index] != null
166                       ? materials [index]
167                       : defaultMaterial;
168                   var blinkColor = 0xFF2244FF;
169                   if (selectedMaterial.getTexture() == null) {
170                     var selectedColor = selectedMaterial.getColor();
171                     if (selectedColor == null) {
172                       selectedColor = defaultMaterial.getColor();
173                       if (selectedColor == null) {
174                         selectedColor = 0xFF000000;
175                       }
176                     }
177                     selectedColor &= 0x00FFFFFF;
178                     var red   = (selectedColor >> 16) & 0xFF;
179                     var green = (selectedColor >> 8) & 0xFF;
180                     var blue  = selectedColor & 0xFF;
181                     if (Math.max(red, Math.max(green, blue)) > 0x77) {
182                       // Display a darker color for a bright color
183                       blinkColor = 0xFF000000 | (red / 2 << 16) | (green / 2 << 8) |  (blue / 2);
184                     } else if ((red + green + blue) / 3 > 0x0F) {
185                       // Display a brighter color for a dark color
186                       blinkColor = 0xFF000000 
187                           | (Math.min(red * 2, 0xFF) << 16) 
188                           | (Math.min(green * 2, 0xFF) << 8) 
189                           | Math.min(blue * 2, 0xFF);
190                     }
191                   }
192                   materials [index] = new HomeMaterial(
193                       selectedMaterial.getName(), blinkColor, null, selectedMaterial.getShininess());
194                 }
195               } else {
196                 selectedMaterialBlinker.delay = 1000;
197               }
198               dialog.previewComponent.setModelMaterials(materials);
199             }
200   
201             selectedMaterialBlinker.animationTask = setTimeout(toggleBlinkingState, 
202                 selectedMaterialBlinker.delay);
203           };
204           
205         selectedMaterialBlinker.animationTask = setTimeout(toggleBlinkingState, selectedMaterialBlinker.delay);
206       },
207       stop: function() {
208         clearTimeout(selectedMaterialBlinker.animationTask);
209       },
210       restart: function() {
211         selectedMaterialBlinker.stop();
212         selectedMaterialBlinker.delay = 101;
213         selectedMaterialBlinker.start();
214       }
215     };
216 
217   selectedMaterialBlinker.start();
218   this.materialsList.addListSelectionListener(function(ev) {
219       selectedMaterialBlinker.restart();
220     });
221   this.selectedMaterialBlinker = selectedMaterialBlinker; 
222 }
223 JSModelMaterialsSelectorDialog.prototype = Object.create(JSDialog.prototype);
224 JSModelMaterialsSelectorDialog.prototype.constructor = JSModelMaterialsSelectorDialog;
225 
226 /**
227  * Returns an array of the given size initialized with <code>null</code>.
228  * @param {number} size
229  * @return {Array}
230  * @private
231  */
232 JSModelMaterialsSelectorDialog.prototype.newArray = function(size) {
233   var array = new Array(size);
234   for (var i = 0; i < array.length; i++) {
235     array [i] = null;
236   }
237   return array;
238 }
239 
240 /**
241  * @private
242  */
243 JSModelMaterialsSelectorDialog.prototype.initMaterialsList = function() {
244   var dialog = this;
245   var controller = this.controller;
246   var materialsListElement = this.getElement("materials-list");
247 
248   var defaultMaterials = [];
249   var materials = controller.getMaterials();
250   if (materials != null) {
251     materials = materials.slice(0);
252   }
253   ModelManager.getInstance().loadModel(controller.getModel(), true, 
254       {
255         modelUpdated : function(modelRoot) {
256           defaultMaterials = ModelManager.getInstance().getMaterials(
257               modelRoot, (controller.getModelFlags() & PieceOfFurniture.HIDE_EDGE_COLOR_MATERIAL) != 0, controller.getModelCreator());
258           if (materials != null) {
259             // Keep only materials that are defined in default materials set
260             // (the list can be different if the model loader interprets differently a 3D model file
261             // or if materials come from a paste style action)
262             var updatedMaterials = dialog.newArray(defaultMaterials.length);
263             var foundInDefaultMaterials = false;
264             for (var i = 0; i < defaultMaterials.length; i++) {
265               var materialName = defaultMaterials [i].getName();
266               for (var j = 0; j < materials.length; j++) {
267                 if (materials [j] != null && materials [j].getName() == materialName) {
268                   updatedMaterials [i] = materials [j];
269                   foundInDefaultMaterials = true;
270                   break;
271                 }
272               }
273             }
274             if (foundInDefaultMaterials) {
275               materials = updatedMaterials;
276             } else {
277               materials = null;
278             }
279           }
280         }
281       });
282 
283   var materialsList = 
284   this.materialsList = {
285       element: materialsListElement,
286       materials: materials,
287       defaultMaterials: defaultMaterials,
288       dataListeners: [],
289       selectionListeners: [],
290       /**
291        * @return {number}
292        */
293       size: function() {
294         if (materialsList.defaultMaterials != null) {
295           return materialsList.defaultMaterials.length;
296         } else {
297           return 0;
298         }
299       },
300       /**
301        * @return {HomeMaterial[]}
302        */
303       getMaterials: function() {
304         return materialsList.materials;
305       },
306       /**
307        * @param {number} index
308        * @return {HomeMaterial}
309        */
310       getDefaultMaterialAt: function(index) {
311         return materialsList.defaultMaterials[index];
312       },
313       /**
314        * @param {number} index
315        * @return {HomeMaterial}
316        */
317       getElementAt: function(index) {
318         var material;
319         if (materialsList.materials != null
320             && materialsList.materials [index] != null
321             && materialsList.materials [index].getName() != null
322             && materialsList.materials [index].getName() == materialsList.defaultMaterials [index].getName()) {
323           material = materialsList.materials [index];
324         } else {
325           material = new HomeMaterial(materialsList.defaultMaterials [index].getName(), null, null, null);
326         }
327         return material;
328       },
329       /**
330        * @param {HomeMaterial} material
331        * @param {number} index
332        */
333       setMaterialAt: function(material, index) {
334         if (material.getColor() == null
335             && material.getTexture() == null
336             && material.getShininess() == null) {
337           if (materialsList.materials != null) {
338             materialsList.materials [index] = null;
339             var containsOnlyNull = true;
340             for (var i = 0; i < materialsList.materials.length; i++) {
341               if (materialsList.materials[i] != null) {
342                 containsOnlyNull = false;
343                 break;
344               }
345             }
346             if (containsOnlyNull) {
347               materialsList.materials = null;
348             }
349           }
350         } else {
351           if (materialsList.materials == null 
352               || materialsList.materials.length != materialsList.defaultMaterials.length) {
353             materialsList.materials = dialog.newArray(materialsList.defaultMaterials.length);
354           }
355           materialsList.materials [index] = material;
356         }
357   
358         materialsList.fireContentsChanged();
359       },
360       fireContentsChanged: function() {
361         for (var i = 0; i < materialsList.dataListeners.length; i++) {
362           materialsList.dataListeners[i]();
363         }
364         materialsList.repaint();
365       },
366       /**
367        * @param {HomeMaterial} material
368        * @param {HomeMaterial} defaultMaterial
369        * @param {boolean} [selected] default false
370        */
371       createListItem: function(material, defaultMaterial, selected) {
372         var materialTexture = material.getTexture();
373         var materialColor = material.getColor();
374         if (materialTexture == null && materialColor == null) {
375           materialTexture = defaultMaterial.getTexture();
376           if (materialTexture == null) {
377             materialColor = defaultMaterial.getColor();
378           }
379         }
380   
381         var itemBackground = document.createElement("div");
382         itemBackground.classList.add("icon");
383         if (materialTexture != null && materialTexture.getImage() != null) {
384           // Display material texture image with an icon
385           TextureManager.getInstance().loadTexture(materialTexture.getImage(), 
386               {
387                 textureUpdated: function(image) {
388                   itemBackground.style.backgroundImage = "url('" + image.src + "')";
389                 },
390                 textureError:  function(error) {
391                   itemBackground.style.backgroundImage = "none";
392                 }
393               });
394         } else if (materialColor != null
395                    && (materialColor & 0xFF000000) != 0) {
396           itemBackground.style.backgroundColor = ColorTools.integerToHexadecimalString(materialColor);
397         } else {
398           // Empty icon
399         }
400   
401         var item = document.createElement("div");
402         item.textContent = material.getName();
403         if (selected) {
404           item.classList.add("selected");
405         }
406   
407         item.appendChild(itemBackground);
408         return item;
409       },
410       repaint: function() {
411         var defaultMaterials = materialsList.defaultMaterials;
412         var materials = materialsList.materials;
413         var selectedIndices = materialsList.getSelectedIndices();
414   
415         materialsList.element.innerHTML = "";
416         if (defaultMaterials != null) {
417           // Generate content
418           materialsList.element.innerHTML = "";
419           for (var i = 0; i < defaultMaterials.length; i++) {
420             var material = materialsList.getElementAt(i);
421             var defaultMaterial = materialsList.getDefaultMaterialAt(i);
422             var selected = selectedIndices.indexOf(i) > -1;
423             materialsList.element.appendChild(materialsList.createListItem(material, defaultMaterial, selected));
424           }
425   
426           // 1) Single click
427           dialog.registerEventListener(materialsList.element.children, "click", function(ev) {
428               var item = this;
429               var multiSelection = OperatingSystem.isMacOSX() ? ev.metaKey : ev.ctrlKey;
430               if (multiSelection) {
431                 if (item.classList.contains("selected")) {
432                   item.classList.remove("selected");
433                   materialsList.fireSelectionChanged();
434                 } else {
435                   materialsList.selectItem(item);
436                 }
437               } else {
438                 for (var i = 0; i < materialsList.element.children.length; i++) {
439                   var otherItem = materialsList.element.children[i];
440                   if (otherItem != item) {
441                     otherItem.classList.remove("selected");
442                   }
443                 }
444                 materialsList.selectItem(item);
445               }
446             });
447           
448           // 2) Double click
449           dialog.registerEventListener(materialsList.element.children, "dblclick", function(ev) {
450               materialsList.selectItem(this);   
451               if (dialog.colorButton.getColor() != null) {
452                 dialog.colorButton.findElement("button").click();
453               } else if (controller.getTextureController().getTexture() != null
454                   && dialog.textureComponent != null) {
455                 dialog.textureComponent.findElement("button").click();
456               }
457             });
458         }
459       },
460       /**
461        * Triggered whenever this list's content changes (materials count or data). This is not about selection,
462        * please also see addListSelectionListener
463        * @param {function()} listener
464        */
465       addListDataListener: function(listener) {
466         materialsList.dataListeners.push(listener);
467       },
468       /**
469        * @param {function()} listener
470        */
471       removeListDataListener: function(listener) {
472         materialsList.dataListeners.splice(materialsList.dataListeners.index(listener), 1);
473       },
474       /**
475        * add listener on selection event (when one or more material are selected, or deselected)
476        * @param {function()} listener
477        */
478       addListSelectionListener: function(listener) {
479         materialsList.selectionListeners.push(listener);
480       },
481       /**
482        * @param {number} index1
483        * @param {number} index2
484        */
485       addSelectionInterval: function(index1, index2) {
486         for (var i = index1; i <= index2; i++) {
487           materialsList.element.children[i].classList.add("selected");
488         }
489         materialsList.fireSelectionChanged();
490       },
491       /**
492        * @param {number} index1
493        * @param {number} index2
494        */
495       removeSelectionInterval: function(index1, index2) {
496         for (var i = index1; i <= index2; i++) {
497           materialsList.element.children[i].classList.remove("selected");
498         }
499         materialsList.fireSelectionChanged();
500       },
501       clearSelection: function() {
502         materialsList.removeSelectionInterval(0, materialsList.size() - 1);
503       },
504       selectItem: function(item) {
505         item.classList.add("selected");
506         materialsList.fireSelectionChanged();
507       },
508       fireSelectionChanged: function() {
509         for (var i = 0; i < materialsList.selectionListeners.length; i++) {
510           materialsList.selectionListeners[i]();
511         }
512       },
513       /**
514        * @return {boolean}
515        */
516       isSelectionEmpty: function() {
517         return materialsList.element.querySelector(".selected") != null;
518       },
519       /**
520        * @return {number[]}
521        */
522       getSelectedIndices: function() {
523         var selectedIndices = [];
524         var items = materialsList.element.children;
525         for (var i = 0; i < items.length; i++) {
526           if (items[i].classList.contains("selected")) {
527             selectedIndices.push(i);
528           }
529         }
530         return selectedIndices;
531       },
532     };
533 
534   this.materialsList.repaint();
535 
536   // List selection change handler
537   this.materialsList.addListSelectionListener(function() {
538       var selectedIndices = dialog.materialsList.getSelectedIndices();
539       if (selectedIndices.length > 0) {
540         var material = dialog.materialsList.getElementAt(selectedIndices[0]);
541         var texture = material.getTexture();
542         var color = material.getColor();
543         var shininess = material.getShininess();
544         var defaultMaterial = dialog.materialsList.getDefaultMaterialAt(selectedIndices[0]);
545         if (color == null && texture == null) {
546           dialog.defaultColorAndTextureRadioButton.checked = true;
547           // Display default color or texture in buttons
548           texture = defaultMaterial.getTexture();
549           if (texture != null) {
550             dialog.colorButton.setColor(null);
551             controller.getTextureController().setTexture(texture);
552           } else {
553             color = defaultMaterial.getColor();
554             if (color != null) {
555               controller.getTextureController().setTexture(null);
556               dialog.colorButton.setColor(color);
557             }
558           }
559         } else if (texture != null) {
560           dialog.textureRadioButton.checked = true;
561           dialog.colorButton.setColor(null);
562           controller.getTextureController().setTexture(texture);
563         } else if ((color & 0xFF000000) == 0) {
564           dialog.invisibleRadioButton.checked = true;
565           // Display default color or texture in buttons
566           texture = defaultMaterial.getTexture();
567           if (texture != null) {
568             dialog.colorButton.setColor(null);
569             controller.getTextureController().setTexture(texture);
570           } else {
571             color = defaultMaterial.getColor();
572             if (color != null) {
573               controller.getTextureController().setTexture(null);
574               dialog.colorButton.setColor(color);
575             }
576           }
577         } else {
578           dialog.colorRadioButton.checked = true;
579           controller.getTextureController().setTexture(null);
580           dialog.colorButton.setColor(color);
581         }
582   
583         if (shininess != null) {
584           dialog.shininessSlider.value = shininess * 128;
585         } else {
586           dialog.shininessSlider.value = defaultMaterial.getShininess() != null
587               ? defaultMaterial.getShininess() * 128
588               : 0;
589         }
590       }
591       dialog.enableComponents();
592     });
593 }
594 
595 /**
596  * @private
597  */
598 JSModelMaterialsSelectorDialog.prototype.initPreviewPanel = function() {
599   var controller = this.controller;
600   var dialog = this;
601 
602   var previewPanel = this.getElement("preview-panel");
603   var previewCanvas = this.findElement("#model-preview-canvas");
604 
605   previewCanvas.width = 250;
606   previewCanvas.height = 250;
607 
608   var previewComponent = 
609   dialog.previewComponent = new ModelPreviewComponent(previewCanvas, true);
610   ModelManager.getInstance().loadModel(controller.getModel(), false, 
611       {
612         modelUpdated : function(modelRoot) {
613           var materialsList = dialog.materialsList;
614           previewComponent.setModel(
615               controller.getModel(), controller.getModelFlags(), controller.getModelRotation(),
616               controller.getModelWidth(), controller.getModelDepth(), controller.getModelHeight());
617           previewComponent.setModelMaterials(materialsList.getMaterials());
618           previewComponent.setModelTransformations(controller.getModelTransformations());
619           materialsList.addListDataListener(function() {
620               previewComponent.setModelMaterials(materialsList.getMaterials());
621             });
622         },
623         modelError: function() {
624           previewPanel.style.visibility = "hidden";
625         }
626       });
627   
628   var mousePressed = function(ev) {
629       var pickedMaterial = dialog.previewComponent.getPickedMaterial();
630       if (pickedMaterial != null) {
631         for (var i = 0, n = dialog.materialsList.size(); i < n; i++) {
632           var material = dialog.materialsList.getDefaultMaterialAt(i);
633           if (material.getName() !== null
634               && material.getName() == pickedMaterial.getName()) {
635             var multiSelection = OperatingSystem.isMacOSX() ? ev.metaKey : ev.ctrlKey;
636             if (multiSelection) {
637               var selectedIndices = dialog.materialsList.getSelectedIndices();
638               if (selectedIndices.indexOf(i) >= 0) {
639                 dialog.materialsList.removeSelectionInterval(i, i);
640               } else {
641                 dialog.materialsList.addSelectionInterval(i, i);
642               }
643             } else {
644               dialog.materialsList.clearSelection();
645               dialog.materialsList.addSelectionInterval(i, i);
646             }
647           }
648         }
649       }
650     };
651   if (OperatingSystem.isInternetExplorerOrLegacyEdge()
652       && window.PointerEvent) {
653     // Multi touch support for IE and Edge
654     dialog.previewComponent.getHTMLElement().addEventListener("pointerdown", mousePressed);
655     dialog.previewComponent.getHTMLElement().addEventListener("mousedown", mousePressed);
656   } else {
657     dialog.previewComponent.getHTMLElement().addEventListener("touchstart", mousePressed);
658     dialog.previewComponent.getHTMLElement().addEventListener("mousedown", mousePressed);
659   }
660 }
661 
662 
663 /**
664  * @private
665  */
666 JSModelMaterialsSelectorDialog.prototype.initColorAndTexturePanel = function() {
667   var dialog = this;
668   var controller = this.controller;
669 
670   this.defaultColorAndTextureRadioButton = this.findElement("[name='color-and-texture-checkbox'][value='DEFAULT']");
671   this.invisibleRadioButton = this.findElement("[name='color-and-texture-checkbox'][value='INVISIBLE']");
672   this.colorRadioButton = this.findElement("[name='color-and-texture-checkbox'][value='COLOR']");
673   this.textureRadioButton = this.findElement("[name='color-and-texture-checkbox'][value='TEXTURE']");
674 
675   /**
676    * @param {function(HomeMaterial, index): HomeMaterial} materialCreator
677    */
678   var modifySelectedMaterials = function(materialCreator) {
679       var selectedIndices = dialog.materialsList.getSelectedIndices();
680       for (var i = 0; i < selectedIndices.length; i++) {
681         var index = selectedIndices[i];
682         var material = dialog.materialsList.getElementAt(index);
683         dialog.materialsList.setMaterialAt(
684             materialCreator(material, index),
685             index);
686       }
687     };
688   
689   // Listen to click events on radio buttons rather than change events 
690   // to avoid firing events when checked state is set internaly 
691   this.registerEventListener(this.defaultColorAndTextureRadioButton, "click", function(ev) {
692       if (!this.disabled && this.checked) {
693         modifySelectedMaterials(function(material) {
694             return new HomeMaterial(material.getName(), null, null, material.getShininess());
695           });
696       }
697     });
698   
699   this.registerEventListener(this.invisibleRadioButton, "click", function(ev) {
700       if (!this.disabled && this.checked) {
701         modifySelectedMaterials(function(material) {
702             return new HomeMaterial(material.getName(), 0, null, material.getShininess());
703           });
704       }
705     });
706 
707   var colorChoiceChangeListener = function() {
708       if (!dialog.colorRadioButton.disabled && dialog.colorRadioButton.checked) {
709         modifySelectedMaterials(function(material, index) {
710             var defaultMaterial = dialog.materialsList.getDefaultMaterialAt(index);
711             var color = defaultMaterial.getColor() != dialog.colorButton.getColor() || defaultMaterial.getTexture() != null
712                 ? dialog.colorButton.getColor()
713                 : null;
714             return new HomeMaterial(material.getName(), color, null, material.getShininess());
715           });
716       }
717     };
718   this.colorButton = new ColorButton(this.getUserPreferences(), 
719       {
720         colorChanged: function(selectedColor) {
721           if (selectedColor != null) {
722             dialog.colorRadioButton.checked = true;
723             colorChoiceChangeListener();
724           }
725         }
726       });
727   this.colorButton.setColorDialogTitle(this.getUserPreferences().getLocalizedString(
728       "ModelMaterialsComponent", "colorDialog.title"));
729   this.registerEventListener(this.colorRadioButton, "click", function(ev) {
730       if (dialog.colorRadioButton.checked) {
731         colorChoiceChangeListener();
732       }
733     });
734   this.attachChildComponent("color-button", this.colorButton);
735 
736   var textureChoiceChangeListener = function() {
737       if (!dialog.textureRadioButton.disabled && dialog.textureRadioButton.checked) {
738         modifySelectedMaterials(function(material, index) {
739             var defaultTexture = dialog.materialsList.getDefaultMaterialAt(index).getTexture();
740             var texture = defaultTexture != controller.getTextureController().getTexture()
741                 ? controller.getTextureController().getTexture()
742                 : null;
743             return new HomeMaterial(material.getName(), null, texture, material.getShininess());
744           });
745       }
746     };
747   this.textureComponent = controller.getTextureController().getView();
748   this.textureComponent.textureChanged = function(texture) {
749       if (texture != null) {
750         dialog.textureRadioButton.checked = true;
751         textureChoiceChangeListener();
752       }
753     };
754   this.registerEventListener(this.textureRadioButton, "click", function(ev) {
755       if (dialog.textureRadioButton.checked) {
756         textureChoiceChangeListener();
757       }
758     });
759   this.registerPropertyChangeListener(controller.getTextureController(), "TEXTURE", function(ev) {
760       if (ev.getNewValue() != null) {
761         if (!dialog.textureRadioButton.checked) {
762           dialog.textureRadioButton.checked = true;
763         }
764         textureChoiceChangeListener();
765       }
766     });
767   this.attachChildComponent("texture-component", this.textureComponent);
768 }
769 
770 /**
771  * @private
772  */
773 JSModelMaterialsSelectorDialog.prototype.initShininessPanel = function() {
774   var dialog = this;
775   this.shininessSlider = this.getElement("shininess-slider");
776 
777   var shininessList = this.findElement("#model-materials-shininess-list");
778   for (var i = 0; i <= 128; i+= 16) {
779     var option = document.createElement("option");
780     option.value = i;
781     shininessList.appendChild(option);
782   }
783 
784   this.registerEventListener(this.shininessSlider, "input", function(ev) {
785       var shininess = this.value;
786       var selectedIndices = dialog.materialsList.getSelectedIndices();
787       for (var i = 0; i < selectedIndices.length; i++) {
788         var index = selectedIndices[i];
789         var material = dialog.materialsList.getElementAt(index);
790         dialog.materialsList.setMaterialAt(
791             new HomeMaterial(material.getName(), material.getColor(), material.getTexture(), shininess / 128),
792             index);
793       }
794     });
795 };
796 
797 /**
798  * @private
799  */
800 JSModelMaterialsSelectorDialog.prototype.enableComponents = function() {
801   var selectionEmpty = !this.materialsList.isSelectionEmpty();
802   this.defaultColorAndTextureRadioButton.disabled = selectionEmpty;
803   this.invisibleRadioButton.disabled = selectionEmpty;
804   this.textureRadioButton.disabled = selectionEmpty;
805   this.textureComponent.setEnabled(!selectionEmpty);
806   this.colorRadioButton.disabled = selectionEmpty;
807   this.colorButton.setEnabled(!selectionEmpty);
808   this.shininessSlider.disabled = selectionEmpty;
809 }
810 
811