1 /* 2 * SweetHome3DJSApplication.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 SweetHome3D.js 22 // Requires HomeRecorder.js 23 // Requires UserPreferences.js 24 // Requires HomePane.js 25 // Requires JSViewFactory.js 26 27 /** 28 * Creates a home controller handling savings for local files. 29 * @param {Home} [home] the home controlled by this controller 30 * @param {HomeApplication} [application] 31 * @param {ViewFactory} [viewFactory] 32 * @constructor 33 * @author Emmanuel Puybaret 34 * @ignore 35 */ 36 function LocalFileHomeController(home, application, viewFactory) { 37 HomeController.call(this, home, application, viewFactory); 38 } 39 LocalFileHomeController.prototype = Object.create(HomeController.prototype); 40 LocalFileHomeController.prototype.constructor = LocalFileHomeController; 41 42 /** 43 * Creates a new home after closing the current home. 44 */ 45 LocalFileHomeController.prototype.newHome = function() { 46 var controller = this; 47 var newHomeTask = function() { 48 controller.close(); 49 controller.application.addHome(controller.application.createHome()); 50 }; 51 52 if (this.home.isModified() || this.home.isRecovered()) { 53 this.getView().confirmSave(this.application.configuration === undefined || this.home.getName() !== this.application.configuration.defaultHomeName ? this.home.getName() : null, 54 function(save) { 55 if (save) { 56 controller.save(newHomeTask); 57 } else { 58 newHomeTask(); 59 } 60 }); 61 } else { 62 newHomeTask(); 63 } 64 } 65 66 /** 67 * Opens a home chosen by the user. 68 */ 69 LocalFileHomeController.prototype.open = function() { 70 var controller = this; 71 var openHome = function(homeName) { 72 var preferences = this.application.getUserPreferences(); 73 var openingTaskDialog = new JSDialog(preferences, 74 preferences.getLocalizedString("ThreadedTaskPanel", "threadedTask.title"), 75 preferences.getLocalizedString("HomeController", "openMessage"), { size: "small" }); 76 openingTaskDialog.findElement(".dialog-cancel-button").style = "display: none"; 77 78 var fileInput = document.createElement("input"); 79 fileInput.setAttribute("style", "display: none"); 80 fileInput.setAttribute("type", "file"); 81 document.body.appendChild(fileInput); 82 fileInput.addEventListener("change", function(ev) { 83 document.body.removeChild(fileInput); 84 if (this.files[0]) { 85 openingTaskDialog.displayView(); 86 var file = this.files[0]; 87 setTimeout(function() { 88 var homeName = file.name.substring(file.name.indexOf("/") + 1); 89 controller.application.getHomeRecorder().readHome(URL.createObjectURL(file), { 90 homeLoaded: function(home) { 91 // Do not set home name because file name may have been altered automatically by browser when saved 92 controller.close(); 93 openingTaskDialog.close(); 94 controller.application.addHome(home); 95 }, 96 homeError: function(error) { 97 openingTaskDialog.close(); 98 var message = preferences. 99 getLocalizedString("HomeController", "openError", homeName) + "\n" + error; 100 console.log(error); 101 alert(message); 102 } 103 }); 104 }, 100); 105 } 106 }); 107 fileInput.click(); 108 }; 109 110 if (this.home.isModified() || this.home.isRecovered()) { 111 this.getView().confirmSave(this.application.configuration === undefined || this.home.getName() !== this.application.configuration.defaultHomeName ? this.home.getName() : null, 112 function(save) { 113 if (save) { 114 controller.save(openHome); 115 } else { 116 openHome(); 117 } 118 }); 119 } else { 120 openHome(); 121 } 122 } 123 124 /** 125 * Saves the home managed by this controller. If home name doesn't exist, 126 * this method will act as {@link #saveAs() saveAs} method. 127 * @param {function} [postSaveTask] 128 */ 129 LocalFileHomeController.prototype.save = function(postSaveTask) { 130 if (this.home.getName() == null 131 || (this.application.configuration !== undefined && this.home.getName() === this.application.configuration.defaultHomeName)) { 132 this.saveAs(postSaveTask); 133 } else { 134 var preferences = this.application.getUserPreferences(); 135 var savingTaskDialog = new JSDialog(preferences, 136 preferences.getLocalizedString("ThreadedTaskPanel", "threadedTask.title"), 137 preferences.getLocalizedString("HomeController", "saveMessage"), 138 { 139 size: "small", 140 disposer: function(dialog) { 141 if (dialog.writingOperation !== undefined) { 142 dialog.writingOperation.abort(); 143 } 144 } 145 }); 146 if (this.application.getHomeRecorder().configuration && this.application.getHomeRecorder().configuration.writeHomeWithWorker) { 147 savingTaskDialog.findElement(".dialog-cancel-button").innerHTML = 148 ResourceAction.getLocalizedLabelText(preferences, "ThreadedTaskPanel", "cancelButton.text"); 149 } else { 150 savingTaskDialog.findElement(".dialog-cancel-button").style = "display: none"; 151 } 152 savingTaskDialog.displayView(); 153 154 var controller = this; 155 var homeExtension = application.getUserPreferences().getLocalizedString("FileContentManager", "homeExtension"); // .sh3d 156 var homeExtension2 = application.getUserPreferences().getLocalizedString("FileContentManager", "homeExtension2"); // .sh3x 157 var homeName = controller.home.getName().replace(homeExtension, homeExtension2); 158 setTimeout(function() { 159 savingTaskDialog.writingOperation = controller.application.getHomeRecorder().writeHome(controller.home, homeName, { 160 homeSaved: function(home, blob) { 161 delete savingTaskDialog.writingOperation; 162 savingTaskDialog.close(); 163 if (navigator.msSaveOrOpenBlob !== undefined) { 164 navigator.msSaveOrOpenBlob(blob, homeName); 165 } else { 166 var downloadLink = document.createElement('a'); 167 downloadLink.setAttribute("style", "display: none"); 168 downloadLink.setAttribute("href", URL.createObjectURL(blob)); 169 downloadLink.setAttribute("download", homeName); 170 document.body.appendChild(downloadLink); 171 downloadLink.click(); 172 setTimeout(function() { 173 document.body.removeChild(downloadLink); 174 URL.revokeObjectURL(downloadLink.getAttribute("href")); 175 }, 500); 176 } 177 home.setModified(false); 178 home.setRecovered(false); 179 if (postSaveTask !== undefined) { 180 postSaveTask(); 181 } 182 }, 183 homeError: function(status, error) { 184 savingTaskDialog.close(); 185 console.log(status + " " + error); 186 new JSDialog(preferences, 187 preferences.getLocalizedString("HomePane", "error.title"), 188 preferences.getLocalizedString("HomeController", "saveError", homeName, status + "<br>" + error), 189 { size: "small" }).displayView(); 190 } 191 }); 192 }, 200); // Add a little delay to ensure savingTaskDialog is displayed immediately and when no worker started 193 } 194 } 195 196 /** 197 * Saves the home managed by this controller with a different name. 198 * @param {function} [postSaveTask] 199 */ 200 LocalFileHomeController.prototype.saveAs = function(postSaveTask) { 201 var preferences = this.application.getUserPreferences(); 202 var message = preferences.getLocalizedString("AppletContentManager", "showSaveDialog.message"); 203 var homeName = prompt(message); 204 if (homeName != null && homeName.length > 0) { 205 var homeExtension2 = application.getUserPreferences().getLocalizedString("FileContentManager", "homeExtension2"); // .sh3x 206 this.home.setName(homeName + (homeName.indexOf('.') < 0 ? homeExtension2 : "")); 207 this.save(postSaveTask); 208 } 209 } 210 211 /** 212 * Removes home from application homes list. 213 */ 214 LocalFileHomeController.prototype.close = function() { 215 this.home.setRecovered(false); 216 this.application.deleteHome(this.home); 217 this.getView().dispose(); 218 } 219 220 221 /** 222 * Creates a home controller handling savings from user interface. 223 * @param {Home} [home] the home controlled by this controller 224 * @param {HomeApplication} [application] 225 * @param {ViewFactory} [viewFactory] 226 * @constructor 227 * @author Emmanuel Puybaret 228 * @ignore 229 */ 230 function DirectRecordingHomeController(home, application, viewFactory) { 231 HomeController.call(this, home, application, viewFactory); 232 } 233 DirectRecordingHomeController.prototype = Object.create(HomeController.prototype); 234 DirectRecordingHomeController.prototype.constructor = DirectRecordingHomeController; 235 236 /** 237 * Creates a new home after saving and closing the current home. 238 */ 239 DirectRecordingHomeController.prototype.newHome = function() { 240 var controller = this; 241 var newHomeTask = function() { 242 controller.close(); 243 controller.application.addHome(controller.application.createHome()); 244 }; 245 246 if (this.home.isModified() || this.home.isRecovered()) { 247 this.getView().confirmSave(this.application.configuration === undefined || this.home.getName() !== this.application.configuration.defaultHomeName ? this.home.getName() : null, 248 function(save) { 249 if (save) { 250 controller.save(newHomeTask); 251 } else { 252 newHomeTask(); 253 } 254 }); 255 } else { 256 newHomeTask(); 257 } 258 } 259 260 /** 261 * Opens a home after saving and deleting the current home. 262 */ 263 DirectRecordingHomeController.prototype.open = function() { 264 var controller = this; 265 var preferences = controller.application.getUserPreferences(); 266 var readTask = function(homeName) { 267 if (homeName != null && homeName.length > 0) { 268 controller.application.getHomeRecorder().readHome(homeName, 269 { 270 homeLoaded: function(home) { 271 home.setName(homeName); 272 controller.close(); 273 controller.application.addHome(home); 274 }, 275 homeError: function(error) { 276 var message = preferences.getLocalizedString("HomeController", "openError", homeName) + "\n" + error; 277 console.log(error); 278 alert(message); 279 }, 280 progression: function(part, info, percentage) { 281 } 282 }); 283 } 284 }; 285 var selectHome = function() { 286 var request = controller.application.getHomeRecorder().getAvailableHomes({ 287 availableHomes: function(homes) { 288 if (homes.length == 0) { 289 var message = preferences.getLocalizedString("AppletContentManager", "showOpenDialog.noAvailableHomes"); 290 alert(message); 291 } else { 292 var html = 293 ' <div class="column1">' + 294 ' <div>@{AppletContentManager.showOpenDialog.message}</div>' + 295 ' <div class="home-list"></div>' + 296 ' </div>'; 297 var fileDialog = new JSDialog(preferences, "@{FileContentManager.openDialog.title}", html, 298 { 299 size: "small", 300 applier: function(dialog) { 301 var selectedItem = fileDialog.findElement(".selected"); 302 if (selectedItem != null) { 303 readTask(selectedItem.innerText); 304 } 305 }, 306 }); 307 fileDialog.getHTMLElement().classList.add("open-dialog"); 308 var okButton = fileDialog.findElement(".dialog-ok-button"); 309 okButton.innerHTML = preferences.getLocalizedString("AppletContentManager", "showOpenDialog.open"); 310 okButton.disabled = true; 311 var cancelButton = fileDialog.findElement(".dialog-cancel-button"); 312 cancelButton.innerHTML = preferences.getLocalizedString("AppletContentManager", "showOpenDialog.cancel"); 313 var deleteButton = document.createElement("button"); 314 deleteButton.innerHTML = preferences.getLocalizedString("AppletContentManager", "showOpenDialog.delete"); 315 deleteButton.disabled = true; 316 cancelButton.parentElement.insertBefore(deleteButton, cancelButton); 317 var homeList = fileDialog.findElement(".home-list"); 318 319 for (var i = 0; i < homes.length; i++) { 320 var item = document.createElement("div"); 321 item.classList.add("item"); 322 item.innerHTML = homes [i]; 323 homeList.appendChild(item); 324 } 325 326 var items = homeList.childNodes; 327 fileDialog.registerEventListener(items, "click", function(ev) { 328 for (var i = 0; i < items.length; i++) { 329 if (ev.target == items [i]) { 330 items [i].classList.add("selected"); 331 okButton.disabled = false; 332 deleteButton.disabled = ev.target.innerHTML == controller.home.getName(); 333 } else { 334 items [i].classList.remove("selected"); 335 } 336 } 337 }); 338 fileDialog.registerEventListener(items, "dblclick", function() { 339 fileDialog.validate(); 340 }); 341 fileDialog.registerEventListener(deleteButton, "click", function(ev) { 342 var item = fileDialog.findElement(".selected"); 343 controller.confirmDeleteHome(item.innerText, function() { 344 controller.application.getHomeRecorder().deleteHome(item.innerText, { 345 homeDeleted: function() { 346 item.remove(); 347 okButton.disabled = true; 348 deleteButton.disabled = true; 349 }, 350 homeError: function(status, error) { 351 var message = preferences.getLocalizedString("AppletContentManager", "confirmDeleteHome.errorMessage", item.innerText); 352 console.log(message + " : " + error); 353 alert(message); 354 } 355 }); 356 }); 357 }); 358 fileDialog.displayView(); 359 } 360 }, 361 homesError: function(status, error) { 362 var message = preferences.getLocalizedString("AppletContentManager", "showOpenDialog.availableHomesError"); 363 console.log(message + " : " + error); 364 alert(message); 365 } 366 }); 367 368 if (request == null) { 369 var message = preferences.getLocalizedString("AppletContentManager", "showOpenDialog.message"); 370 readTask(prompt(message)); 371 } 372 }; 373 374 if (this.home.isModified() || this.home.isRecovered()) { 375 this.getView().confirmSave(this.application.configuration === undefined || this.home.getName() !== this.application.configuration.defaultHomeName ? this.home.getName() : null, 376 function(save) { 377 if (save) { 378 controller.save(selectHome); 379 } else { 380 selectHome(); 381 } 382 }); 383 } else { 384 selectHome(); 385 } 386 } 387 388 /** 389 * Saves the home managed by this controller. If home name doesn't exist, 390 * this method will act as {@link #saveAs() saveAs} method. 391 * @param {function} [postSaveTask] 392 */ 393 DirectRecordingHomeController.prototype.save = function(postSaveTask) { 394 if (this.home.getName() == null 395 || (this.application.configuration !== undefined && this.home.getName() === this.application.configuration.defaultHomeName)) { 396 this.saveAs(postSaveTask); 397 } else { 398 var preferences = this.application.getUserPreferences(); 399 var savingTaskDialog = new JSDialog(preferences, 400 preferences.getLocalizedString("ThreadedTaskPanel", "threadedTask.title"), 401 preferences.getLocalizedString("HomeController", "saveMessage"), 402 { 403 size: "small", 404 disposer: function(dialog) { 405 if (dialog.writingOperation !== undefined) { 406 dialog.writingOperation.abort(); 407 } 408 } 409 }); 410 savingTaskDialog.findElement(".dialog-cancel-button").innerHTML = 411 ResourceAction.getLocalizedLabelText(preferences, "ThreadedTaskPanel", "cancelButton.text"); 412 savingTaskDialog.displayView(); 413 414 var controller = this; 415 savingTaskDialog.writingOperation = this.application.getHomeRecorder().writeHome(this.home, this.home.getName(), { 416 homeSaved: function(home) { 417 delete savingTaskDialog.writingOperation; 418 savingTaskDialog.close(); 419 home.setModified(false); 420 home.setRecovered(false); 421 if (postSaveTask !== undefined) { 422 postSaveTask(); 423 } 424 }, 425 homeError: function(status, error) { 426 savingTaskDialog.close(); 427 new JSDialog(preferences, 428 preferences.getLocalizedString("HomePane", "error.title"), 429 preferences.getLocalizedString("HomeController", "saveError", [controller.home.getName(), error]), 430 { size: "small" }).displayView(); 431 } 432 }); 433 } 434 } 435 436 /** 437 * Saves the home managed by this controller with a different name. 438 * @param {function} [postSaveTask] 439 */ 440 DirectRecordingHomeController.prototype.saveAs = function(postSaveTask) { 441 var preferences = this.application.getUserPreferences(); 442 var message = preferences.getLocalizedString("AppletContentManager", "showSaveDialog.message"); 443 var homeName = prompt(message, this.home.getName() != null 444 && (this.application.configuration === undefined 445 || this.home.getName() !== this.application.configuration.defaultHomeName) 446 ? this.home.getName() 447 : undefined); 448 449 if (homeName != null && homeName.length > 0) { 450 var controller = this; 451 var request = controller.application.getHomeRecorder().getAvailableHomes({ 452 availableHomes: function(homeNames) { 453 var homeExists = false; 454 for (var i = 0; i < homeNames.length; i++) { 455 if (homeName == homeNames [i]) { 456 homeExists = true; 457 } 458 } 459 if (!homeExists) { 460 controller.home.setName(homeName); 461 controller.save(postSaveTask); 462 } else { 463 var message = preferences.getLocalizedString("FileContentManager", "confirmOverwrite.message", homeName).replace(/\<br\>/g, " "); 464 var confirmOverwriteDialog = new JSDialog(preferences, 465 preferences.getLocalizedString("FileContentManager", "confirmOverwrite.title"), 466 message + "</font>", 467 { 468 size: "small", 469 applier: function() { 470 controller.home.setName(homeName); 471 controller.save(postSaveTask); 472 } 473 }); 474 confirmOverwriteDialog.findElement(".dialog-ok-button").innerHTML = 475 preferences.getLocalizedString("FileContentManager", "confirmOverwrite.overwrite"); 476 var cancelButton = confirmOverwriteDialog.findElement(".dialog-cancel-button"); 477 cancelButton.innerHTML = preferences.getLocalizedString("FileContentManager", "confirmOverwrite.cancel"); 478 confirmOverwriteDialog.displayView(); 479 } 480 }, 481 homesError: function(status, error) { 482 var message = preferences.getLocalizedString("AppletContentManager", "showSaveDialog.checkHomeError"); 483 console.log(message + " : " + error); 484 alert(message); 485 } 486 }); 487 488 if (request == null) { 489 this.home.setName(homeName); 490 this.save(postSaveTask); 491 }; 492 } 493 } 494 495 /** 496 * Removes home from application homes list. 497 */ 498 DirectRecordingHomeController.prototype.close = function() { 499 this.home.setRecovered(false); 500 this.application.deleteHome(this.home); 501 this.getView().dispose(); 502 } 503 504 /** 505 * Displays a dialog that lets user choose whether he wants to delete 506 * a home or not, then calls <code>confirm</code>. 507 * @param {function} confirm 508 * @private 509 */ 510 DirectRecordingHomeController.prototype.confirmDeleteHome = function(homeName, confirm) { 511 var preferences = this.application.getUserPreferences(); 512 var message = preferences.getLocalizedString("AppletContentManager", "confirmDeleteHome.message", homeName).replace(/\<br\>/g, " "); 513 var confirmDeletionDialog = new JSDialog(preferences, 514 preferences.getLocalizedString("AppletContentManager", "confirmDeleteHome.title"), 515 message + "</font>", 516 { 517 size: "small", 518 applier: function() { 519 confirm(); 520 } 521 }); 522 confirmDeletionDialog.findElement(".dialog-ok-button").innerHTML = 523 preferences.getLocalizedString("AppletContentManager", "confirmDeleteHome.delete"); 524 var cancelButton = confirmDeletionDialog.findElement(".dialog-cancel-button"); 525 cancelButton.innerHTML = preferences.getLocalizedString("AppletContentManager", "confirmDeleteHome.cancel"); 526 confirmDeletionDialog.displayView(); 527 } 528 529 530 /** 531 * <code>HomeApplication</code> implementation for JavaScript. 532 * @param {{furnitureCatalogURLs: string[], 533 * furnitureResourcesURLBase: string, 534 * texturesCatalogURLs: string[], 535 * texturesResourcesURLBase: string, 536 * readHomeURL: string, 537 * writeHomeEditsURL|writeHomeURL: string, 538 * closeHomeURL: string, 539 * writeResourceURL: string, 540 * readResourceURL: string, 541 * writePreferencesURL: string, 542 * readPreferencesURL: string, 543 * writePreferencesResourceURL: string, 544 * readPreferencesResourceURL: string, 545 * pingURL: string, 546 * autoWriteDelay: number, 547 * trackedHomeProperties: string[], 548 * autoWriteTrackedStateChange: boolean, 549 * defaultUserLanguage: string, 550 * writeCacheResourceURL: string, 551 * readCacheResourceURL: string, 552 * listCacheResourcesURL: string, 553 * listHomesURL: string, 554 * deleteHomeURL: string, 555 * autoRecovery: boolean, 556 * autoRecoveryDatabase: string, 557 * autoRecoveryObjectstore: string, 558 * silentAutoRecovery: boolean, 559 * compressionLevel: number, 560 * includeAllContent: boolean, 561 * writeDataType: string, 562 * writeHomeWithWorker: boolean, 563 * defaultHomeName: string, 564 * writingObserver: {writeStarted: Function, 565 * writeSucceeded: Function, 566 * writeFailed: Function, 567 * connectionFound: Function, 568 * connectionLost: Function}} [configuration] 569 * the URLs of resources and services required on server 570 * (if undefined, will use local files for testing). 571 * If writePreferencesResourceURL / readPreferencesResourceURL is missing, 572 * writeResourceURL / readResourceURL will be used. 573 * If writeHomeEditsURL and readHomeURL are missing, application recorder will be 574 * an instance of <code>HomeRecorder</code>. 575 * If writeHomeEditsURL is missing, application recorder will be 576 * an instance of <code>DirectHomeRecorder</code>. 577 * Auto recovery not available for incremental recorder. 578 * @constructor 579 * @author Emmanuel Puybaret 580 * @author Renaud Pawlak 581 */ 582 function SweetHome3DJSApplication(configuration) { 583 HomeApplication.call(this); 584 this.homeControllers = []; 585 this.configuration = configuration; 586 var application = this; 587 this.addHomesListener(function(ev) { 588 if (ev.getType() == CollectionEvent.Type.ADD) { 589 var homeController = application.createHomeController(ev.getItem()); 590 application.homeControllers.push(homeController); 591 if (application.getHomeRecorder() instanceof IncrementalHomeRecorder) { 592 application.getHomeRecorder().addHome(ev.getItem(), homeController); 593 } 594 homeController.getView(); 595 } else if (ev.getType() == CollectionEvent.Type.DELETE) { 596 application.homeControllers.splice(ev.getIndex(), 1); 597 if (application.getHomeRecorder() instanceof IncrementalHomeRecorder) { 598 application.getHomeRecorder().removeHome(ev.getItem()); 599 } 600 } 601 }); 602 603 if (this.configuration !== undefined 604 && this.configuration.autoRecovery 605 && this.configuration.writeHomeEditsURL === undefined) { 606 setTimeout(function() { 607 // Launch auto recovery manager 608 application.autoRecoveryManager = new AutoRecoveryManager(application); 609 }); 610 } 611 } 612 SweetHome3DJSApplication.prototype = Object.create(HomeApplication.prototype); 613 SweetHome3DJSApplication.prototype.constructor = SweetHome3DJSApplication; 614 615 SweetHome3DJSApplication.prototype.getVersion = function() { 616 return "7.5"; 617 } 618 619 SweetHome3DJSApplication.prototype.getHomeRecorder = function() { 620 if (!this.homeRecorder) { 621 this.homeRecorder = this.configuration === undefined || this.configuration.readHomeURL === undefined 622 ? new HomeRecorder(this.configuration) 623 : (this.configuration.writeHomeEditsURL !== undefined 624 ? new IncrementalHomeRecorder(this, this.configuration) 625 : new DirectHomeRecorder(this.configuration)); 626 } 627 return this.homeRecorder; 628 } 629 630 SweetHome3DJSApplication.prototype.getUserPreferences = function() { 631 if (this.preferences == null) { 632 if (this.configuration === undefined) { 633 this.preferences = new DefaultUserPreferences(); 634 } else { 635 this.preferences = new RecordedUserPreferences(this.configuration); 636 } 637 } 638 return this.preferences; 639 } 640 641 SweetHome3DJSApplication.prototype.createHome = function() { 642 var home = HomeApplication.prototype.createHome.call(this); 643 if (this.configuration !== undefined && this.configuration.defaultHomeName !== undefined) { 644 home.setName(this.configuration.defaultHomeName); 645 } 646 return home; 647 } 648 649 /** 650 * Returns the view factory which will create the views associated to their controllers. 651 * @return {Object} 652 */ 653 SweetHome3DJSApplication.prototype.getViewFactory = function() { 654 if (this.viewFactory == null) { 655 this.viewFactory = new JSViewFactory(this); 656 } 657 return this.viewFactory; 658 } 659 660 /** 661 * Create the <code>HomeController</code> which controls the given <code>home</code>. 662 * @param {Home} home 663 */ 664 SweetHome3DJSApplication.prototype.createHomeController = function(home) { 665 return this.configuration === undefined || this.configuration.readHomeURL === undefined 666 ? new LocalFileHomeController(home, this, this.getViewFactory()) 667 : (this.configuration.writeHomeEditsURL !== undefined 668 ? new HomeController(home, this, this.getViewFactory()) 669 : new DirectRecordingHomeController(home, this, this.getViewFactory())); 670 } 671 672 /** 673 * Returns the <code>HomeController</code> associated to the given <code>home</code>. 674 * @return {HomeController} 675 */ 676 SweetHome3DJSApplication.prototype.getHomeController = function(home) { 677 return this.homeControllers[this.getHomes().indexOf(home)]; 678 } 679 680 /** 681 * Manager able to automatically save open homes in recovery database with a timer. 682 * The delay between two automatic save operations is specified by 683 * {@link UserPreferences#getAutoSaveDelayForRecovery() auto save delay for recovery} 684 * property. 685 * @constructor 686 * @param {SweetHome3DJSApplication} application 687 * @ignore 688 * @author Emmanuel Puybaret 689 */ 690 function AutoRecoveryManager(application) { 691 this.application = application; 692 var autoRecoveryDatabase = "SweetHome3DJS"; 693 var autoRecoveryObjectstore = "Recovery"; 694 if (application.configuration.autoRecoveryDatabase !== undefined) { 695 autoRecoveryDatabase = application.configuration.autoRecoveryDatabase; 696 } 697 if (application.configuration.autoRecoveryObjectstore !== undefined) { 698 autoRecoveryObjectstore = application.configuration.autoRecoveryObjectstore; 699 } 700 701 var manager = this; 702 this.autoRecoveryDatabaseUrlBase = "indexeddb://" + autoRecoveryDatabase + "/" + autoRecoveryObjectstore; 703 // Auto recovery recorder stores data in autoRecoveryObjectstore object store of IndexedDB 704 function AutoRecoveryRecorder() { 705 HomeRecorder.call(this, { 706 readHomeURL: manager.autoRecoveryDatabaseUrlBase + "/content?name=%s.recovered", 707 writeHomeURL: manager.autoRecoveryDatabaseUrlBase + "?keyPathField=name&contentField=content&dateField=date&name=%s.recovered", 708 readResourceURL: manager.autoRecoveryDatabaseUrlBase + "/content?name=%s", 709 writeResourceURL: manager.autoRecoveryDatabaseUrlBase + "?keyPathField=name&contentField=content&dateField=date&name=%s", 710 listHomesURL: manager.autoRecoveryDatabaseUrlBase + "?name=(.*).recovered", 711 deleteHomeURL: manager.autoRecoveryDatabaseUrlBase + "?name=%s.recovered", 712 writeHomeWithWorker: true 713 }); 714 } 715 AutoRecoveryRecorder.prototype = Object.create(DirectHomeRecorder.prototype); 716 AutoRecoveryRecorder.prototype.constructor = DirectHomeRecorder; 717 718 // Reuse XML handler of application recorder 719 AutoRecoveryRecorder.prototype.getHomeXMLHandler = function() { 720 return application.getHomeRecorder().getHomeXMLHandler(); 721 } 722 723 // Reuse XML exporter of application recorder 724 AutoRecoveryRecorder.prototype.getHomeXMLExporter = function() { 725 return application.getHomeRecorder().getHomeXMLExporter(); 726 } 727 728 this.autoSaveRecorder = new AutoRecoveryRecorder(); 729 this.recoveredHomeNames = []; 730 var homeExtension1 = application.getUserPreferences().getLocalizedString("FileContentManager", "homeExtension"); 731 var homeExtension2 = application.getUserPreferences().getLocalizedString("FileContentManager", "homeExtension2"); 732 var homeModificationListener = function(ev) { 733 var home = ev.getSource(); 734 if (!home.isModified()) { 735 home.removePropertyChangeListener("MODIFIED", homeModificationListener); 736 // Delete auto saved in 1s in case user was traversing quickly the undo/redo pile 737 setTimeout(function() { 738 if (!home.isModified()) { 739 manager.deleteRecoveredHome(home.getName()); 740 } 741 home.addPropertyChangeListener("MODIFIED", homeModificationListener); 742 }, 1000); 743 } 744 }; 745 var homeNameModificationListener = function(ev) { 746 manager.recoveredHomeNames.splice(manager.recoveredHomeNames.indexOf(ev.getOldValue()), 1); 747 if (!ev.getSource().isRecovered()) { 748 manager.deleteRecoveredHome(ev.getOldValue()); 749 } 750 manager.recoveredHomeNames.push(ev.getNewValue()); 751 }; 752 var homesListener = function(ev) { 753 var home = ev.getItem(); 754 if (ev.getType() == CollectionEvent.Type.ADD) { 755 manager.autoSaveRecorder.getAvailableHomes({ 756 availableHomes: function(homeNames) { 757 if (home.getName() != null) { 758 manager.recoveredHomeNames.push(home.getName()); 759 } 760 var recoveredHome = false; 761 for (var i = 0; i < homeNames.length; i++) { 762 if (home.getName() != null 763 && this.homeNamesEqual(home.getName(), homeNames [i])) { 764 if (application.configuration.silentAutoRecovery 765 || confirm(application.getUserPreferences().getLocalizedString("SweetHome3DJSApplication", "confirmRecoverHome"))) { 766 recoveredHome = true; 767 manager.autoSaveRecorder.readHome(homeNames [i], { 768 homeLoaded: function(replacingHome) { 769 application.removeHomesListener(homesListener); 770 application.getHomeController(home).close(); 771 var homeName = replacingHome.getName(); 772 replacingHome.setRecovered(true); 773 replacingHome.addPropertyChangeListener("RECOVERED", function(ev) { 774 if (!replacingHome.isRecovered()) { 775 manager.recoveredHomeNames.splice(manager.recoveredHomeNames.indexOf(replacingHome.getName()), 1); 776 manager.deleteRecoveredHome(homeName); 777 replacingHome.addPropertyChangeListener("MODIFIED", homeModificationListener); 778 } 779 }); 780 replacingHome.addPropertyChangeListener("NAME", homeNameModificationListener); 781 application.addHome(replacingHome); 782 application.addHomesListener(homesListener); 783 }, 784 homeError: function(error) { 785 var message = application.getUserPreferences(). 786 getLocalizedString("HomeController", "openError", home.getName()) + "\n" + error; 787 console.log(message); 788 alert(message); 789 }, 790 }); 791 } else { 792 manager.recoveredHomeNames.splice(manager.recoveredHomeNames.indexOf(home.getName()), 1); 793 manager.deleteRecoveredHome(homeNames [i]); 794 } 795 break; 796 } 797 } 798 799 if (!recoveredHome) { 800 home.addPropertyChangeListener("MODIFIED", homeModificationListener); 801 home.addPropertyChangeListener("NAME", homeNameModificationListener); 802 if (home.isModified()) { 803 manager.saveRecoveredHomes(); 804 manager.restartTimer(); 805 } 806 } 807 }, 808 homesError: function(status, error) { 809 console.log("Couldn't retrieve homes from database : " + status + " " + error); 810 }, 811 homeNamesEqual: function(name1, name2) { 812 // If both names ends by a home extension 813 var name1Extension1Index = name1.indexOf(homeExtension1, name1.length - homeExtension1.length); 814 var name1Extension2Index = name1.indexOf(homeExtension2, name1.length - homeExtension2.length); 815 var name2Extension1Index = name2.indexOf(homeExtension1, name2.length - homeExtension1.length); 816 var name2Extension2Index = name2.indexOf(homeExtension2, name2.length - homeExtension2.length); 817 if ((name1Extension1Index > 0 || name1Extension2Index > 0) 818 && (name2Extension1Index > 0 || name2Extension2Index > 0)) { 819 var name1WithoutExtension = name1Extension1Index > 0 820 ? name1.substring(0, name1Extension1Index) 821 : name1.substring(0, name1Extension2Index); 822 var name2WithoutExtension = name2Extension1Index > 0 823 ? name2.substring(0, name2Extension1Index) 824 : name2.substring(0, name2Extension2Index); 825 return name1WithoutExtension === name2WithoutExtension; 826 } else { 827 return name1 === name2; 828 } 829 } 830 }); 831 } else if (ev.getType() == CollectionEvent.Type.DELETE 832 && home.getName() != null) { 833 if (manager.recoveredHomeNames.indexOf(home.getName()) >= 0) { 834 manager.recoveredHomeNames.splice(manager.recoveredHomeNames.indexOf(home.getName()), 1); 835 manager.deleteRecoveredHome(home.getName()); 836 } 837 home.removePropertyChangeListener("MODIFIED", homeModificationListener); 838 } 839 }; 840 application.addHomesListener(homesListener); 841 842 this.lastAutoSaveTime = Date.now(); 843 application.getUserPreferences().addPropertyChangeListener("AUTO_SAVE_DELAY_FOR_RECOVERY", function(ev) { 844 manager.restartTimer(); 845 }); 846 this.restartTimer(); 847 } 848 849 /** 850 * Saves now modified document in auto recovery. 851 * @ignore 852 */ 853 AutoRecoveryManager.prototype.saveRecoveredHomes = function() { 854 var homes = this.application.getHomes(); 855 for (var i = 0; i < homes.length; i++) { 856 var home = homes [i]; 857 if (home.getName() != null) { 858 if (home.isModified()) { 859 this.autoSaveRecorder.writeHome(home, home.getName(), { 860 homeSaved: function(home) { 861 }, 862 homeError: function(status, error) { 863 console.log("Couldn't save home for recovery : " + status + " " + error); 864 } 865 }); 866 } else if (this.recoveredHomeNames.indexOf(home.getName()) < 0) { 867 this.deleteRecoveredHome(home.getName()); 868 } 869 } 870 } 871 this.lastAutoSaveTime = Math.max(this.lastAutoSaveTime, Date.now()); 872 } 873 874 /** 875 * Restarts the timer that regularly saves application homes. 876 * @ignore 877 */ 878 AutoRecoveryManager.prototype.restartTimer = function() { 879 var manager = this; 880 this.stopTimer(); 881 var autoSaveDelayForRecovery = application.getUserPreferences().getAutoSaveDelayForRecovery(); 882 if (autoSaveDelayForRecovery > 0) { 883 var autoSaveTask = function() { 884 if (Date.now() - manager.lastAutoSaveTime > 5000) { 885 manager.saveRecoveredHomes(); 886 } 887 }; 888 this.timerIntervalId = setInterval(autoSaveTask, autoSaveDelayForRecovery); 889 } 890 } 891 892 /** 893 * Restarts the timer that regularly saves application homes. 894 * @ignore 895 */ 896 AutoRecoveryManager.prototype.stopTimer = function() { 897 if (this.timerIntervalId) { 898 window.clearInterval(this.timerIntervalId); 899 delete this.timerIntervalId; 900 } 901 } 902 903 /** 904 * Deletes the given home form auto recovery database. 905 * @private 906 */ 907 AutoRecoveryManager.prototype.deleteRecoveredHome = function(homeName) { 908 var manager = this; 909 this.autoSaveRecorder.deleteHome(homeName, { 910 homeDeleted: function() { 911 if (manager.recoveredHomeNames.length == 0) { 912 // Remove all data if no homes are left in Recovery database 913 // except if an opened home was previously recovered (saving it again will make its data necessary) 914 manager.autoSaveRecorder.getAvailableHomes({ 915 availableHomes: function(homeNames) { 916 if (homeNames.length === 0) { 917 var dummyRecorder = new DirectHomeRecorder({ 918 listHomesURL: manager.autoRecoveryDatabaseUrlBase + "?name=.*", 919 deleteHomeURL: manager.autoRecoveryDatabaseUrlBase + "?name=%s" 920 }); 921 dummyRecorder.getAvailableHomes({ 922 availableHomes: function(dataNames) { 923 for (var i = 0; i < dataNames.length; i++) { 924 dummyRecorder.deleteHome(dataNames [i], { homeDeleted: function() {} }); 925 } 926 } 927 }); 928 } 929 } 930 }); 931 } 932 } 933 }); 934 } 935