1 /* 2 * TextureChoiceComponent.js 3 * 4 * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <info@sweethome3d.com> 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 * Button displaying a texture as an icon. When the user clicks 26 * on this button a dialog appears to let him choose an other texture. 27 * @param {UserPreferences} preferences user preferences 28 * @param {TextureChoiceController} controller texture choice controller 29 * @constructor 30 * @author Louis Grignon 31 * @author Emmanuel Puybaret 32 */ 33 function TextureChoiceComponent(preferences, controller) { 34 JSComponent.call(this, preferences, document.createElement("span"), true); 35 36 this.controller = controller; 37 this.getHTMLElement().innerHTML = '<button class="texture-button"><div class="texture-preview" /></button>'; 38 this.button = this.findElement(".texture-button"); 39 40 var component = this; 41 this.registerEventListener(this.button, "click", function(ev) { 42 component.openTextureDialog(); 43 }); 44 45 this.preview = this.findElement(".texture-preview"); 46 var textureChangeListener = function() { 47 component.updateTexture(controller.getTexture()); 48 }; 49 this.registerPropertyChangeListener(controller, "TEXTURE", textureChangeListener); 50 this.updateTexture(controller.getTexture()); 51 } 52 TextureChoiceComponent.prototype = Object.create(JSComponent.prototype); 53 TextureChoiceComponent.prototype.constructor = TextureChoiceComponent; 54 55 /** 56 * Updates the texture image displayed by this button. 57 * @param {Texture} texture 58 * @private 59 */ 60 TextureChoiceComponent.prototype.updateTexture = function(texture) { 61 if (texture == null) { 62 this.preview.style.backgroundImage = "none"; 63 } else { 64 var component = this; 65 TextureManager.getInstance().loadTexture(texture.getImage(), 66 { 67 textureUpdated: function(image) { 68 component.preview.style.backgroundImage = "url('" + image.src + "')"; 69 }, 70 textureError: function(error) { 71 console.log("Image cannot be loaded", error); 72 } 73 }); 74 } 75 } 76 77 /** 78 * Enables or disables this component. 79 * @param {boolean} enabled 80 */ 81 TextureChoiceComponent.prototype.setEnabled = function(enabled) { 82 this.button.disabled = !enabled; 83 } 84 85 /** 86 * @private 87 */ 88 TextureChoiceComponent.prototype.openTextureDialog = function() { 89 var dialog = new JSTextureDialog(this.getUserPreferences(), this.controller); 90 if (this.controller.getTexture() != null) { 91 dialog.setSelectedTexture(this.controller.getTexture()); 92 } 93 dialog.displayView(); 94 } 95 96 /** 97 * @return {boolean} 98 */ 99 TextureChoiceComponent.prototype.confirmDeleteSelectedCatalogTexture = function() { 100 // Remove html tags from message because confirm does not support it 101 var messageText = this.getLocalizedLabelText("TextureChoiceComponent", "confirmDeleteSelectedCatalogTexture.message"). 102 replace(/\<[^\>]*\>/g, " ").replace(/[ ]+/g, " ").replace(/^\s*/, ""); 103 return confirm(messageText); 104 } 105 106 107 /** 108 * The texture selector dialog class. 109 * @param {UserPreferences} preferences the current user preferences 110 * @param {TextureChoiceController} controller texture choice controller 111 * @extends JSDialog 112 * @constructor 113 * @private 114 */ 115 function JSTextureDialog(preferences, controller) { 116 this.controller = controller; 117 this.selectedTextureModel = { 118 texture: null, 119 xOffset: 0, 120 yOffset: 0, 121 angleInRadians: 0, 122 scale: 1 123 }; 124 125 /** 126 * @param {CatalogTexture} catalogTexture 127 * @return {HTMLElement} 128 */ 129 var createTextureListItem = function(catalogTexture) { 130 var textureCategory = catalogTexture.getCategory(); 131 var catalogTextureItem = document.createElement("div"); 132 catalogTextureItem.classList.add("item"); 133 catalogTexture.getImage().getStreamURL({ 134 urlReady: function(streamUrl) { 135 catalogTextureItem.innerHTML = '<img src="' + streamUrl + '" />' 136 + textureCategory.getName() + " - " + catalogTexture.getName(); 137 }, 138 urlError: function(url) { 139 catalogTextureItem.innerHTML = '<img/>' 140 + textureCategory.getName() + " - " + catalogTexture.getName(); 141 } 142 }); 143 catalogTextureItem._catalogTexture = catalogTexture; 144 return catalogTextureItem; 145 }; 146 147 var html = 148 '<div class="columns-2">' + 149 ' <div class="column1">' + 150 ' <div class="texture-search"><input type="text" /></div>' + 151 ' <div class="texture-catalog-list"></div>' + 152 ' <div class="recent-textures"></div>' + 153 ' </div>' + 154 ' <div class="column2">' + 155 ' <div class="selected-texture-preview">' + 156 ' <div></div>' + 157 ' </div>' + 158 ' <div class="selected-texture-config label-input-grid">' + 159 ' <div>@{TextureChoiceComponent.xOffsetLabel.text}</div>' + 160 ' <div><span data-name="selected-texture-offset-x" /></div>' + 161 ' <div>@{TextureChoiceComponent.yOffsetLabel.text}</div>' + 162 ' <div><span data-name="selected-texture-offset-y" /></div>' + 163 ' <div>@{TextureChoiceComponent.angleLabel.text}</div>' + 164 ' <div><span data-name="selected-texture-angle" /></div>' + 165 ' <div>@{TextureChoiceComponent.scaleLabel.text}</div>' + 166 ' <div><span data-name="selected-texture-scale" /></div>' + 167 ' </div>' + 168 ' <hr />' + 169 ' <div class="imported-textures-panel">' + 170 ' <div><button import>@{TextureChoiceComponent.importTextureButton.text}</button></div>' + 171 ' <div><button disabled modify>@{TextureChoiceComponent.modifyTextureButton.text}</button></div>' + 172 ' <div><button disabled delete>@{TextureChoiceComponent.deleteTextureButton.text}</button></div>' + 173 ' </div>' + 174 ' </div>' + 175 '</div>'; 176 177 JSDialog.call(this, preferences, controller.getDialogTitle(), html, 178 { 179 applier: function(dialog) { 180 // Force refresh model from inputs, even if "change" event was not raised 181 this.updateTextureTransform(); 182 var selectedTexture = dialog.getSelectedTexture(); 183 controller.setTexture(selectedTexture); 184 if (selectedTexture != null) { 185 controller.addRecentTexture(selectedTexture); 186 } 187 }, 188 disposer: function(dialog) { 189 preferences.getTexturesCatalog().removeTexturesListener(dialog.texturesCatalogListener); 190 } 191 }); 192 193 this.getHTMLElement().classList.add("texture-chooser-dialog"); 194 195 this.recentTexturesPanel = this.findElement(".recent-textures"); 196 197 this.catalogList = this.findElement(".texture-catalog-list"); 198 this.selectedTexturePreview = this.findElement(".selected-texture-preview > div"); 199 200 this.xOffsetInput = new JSSpinner(preferences, this.getElement("selected-texture-offset-x"), 201 { 202 value: 0, 203 minimum: 0, 204 maximum: 100, 205 stepSize: 5 206 }); 207 this.yOffsetInput = new JSSpinner(preferences, this.getElement("selected-texture-offset-y"), 208 { 209 value: 0, 210 minimum: 0, 211 maximum: 100, 212 stepSize: 5 213 }); 214 this.angleInput = new JSSpinner(preferences, this.getElement("selected-texture-angle"), 215 { 216 format: new IntegerFormat(), 217 value: 0, 218 minimum: 0, 219 maximum: 360, 220 stepSize: 15 221 }); 222 this.scaleInput = new JSSpinner(preferences, this.getElement("selected-texture-scale"), 223 { 224 value: 100, 225 minimum: 1, 226 maximum: 10000, 227 stepSize: 5 228 }); 229 var dialog = this; 230 this.registerEventListener([this.xOffsetInput, this.yOffsetInput, this.angleInput, this.scaleInput], "input", 231 function(ev) { 232 dialog.updateTextureTransform(); 233 }); 234 235 var textureCategories = preferences.getTexturesCatalog().getCategories(); 236 for (var i = 0; i < textureCategories.length; i++) { 237 var textureCategory = textureCategories[i]; 238 for (var j = 0; j < textureCategory.getTextures().length; j++) { 239 var catalogTexture = textureCategory.getTextures()[j]; 240 dialog.catalogList.appendChild(createTextureListItem(catalogTexture)); 241 } 242 } 243 this.texturesCatalogItems = this.catalogList.childNodes; 244 245 var mouseClicked = function(ev) { 246 dialog.selectTexture(dialog.getCatalogTextureFromItem(this)); 247 }; 248 this.registerEventListener(this.texturesCatalogItems, "click", mouseClicked); 249 this.initCatalogTextureSearch(preferences); 250 this.initRecentTextures(); 251 this.registerPropertyChangeListener(preferences, "RECENT_TEXTURES", function(ev) { 252 dialog.initRecentTextures(); 253 }); 254 this.registerEventListener(this.findElements(".item"), "dblclick", function(ev) { 255 dialog.validate(); 256 }); 257 if (!OperatingSystem.isInternetExplorerOrLegacyEdge() 258 || !window.PointerEvent) { 259 // Simulate double touch on the same element 260 var lastTouchTime = -1; 261 var textureElement = null; 262 this.registerEventListener(this.findElements(".item"), "touchstart", function(ev) { 263 var time = Date.now(); 264 if (time - lastTouchTime < 500 265 && textureElement === ev.target) { 266 ev.preventDefault(); 267 dialog.validate(); 268 } else { 269 lastTouchTime = time; 270 textureElement = ev.target; 271 } 272 }); 273 } 274 275 this.initImportTexturesPanel(); 276 277 this.texturesCatalogListener = function(ev) { 278 var catalogTexture = ev.getItem && ev.getItem(); 279 switch (ev.getType()) { 280 case CollectionEvent.Type.ADD: 281 dialog.searchInput.value = ""; 282 var listItem = createTextureListItem(catalogTexture); 283 dialog.catalogList.appendChild(listItem); 284 dialog.registerEventListener(listItem, "click", mouseClicked); 285 dialog.selectTexture(catalogTexture); 286 break; 287 case CollectionEvent.Type.DELETE: 288 var catalogTextureItem = dialog.getCatalogTextureItem(catalogTexture); 289 dialog.catalogList.removeChild(catalogTextureItem); 290 var firstItem = dialog.catalogList.querySelector(".item"); 291 if (firstItem) { 292 dialog.selectTexture(dialog.getCatalogTextureFromItem(firstItem)); 293 } 294 break; 295 } 296 }; 297 preferences.getTexturesCatalog().addTexturesListener(this.texturesCatalogListener); 298 } 299 JSTextureDialog.prototype = Object.create(JSDialog.prototype); 300 JSTextureDialog.prototype.constructor = JSTextureDialog; 301 302 /** 303 * Returns the currently selected texture. 304 * @return {HomeTexture} currently selected texture 305 */ 306 JSTextureDialog.prototype.getSelectedTexture = function() { 307 if (this.selectedTextureModel.texture != null) { 308 return new HomeTexture( 309 this.selectedTextureModel.texture, 310 this.selectedTextureModel.xOffset, 311 this.selectedTextureModel.yOffset, 312 this.selectedTextureModel.angleInRadians, 313 this.selectedTextureModel.scale, 314 this.controller.getTexture() instanceof HomeTexture 315 ? this.controller.getTexture().isFittingArea() 316 : false, 317 true); 318 } else { 319 return null; 320 } 321 } 322 323 /** 324 * Applies given texture values to this dialog. 325 * @param {HomeTexture} texture 326 */ 327 JSTextureDialog.prototype.setSelectedTexture = function(texture) { 328 if (texture != null) { 329 this.selectedTextureModel.texture = texture; 330 this.selectedTextureModel.xOffset = texture.getXOffset(); 331 this.selectedTextureModel.yOffset = texture.getYOffset(); 332 this.selectedTextureModel.angleInRadians = texture.getAngle(); 333 this.selectedTextureModel.scale = texture.getScale(); 334 335 this.xOffsetInput.setValue(this.selectedTextureModel.xOffset * 100); 336 this.yOffsetInput.setValue(this.selectedTextureModel.yOffset * 100); 337 this.angleInput.setValue(Math.toDegrees(this.selectedTextureModel.angleInRadians)); 338 this.scaleInput.setValue(this.selectedTextureModel.scale * 100); 339 340 // Search texture in catalog 341 var textureContent = texture.getImage(); 342 var textureCategories = this.getUserPreferences().getTexturesCatalog().getCategories(); 343 var catalogTexture = null; 344 for (var i = 0; i < textureCategories.length && catalogTexture === null; i++) { 345 var categoryTextures = textureCategories[i].getTextures(); 346 for (var j = 0; j < categoryTextures.length; j++) { 347 if (textureContent.equals(categoryTextures[j].getImage())) { 348 catalogTexture = categoryTextures[j]; 349 break; 350 } 351 } 352 } 353 if (catalogTexture !== null) { 354 this.selectTexture(catalogTexture); 355 } else { 356 this.selectTexture(texture); 357 } 358 } else { 359 this.selectedTexturePreview.style.backgroundImage = "none"; 360 } 361 362 this.updateTextureTransform(); 363 } 364 365 /** 366 * @param {Texture} texture the texture to be selected 367 * @private 368 */ 369 JSTextureDialog.prototype.selectTexture = function(texture) { 370 this.selectedTextureModel.texture = texture; 371 var modifyTextureEnabled = false; 372 if (texture != null) { 373 for (var i = 0; i < this.texturesCatalogItems.length; i++) { 374 this.texturesCatalogItems[i].classList.remove("selected"); 375 } 376 if (texture instanceof CatalogTexture) { 377 var catalogTextureItem = this.getCatalogTextureItem(texture); 378 catalogTextureItem.classList.add("selected"); 379 var catalogList = this.catalogList; 380 setTimeout(function() { 381 var textureItemTop = catalogTextureItem.offsetTop - catalogList.offsetTop; 382 var textureItemBottom = textureItemTop + catalogTextureItem.clientHeight; 383 if (textureItemTop < catalogList.scrollTop || textureItemBottom > (catalogList.scrollTop + catalogList.clientHeight)) { 384 catalogList.scrollTop = textureItemTop - catalogList.offsetTop; 385 } 386 }, 10); 387 this.selectedTexturePreview.style.backgroundImage = "url('" + catalogTextureItem.querySelector("img").src + "')"; 388 modifyTextureEnabled = texture != null && texture.isModifiable(); 389 } else { 390 var dialog = this; 391 TextureManager.getInstance().loadTexture(texture.getImage(), 392 { 393 textureUpdated: function(image) { 394 dialog.selectedTexturePreview.style.backgroundImage = "url('" + image.src + "')"; 395 }, 396 textureError: function(error) { 397 dialog.selectedTexturePreview.style.backgroundImage = "none"; 398 } 399 }); 400 } 401 } else { 402 dialog.selectedTexturePreview.style.backgroundImage = "none"; 403 } 404 405 this.modifyTextureButton.disabled = !modifyTextureEnabled; 406 this.deleteTextureButton.disabled = !modifyTextureEnabled; 407 } 408 409 /** 410 * @private 411 */ 412 JSTextureDialog.prototype.updateTextureTransform = function() { 413 this.selectedTextureModel.xOffset = this.xOffsetInput.getValue() / 100; 414 this.selectedTextureModel.yOffset = this.yOffsetInput.getValue() / 100; 415 this.selectedTextureModel.angleInRadians = Math.toRadians(this.angleInput.getValue()); 416 this.selectedTextureModel.scale = this.scaleInput.getValue() / 100; 417 this.selectedTexturePreview.style.transform = 418 /* "translate(" + this.xOffsetInput.getValue() + "%, " + this.yOffsetInput.getValue() + "%)" */ 419 " rotate(" + this.angleInput.getValue() + "deg)" 420 /* + " scale(" + this.selectedTextureModel.scale + ", " + this.selectedTextureModel.scale + ")" */; 421 } 422 423 /** 424 * @param {CatalogTexture} catalogTexture 425 * @return {HTMLElement | null} null if no item found for given texture 426 * @private 427 */ 428 JSTextureDialog.prototype.getCatalogTextureItem = function(catalogTexture) { 429 if (catalogTexture != null) { 430 var catalogContent = catalogTexture.getImage(); 431 for (var i = 0; i < this.texturesCatalogItems.length; i++) { 432 var item = this.texturesCatalogItems[i]; 433 var itemContent = this.getCatalogTextureFromItem(item).getImage(); 434 if (catalogContent.equals(itemContent)) { 435 return item; 436 } 437 } 438 } 439 return null; 440 } 441 442 /** 443 * @param {HTMLElement} item 444 * @return {CatalogTexture} matching texture 445 * @private 446 */ 447 JSTextureDialog.prototype.getCatalogTextureFromItem = function(item) { 448 return item._catalogTexture; 449 } 450 451 /** 452 * @param {UserPreferences} preferences 453 * @private 454 */ 455 JSTextureDialog.prototype.initCatalogTextureSearch = function(preferences) { 456 var dialog = this; 457 this.searchInput = this.findElement(".texture-search input"); 458 this.searchInput.placeholder = ResourceAction.getLocalizedLabelText(preferences, "TextureChoiceComponent", "searchLabel.text").replace(":", ""); 459 this.registerEventListener(dialog.searchInput, "input", function() { 460 var valueToSearch = CoreTools.removeAccents(dialog.searchInput.value.trim()); 461 for (var i = 0; i < dialog.texturesCatalogItems.length; i++) { 462 var item = dialog.texturesCatalogItems[i]; 463 var textureDescriptor = item._catalogTexture.getName() + "|" + item._catalogTexture.getCategory().getName(); 464 if (item._catalogTexture.getCreator() !== null) { 465 textureDescriptor += "|" + item._catalogTexture.getCreator(); 466 } 467 if (RegExp(valueToSearch, "i").test(CoreTools.removeAccents(textureDescriptor))) { 468 item.classList.remove("hidden"); 469 } else { 470 item.classList.add("hidden"); 471 } 472 } 473 }); 474 this.searchInput.addEventListener("focusin", function(ev) { 475 setTimeout(function() { 476 if (dialog.searchInput.value != "") { 477 dialog.searchInput.select(); 478 } 479 }, 100); 480 }); 481 } 482 483 /** 484 * @private 485 */ 486 JSTextureDialog.prototype.initRecentTextures = function() { 487 var dialog = this; 488 this.recentTexturesPanel.innerHTML = ""; 489 var recentTextures = this.getUserPreferences().getRecentTextures(); 490 for (var i = 0; i < recentTextures.length; i++) { 491 var recentTexture = recentTextures[i]; 492 var recentTextureElement = document.createElement("div"); 493 recentTextureElement.classList.add("item"); 494 recentTextureElement._catalogTexture = recentTexture; 495 this.recentTexturesPanel.appendChild(recentTextureElement); 496 TextureManager.getInstance().loadTexture(recentTexture.getImage(), 497 { 498 textureUpdated: function(image) { 499 recentTextureElement.style.backgroundImage = "url('" + image.src + "')"; 500 }, 501 textureError: function(error) { 502 dialog.recentTexturesPanel.removeChild(recentTextureElement); 503 } 504 }); 505 } 506 507 var dialog = this; 508 this.registerEventListener(this.recentTexturesPanel.childNodes, "click", function(ev) { 509 dialog.selectTexture(dialog.getCatalogTextureFromItem(this)); 510 }); 511 } 512 513 /** 514 * @private 515 */ 516 JSTextureDialog.prototype.initImportTexturesPanel = function() { 517 this.importTextureButton = this.findElement(".imported-textures-panel [import]"); 518 this.modifyTextureButton = this.findElement(".imported-textures-panel [modify]"); 519 this.deleteTextureButton = this.findElement(".imported-textures-panel [delete]"); 520 521 var dialog = this; 522 var controller = this.controller; 523 this.registerEventListener(this.importTextureButton, "click", function(ev) { 524 controller.importTexture(); 525 }); 526 this.registerEventListener(this.modifyTextureButton, "click", function(ev) { 527 controller.modifyTexture(dialog.selectedTextureModel.texture); 528 }); 529 this.registerEventListener(this.deleteTextureButton, "click", function(ev) { 530 controller.deleteTexture(dialog.selectedTextureModel.texture); 531 }); 532 } 533