OLD | NEW |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * FileManager constructor. | 6 * FileManager constructor. |
7 * | 7 * |
8 * FileManager objects encapsulate the functionality of the file selector | 8 * FileManager objects encapsulate the functionality of the file selector |
9 * dialogs, as well as the full screen file manager application (though the | 9 * dialogs, as well as the full screen file manager application (though the |
10 * latter is not yet implemented). | 10 * latter is not yet implemented). |
(...skipping 12 matching lines...) Expand all Loading... |
23 * end with a trailing slash if it represents a directory. | 23 * end with a trailing slash if it represents a directory. |
24 */ | 24 */ |
25 function FileManager(dialogDom, rootEntries, params) { | 25 function FileManager(dialogDom, rootEntries, params) { |
26 console.log('Init FileManager: ' + dialogDom); | 26 console.log('Init FileManager: ' + dialogDom); |
27 | 27 |
28 this.dialogDom_ = dialogDom; | 28 this.dialogDom_ = dialogDom; |
29 this.rootEntries_ = rootEntries; | 29 this.rootEntries_ = rootEntries; |
30 this.filesystem_ = rootEntries[0].filesystem; | 30 this.filesystem_ = rootEntries[0].filesystem; |
31 this.params_ = params || {}; | 31 this.params_ = params || {}; |
32 | 32 |
| 33 this.listType_ = null; |
| 34 |
33 this.document_ = dialogDom.ownerDocument; | 35 this.document_ = dialogDom.ownerDocument; |
34 this.dialogType_ = | 36 this.dialogType_ = |
35 this.params_.type || FileManager.DialogType.FULL_PAGE; | 37 this.params_.type || FileManager.DialogType.FULL_PAGE; |
36 | 38 |
37 this.defaultPath_ = this.params_.defaultPath || '/'; | 39 this.defaultPath_ = this.params_.defaultPath || '/'; |
38 | 40 |
39 // DirectoryEntry representing the current directory of the dialog. | 41 // DirectoryEntry representing the current directory of the dialog. |
40 this.currentDirEntry_ = null; | 42 this.currentDirEntry_ = null; |
41 | 43 |
42 this.addEventListener('directory-changed', | 44 this.addEventListener('directory-changed', |
43 this.onDirectoryChanged_.bind(this)); | 45 this.onDirectoryChanged_.bind(this)); |
44 this.addEventListener('selection-summarized', | 46 this.addEventListener('selection-summarized', |
45 this.onSelectionSummarized_.bind(this)); | 47 this.onSelectionSummarized_.bind(this)); |
46 | 48 |
47 this.initDom_(); | 49 this.initDom_(); |
48 this.initDialogType_(); | 50 this.initDialogType_(); |
49 | 51 |
50 this.onDetailSelectionChanged_(); | 52 this.onDetailSelectionChanged_(); |
51 | 53 |
52 chrome.fileBrowserPrivate.onDiskChanged.addListener( | 54 chrome.fileBrowserPrivate.onDiskChanged.addListener( |
53 this.onDiskChanged_.bind(this)); | 55 this.onDiskChanged_.bind(this)); |
54 | 56 |
55 this.table.querySelector('.list').focus(); | 57 // TODO(rginda) Add a focus() method to the various list classes to take care |
| 58 // of this. |
| 59 // this.currentList_.list_.focus(); |
56 } | 60 } |
57 | 61 |
58 FileManager.prototype = { | 62 FileManager.prototype = { |
59 __proto__: cr.EventTarget.prototype | 63 __proto__: cr.EventTarget.prototype |
60 }; | 64 }; |
61 | 65 |
62 // Anonymous "namespace". | 66 // Anonymous "namespace". |
63 (function() { | 67 (function() { |
64 | 68 |
65 // Private variables and helper functions. | 69 // Private variables and helper functions. |
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
139 } | 143 } |
140 | 144 |
141 /** | 145 /** |
142 * Get the icon type for a given Entry. | 146 * Get the icon type for a given Entry. |
143 * | 147 * |
144 * @param {Entry} entry An Entry subclass (FileEntry or DirectoryEntry). | 148 * @param {Entry} entry An Entry subclass (FileEntry or DirectoryEntry). |
145 * @return {string} One of the keys from FileManager.iconTypes, or | 149 * @return {string} One of the keys from FileManager.iconTypes, or |
146 * 'unknown'. | 150 * 'unknown'. |
147 */ | 151 */ |
148 function getIconType(entry) { | 152 function getIconType(entry) { |
149 if (entry.isDirectory) | 153 if (entry.cachedIconType_) |
150 return 'folder'; | 154 return entry.cachedIconType_; |
151 | 155 |
152 for (var name in iconTypes) { | 156 var rv = 'unknown'; |
153 var value = iconTypes[name]; | |
154 | 157 |
155 if (value instanceof RegExp) { | 158 if (entry.isDirectory) { |
156 if (value.test(entry.name)) | 159 rv = 'folder'; |
157 return name; | 160 } else { |
158 } else if (typeof value == 'function') { | 161 for (var name in iconTypes) { |
159 try { | 162 var value = iconTypes[name]; |
160 if (value(entry)) | 163 |
161 return name; | 164 if (value instanceof RegExp) { |
162 } catch (ex) { | 165 if (value.test(entry.name)) { |
163 console.error('Caught exception while evaluating iconType: ' + | 166 rv = name; |
164 name, ex); | 167 break; |
| 168 } |
| 169 } else if (typeof value == 'function') { |
| 170 try { |
| 171 if (value(entry)) { |
| 172 rv = name; |
| 173 break; |
| 174 } |
| 175 } catch (ex) { |
| 176 console.error('Caught exception while evaluating iconType: ' + |
| 177 name, ex); |
| 178 } |
| 179 } else { |
| 180 console.log('Unexpected value in iconTypes[' + name + ']: ' + value); |
165 } | 181 } |
166 } else { | |
167 console.log('Unexpected value in iconTypes[' + name + ']: ' + value); | |
168 } | 182 } |
169 } | 183 } |
170 | 184 |
171 return 'unknown'; | 185 entry.cachedIconType_ = rv; |
| 186 return rv; |
172 } | 187 } |
173 | 188 |
174 /** | 189 /** |
175 * Call an asynchronous method on dirEntry, batching multiple callers. | 190 * Call an asynchronous method on dirEntry, batching multiple callers. |
176 * | 191 * |
177 * This batches multiple callers into a single invocation, calling all | 192 * This batches multiple callers into a single invocation, calling all |
178 * interested parties back when the async call completes. | 193 * interested parties back when the async call completes. |
179 * | 194 * |
180 * The Entry method to be invoked should take two callbacks as parameters | 195 * The Entry method to be invoked should take two callbacks as parameters |
181 * (one for success and one for failure), and it should invoke those | 196 * (one for success and one for failure), and it should invoke those |
(...skipping 139 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
321 * | 336 * |
322 * The successCallback is always invoked synchronously, since this does not | 337 * The successCallback is always invoked synchronously, since this does not |
323 * actually require an async call. You should not depend on this, as it may | 338 * actually require an async call. You should not depend on this, as it may |
324 * change if we were to start reading magic numbers (for example). | 339 * change if we were to start reading magic numbers (for example). |
325 * | 340 * |
326 * @param {Entry} entry An HTML5 Entry object. | 341 * @param {Entry} entry An HTML5 Entry object. |
327 * @param {function(Entry)} successCallback The function to invoke once the | 342 * @param {function(Entry)} successCallback The function to invoke once the |
328 * icon type is known. | 343 * icon type is known. |
329 */ | 344 */ |
330 function cacheEntryIconType(entry, successCallback) { | 345 function cacheEntryIconType(entry, successCallback) { |
331 entry.cachedIconType_ = getIconType(entry); | 346 getIconType(entry); |
332 if (successCallback) | 347 if (successCallback) |
333 setTimeout(function() { successCallback(entry) }, 0); | 348 setTimeout(function() { successCallback(entry) }, 0); |
334 } | 349 } |
335 | 350 |
336 // Public statics. | 351 // Public statics. |
337 | 352 |
338 /** | 353 /** |
339 * List of dialog types. | 354 * List of dialog types. |
340 * | 355 * |
341 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except | 356 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except |
342 * FULL_PAGE which is specific to this code. | 357 * FULL_PAGE which is specific to this code. |
343 * | 358 * |
344 * @enum {string} | 359 * @enum {string} |
345 */ | 360 */ |
346 FileManager.DialogType = { | 361 FileManager.DialogType = { |
347 SELECT_FOLDER: 'folder', | 362 SELECT_FOLDER: 'folder', |
348 SELECT_SAVEAS_FILE: 'saveas-file', | 363 SELECT_SAVEAS_FILE: 'saveas-file', |
349 SELECT_OPEN_FILE: 'open-file', | 364 SELECT_OPEN_FILE: 'open-file', |
350 SELECT_OPEN_MULTI_FILE: 'open-multi-file', | 365 SELECT_OPEN_MULTI_FILE: 'open-multi-file', |
351 FULL_PAGE: 'full-page' | 366 FULL_PAGE: 'full-page' |
352 }; | 367 }; |
353 | 368 |
| 369 FileManager.ListType = { |
| 370 DETAIL: 'detail', |
| 371 THUMBNAIL: 'thumb' |
| 372 }; |
| 373 |
354 /** | 374 /** |
355 * Load translated strings. | 375 * Load translated strings. |
356 */ | 376 */ |
357 FileManager.initStrings = function(callback) { | 377 FileManager.initStrings = function(callback) { |
358 chrome.fileBrowserPrivate.getStrings(function(strings) { | 378 chrome.fileBrowserPrivate.getStrings(function(strings) { |
359 localStrings = new LocalStrings(strings); | 379 localStrings = new LocalStrings(strings); |
360 cr.initLocale(strings); | 380 cr.initLocale(strings); |
361 | 381 |
362 if (callback) | 382 if (callback) |
363 callback(); | 383 callback(); |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
397 var ary = this.dialogDom_.querySelectorAll('[visibleif]'); | 417 var ary = this.dialogDom_.querySelectorAll('[visibleif]'); |
398 for (var i = 0; i < ary.length; i++) { | 418 for (var i = 0; i < ary.length; i++) { |
399 var expr = ary[i].getAttribute('visibleif'); | 419 var expr = ary[i].getAttribute('visibleif'); |
400 if (!eval(expr)) | 420 if (!eval(expr)) |
401 ary[i].style.display = 'none'; | 421 ary[i].style.display = 'none'; |
402 } | 422 } |
403 | 423 |
404 // Populate the static localized strings. | 424 // Populate the static localized strings. |
405 i18nTemplate.process(this.document_, localStrings.templateData); | 425 i18nTemplate.process(this.document_, localStrings.templateData); |
406 | 426 |
407 // Set up the detail table. | 427 this.dataModel_ = new cr.ui.table.TableDataModel([]); |
408 var dataModel = new cr.ui.table.TableDataModel([]); | 428 this.dataModel_.sort('name'); |
409 dataModel.sort('name'); | 429 this.dataModel_.addEventListener('sorted', |
410 dataModel.addEventListener('sorted', | 430 this.onDataModelSorted_.bind(this)); |
411 this.onDataModelSorted_.bind(this)); | 431 this.dataModel_.prepareSort = this.prepareSort_.bind(this); |
412 dataModel.prepareSort = this.prepareSort_.bind(this); | |
413 | 432 |
| 433 this.initTable_(); |
| 434 this.initGrid_(); |
| 435 |
| 436 this.setListType(FileManager.ListType.DETAIL); |
| 437 |
| 438 this.onResize_(); |
| 439 this.dialogDom_.style.opacity = '1'; |
| 440 }; |
| 441 |
| 442 |
| 443 FileManager.prototype.setListType = function(type) { |
| 444 console.log('set list: ' + type); |
| 445 |
| 446 if (type && type == this.listType_) { |
| 447 console.log('no change: ' + type); |
| 448 return; |
| 449 } |
| 450 |
| 451 if (type == FileManager.ListType.DETAIL) { |
| 452 this.table_.style.display = ''; |
| 453 this.grid_.style.display = 'none'; |
| 454 this.currentList_ = this.table_; |
| 455 this.dialogDom_.querySelector('button.detail-view').disabled = true; |
| 456 this.dialogDom_.querySelector('button.thumbnail-view').disabled = false; |
| 457 } else if (type == FileManager.ListType.THUMBNAIL) { |
| 458 this.grid_.style.display = ''; |
| 459 this.table_.style.display = 'none'; |
| 460 this.currentList_ = this.grid_; |
| 461 this.dialogDom_.querySelector('button.thumbnail-view').disabled = true; |
| 462 this.dialogDom_.querySelector('button.detail-view').disabled = false; |
| 463 } else { |
| 464 throw new Error('Unknown list type: ' + type); |
| 465 } |
| 466 |
| 467 this.listType_ = type; |
| 468 this.onResize_(); |
| 469 this.currentList_.redraw(); |
| 470 |
| 471 console.log('type now: ' + this.listType_); |
| 472 }; |
| 473 |
| 474 /** |
| 475 * Initialize the file thumbnail grid. |
| 476 */ |
| 477 FileManager.prototype.initGrid_ = function() { |
| 478 this.grid_ = this.dialogDom_.querySelector('.thumbnail-grid'); |
| 479 cr.ui.Grid.decorate(this.grid_); |
| 480 this.grid_.dataModel = this.dataModel_; |
| 481 |
| 482 var self = this; |
| 483 this.grid_.itemConstructor = function(entry) { |
| 484 return self.renderThumbnail_(entry); |
| 485 }; |
| 486 |
| 487 if (this.dialogType_ != FileManager.DialogType.SELECT_OPEN_MULTI_FILE) { |
| 488 this.grid_.selectionModel = new cr.ui.ListSingleSelectionModel(); |
| 489 } |
| 490 |
| 491 this.grid_.addEventListener( |
| 492 'dblclick', this.onDetailDoubleClick_.bind(this)); |
| 493 this.grid_.selectionModel.addEventListener( |
| 494 'change', this.onDetailSelectionChanged_.bind(this)); |
| 495 }; |
| 496 |
| 497 /** |
| 498 * Initialize the file list table. |
| 499 */ |
| 500 FileManager.prototype.initTable_ = function() { |
414 var columns = [ | 501 var columns = [ |
415 new cr.ui.table.TableColumn('cachedIconType_', '', 5.4), | 502 new cr.ui.table.TableColumn('cachedIconType_', '', 5.4), |
416 new cr.ui.table.TableColumn('name', str('NAME_COLUMN_LABEL'), 64), | 503 new cr.ui.table.TableColumn('name', str('NAME_COLUMN_LABEL'), 64), |
417 new cr.ui.table.TableColumn('cachedSize_', | 504 new cr.ui.table.TableColumn('cachedSize_', |
418 str('SIZE_COLUMN_LABEL'), 15.5), | 505 str('SIZE_COLUMN_LABEL'), 15.5), |
419 new cr.ui.table.TableColumn('cachedMtime_', | 506 new cr.ui.table.TableColumn('cachedMtime_', |
420 str('DATE_COLUMN_LABEL'), 21) | 507 str('DATE_COLUMN_LABEL'), 21) |
421 ]; | 508 ]; |
422 | 509 |
423 columns[0].renderFunction = this.renderIconType_.bind(this); | 510 columns[0].renderFunction = this.renderIconType_.bind(this); |
424 columns[1].renderFunction = this.renderName_.bind(this); | 511 columns[1].renderFunction = this.renderName_.bind(this); |
425 columns[2].renderFunction = this.renderSize_.bind(this); | 512 columns[2].renderFunction = this.renderSize_.bind(this); |
426 columns[3].renderFunction = this.renderDate_.bind(this); | 513 columns[3].renderFunction = this.renderDate_.bind(this); |
427 | 514 |
428 this.table = this.dialogDom_.querySelector('.detail-table'); | 515 this.table_ = this.dialogDom_.querySelector('.detail-table'); |
429 cr.ui.Table.decorate(this.table); | 516 cr.ui.Table.decorate(this.table_); |
430 | 517 |
431 this.table.dataModel = dataModel; | 518 this.table_.dataModel = this.dataModel_; |
432 this.table.columnModel = new cr.ui.table.TableColumnModel(columns); | 519 this.table_.columnModel = new cr.ui.table.TableColumnModel(columns); |
433 | 520 |
434 if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE || | 521 if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE || |
435 this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FOLDER || | 522 this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FOLDER || |
436 this.dialogType_ == FileManager.DialogType.SELECT_SAVEAS_FILE) { | 523 this.dialogType_ == FileManager.DialogType.SELECT_SAVEAS_FILE) { |
437 this.table.selectionModel = new cr.ui.table.TableSingleSelectionModel(); | 524 this.table_.selectionModel = new cr.ui.table.TableSingleSelectionModel(); |
438 } | 525 } |
439 | 526 |
440 this.table.addEventListener( | 527 this.table_.addEventListener( |
441 'dblclick', this.onDetailDoubleClick_.bind(this)); | 528 'dblclick', this.onDetailDoubleClick_.bind(this)); |
442 this.table.selectionModel.addEventListener( | 529 this.table_.selectionModel.addEventListener( |
443 'change', this.onDetailSelectionChanged_.bind(this)); | 530 'change', this.onDetailSelectionChanged_.bind(this)); |
444 | |
445 this.onResize_(); | |
446 this.dialogDom_.style.opacity = '1'; | |
447 }; | 531 }; |
448 | 532 |
449 FileManager.prototype.onResize_ = function() { | 533 FileManager.prototype.onResize_ = function() { |
450 // TODO(rginda): Remove this hack when cr.ui.List supports resizing. | 534 console.log('onResize'); |
451 this.table.list_.style.height = | 535 |
452 (this.table.clientHeight - this.table.header_.clientHeight) + 'px'; | 536 this.table_.style.height = this.grid_.style.height = |
453 this.table.redraw(); | 537 this.grid_.parentNode.clientHeight + 'px'; |
| 538 this.table_.style.width = this.grid_.style.width = |
| 539 this.grid_.parentNode.clientWidth + 'px'; |
| 540 |
| 541 this.table_.list_.style.width = this.table_.parentNode.clientWidth + 'px'; |
| 542 this.table_.list_.style.height = (this.table_.clientHeight - 1 - |
| 543 this.table_.header_.clientHeight) + 'px'; |
| 544 |
| 545 if (this.listType_ == FileManager.ListType.THUMBNAIL) { |
| 546 var self = this; |
| 547 setTimeout(function () { self.grid_.columns = 0 }, 100); |
| 548 } else { |
| 549 this.currentList_.redraw(); |
| 550 } |
454 }; | 551 }; |
455 | 552 |
456 /** | 553 /** |
457 * Tweak the UI to become a particular kind of dialog, as determined by the | 554 * Tweak the UI to become a particular kind of dialog, as determined by the |
458 * dialog type parameter passed to the constructor. | 555 * dialog type parameter passed to the constructor. |
459 */ | 556 */ |
460 FileManager.prototype.initDialogType_ = function() { | 557 FileManager.prototype.initDialogType_ = function() { |
461 var defaultTitle; | 558 var defaultTitle; |
462 var okLabel = str('OPEN_LABEL'); | 559 var okLabel = str('OPEN_LABEL'); |
463 | 560 |
(...skipping 95 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
559 } | 656 } |
560 | 657 |
561 function checkCount() { | 658 function checkCount() { |
562 if (uncachedCount == 0) { | 659 if (uncachedCount == 0) { |
563 // Callback via a setTimeout so the sync/async semantics don't change | 660 // Callback via a setTimeout so the sync/async semantics don't change |
564 // based on whether or not the value is cached. | 661 // based on whether or not the value is cached. |
565 setTimeout(callback, 0); | 662 setTimeout(callback, 0); |
566 } | 663 } |
567 } | 664 } |
568 | 665 |
569 var dataModel = this.table.dataModel; | 666 var dataModel = this.dataModel_; |
570 var uncachedCount = dataModel.length; | 667 var uncachedCount = dataModel.length; |
571 | 668 |
572 for (var i = uncachedCount - 1; i >= 0 ; i--) { | 669 for (var i = uncachedCount - 1; i >= 0 ; i--) { |
573 var entry = dataModel.item(i); | 670 var entry = dataModel.item(i); |
574 if (field in entry) { | 671 if (field in entry) { |
575 uncachedCount--; | 672 uncachedCount--; |
576 } else { | 673 } else { |
577 cacheFunction(entry, function() { | 674 cacheFunction(entry, function() { |
578 uncachedCount--; | 675 uncachedCount--; |
579 checkCount(); | 676 checkCount(); |
580 }); | 677 }); |
581 } | 678 } |
582 } | 679 } |
583 | 680 |
584 checkCount(); | 681 checkCount(); |
585 } | 682 } |
586 | 683 |
| 684 FileManager.prototype.renderThumbnail_ = function(entry) { |
| 685 var li = this.document_.createElement('li'); |
| 686 li.className = 'thumbnail-item'; |
| 687 |
| 688 var img = this.document_.createElement('img'); |
| 689 this.setIconSrc(entry, img); |
| 690 li.appendChild(img); |
| 691 |
| 692 var div = this.document_.createElement('div'); |
| 693 div.textContent = entry.name; |
| 694 li.appendChild(div); |
| 695 |
| 696 cr.defineProperty(li, 'lead', cr.PropertyKind.BOOL_ATTR); |
| 697 cr.defineProperty(li, 'selected', cr.PropertyKind.BOOL_ATTR); |
| 698 |
| 699 return li; |
| 700 } |
| 701 |
587 /** | 702 /** |
588 * Render the type column of the detail table. | 703 * Render the type column of the detail table. |
589 * | 704 * |
590 * Invoked by cr.ui.Table when a file needs to be rendered. | 705 * Invoked by cr.ui.Table when a file needs to be rendered. |
591 * | 706 * |
592 * @param {Entry} entry The Entry object to render. | 707 * @param {Entry} entry The Entry object to render. |
593 * @param {string} columnId The id of the column to be rendered. | 708 * @param {string} columnId The id of the column to be rendered. |
594 * @param {cr.ui.Table} table The table doing the rendering. | 709 * @param {cr.ui.Table} table The table doing the rendering. |
595 */ | 710 */ |
596 FileManager.prototype.renderIconType_ = function(entry, columnId, table) { | 711 FileManager.prototype.renderIconType_ = function(entry, columnId, table) { |
(...skipping 83 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
680 /** | 795 /** |
681 * Compute summary information about the current selection. | 796 * Compute summary information about the current selection. |
682 * | 797 * |
683 * This method dispatches the 'selection-summarized' event when it completes. | 798 * This method dispatches the 'selection-summarized' event when it completes. |
684 * Depending on how many of the selected files already have known sizes, the | 799 * Depending on how many of the selected files already have known sizes, the |
685 * dispatch may happen immediately, or after a number of async calls complete. | 800 * dispatch may happen immediately, or after a number of async calls complete. |
686 */ | 801 */ |
687 FileManager.prototype.summarizeSelection_ = function() { | 802 FileManager.prototype.summarizeSelection_ = function() { |
688 var selection = this.selection = { | 803 var selection = this.selection = { |
689 entries: [], | 804 entries: [], |
690 uris: [], | 805 urls: [], |
691 leadEntry: null, | 806 leadEntry: null, |
692 totalCount: 0, | 807 totalCount: 0, |
693 fileCount: 0, | 808 fileCount: 0, |
694 directoryCount: 0, | 809 directoryCount: 0, |
695 bytes: 0, | 810 bytes: 0, |
696 iconType: null, | 811 iconType: null, |
697 }; | 812 }; |
698 | 813 |
699 this.previewSummary_.textContent = str('COMPUTING_SELECTION'); | 814 this.previewSummary_.textContent = str('COMPUTING_SELECTION'); |
700 this.taskButtons_.innerHTML = ''; | 815 this.taskButtons_.innerHTML = ''; |
701 | 816 |
702 var selectedIndexes = this.table.selectionModel.selectedIndexes; | 817 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes; |
703 if (!selectedIndexes.length) { | 818 if (!selectedIndexes.length) { |
704 cr.dispatchSimpleEvent(this, 'selection-summarized'); | 819 cr.dispatchSimpleEvent(this, 'selection-summarized'); |
705 return; | 820 return; |
706 } | 821 } |
707 | 822 |
708 var fileCount = 0; | 823 var fileCount = 0; |
709 var byteCount = 0; | 824 var byteCount = 0; |
710 var pendingFiles = []; | 825 var pendingFiles = []; |
711 | 826 |
712 for (var i = 0; i < selectedIndexes.length; i++) { | 827 for (var i = 0; i < selectedIndexes.length; i++) { |
713 var entry = this.table.dataModel.item(selectedIndexes[i]); | 828 var entry = this.dataModel_.item(selectedIndexes[i]); |
714 | 829 |
715 selection.entries.push(entry); | 830 selection.entries.push(entry); |
716 selection.uris.push(entry.toURI()); | 831 |
| 832 selection.urls.push(entry.toURL()); |
717 | 833 |
718 if (selection.iconType == null) { | 834 if (selection.iconType == null) { |
719 selection.iconType = getIconType(entry); | 835 selection.iconType = getIconType(entry); |
720 } else if (selection.iconType != 'unknown') { | 836 } else if (selection.iconType != 'unknown') { |
721 var iconType = getIconType(entry); | 837 var iconType = getIconType(entry); |
722 if (selection.iconType != iconType) | 838 if (selection.iconType != iconType) |
723 selection.iconType = 'unknown'; | 839 selection.iconType = 'unknown'; |
724 } | 840 } |
725 | 841 |
726 selection.totalCount++; | 842 selection.totalCount++; |
727 | 843 |
728 if (entry.isFile) { | 844 if (entry.isFile) { |
729 if (!('cachedSize_' in entry)) { | 845 if (!('cachedSize_' in entry)) { |
730 // Any file that hasn't been rendered may be missing its cachedSize_ | 846 // Any file that hasn't been rendered may be missing its cachedSize_ |
731 // property. For example, visit a large file list, and press ctrl-a | 847 // property. For example, visit a large file list, and press ctrl-a |
732 // to select all. In this case, we need to asynchronously get the | 848 // to select all. In this case, we need to asynchronously get the |
733 // sizes for these files before telling the world the selection has | 849 // sizes for these files before telling the world the selection has |
734 // been summarized. See the 'computeNextFile' logic below. | 850 // been summarized. See the 'computeNextFile' logic below. |
735 pendingFiles.push(entry); | 851 pendingFiles.push(entry); |
736 continue; | 852 continue; |
737 } else { | 853 } else { |
738 selection.bytes += entry.cachedSize_; | 854 selection.bytes += entry.cachedSize_; |
739 } | 855 } |
740 selection.fileCount += 1; | 856 selection.fileCount += 1; |
741 } else { | 857 } else { |
742 selection.directoryCount += 1; | 858 selection.directoryCount += 1; |
743 } | 859 } |
744 } | 860 } |
745 | 861 |
746 var leadIndex = this.table.selectionModel.leadIndex; | 862 var leadIndex = this.currentList_.selectionModel.leadIndex; |
747 if (leadIndex > -1) { | 863 if (leadIndex > -1) { |
748 selection.leadEntry = this.table.dataModel.item(leadIndex); | 864 selection.leadEntry = this.dataModel_.item(leadIndex); |
749 } else { | 865 } else { |
750 selection.leadEntry = selection.entries[0]; | 866 selection.leadEntry = selection.entries[0]; |
751 } | 867 } |
752 | 868 |
753 var self = this; | 869 var self = this; |
754 | 870 |
755 function cacheNextFile(fileEntry) { | 871 function cacheNextFile(fileEntry) { |
756 if (fileEntry) { | 872 if (fileEntry) { |
757 // We're careful to modify the 'selection', rather than 'self.selection' | 873 // We're careful to modify the 'selection', rather than 'self.selection' |
758 // here, just in case the selection has changed since this summarization | 874 // here, just in case the selection has changed since this summarization |
759 // began. | 875 // began. |
760 selection.bytes += fileEntry.cachedSize_; | 876 selection.bytes += fileEntry.cachedSize_; |
761 } | 877 } |
762 | 878 |
763 if (pendingFiles.length) { | 879 if (pendingFiles.length) { |
764 cacheEntrySize(pendingFiles.pop(), cacheNextFile); | 880 cacheEntrySize(pendingFiles.pop(), cacheNextFile); |
765 } else { | 881 } else { |
766 self.dispatchEvent(new cr.Event('selection-summarized')); | 882 self.dispatchEvent(new cr.Event('selection-summarized')); |
767 } | 883 } |
768 }; | 884 }; |
769 | 885 |
770 if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) { | 886 if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) { |
771 chrome.fileBrowserPrivate.getFileTasks(selection.uris, | 887 chrome.fileBrowserPrivate.getFileTasks(selection.urls, |
772 this.onTasksFound_.bind(this)); | 888 this.onTasksFound_.bind(this)); |
773 } | 889 } |
774 | 890 |
775 cacheNextFile(); | 891 cacheNextFile(); |
776 }; | 892 }; |
777 | 893 |
778 FileManager.prototype.onTasksFound_ = function(tasksList) { | 894 FileManager.prototype.onTasksFound_ = function(tasksList) { |
779 for (var i = 0; i < tasksList.length; i++) { | 895 for (var i = 0; i < tasksList.length; i++) { |
780 var task = tasksList[i]; | 896 var task = tasksList[i]; |
781 | 897 |
782 var button = this.document_.createElement('button'); | 898 var button = this.document_.createElement('button'); |
783 button.addEventListener('click', this.onTaskButtonClicked_.bind(this)); | 899 button.addEventListener('click', this.onTaskButtonClicked_.bind(this)); |
784 button.className = 'task-button'; | 900 button.className = 'task-button'; |
785 button.task = task; | 901 button.task = task; |
786 | 902 |
787 var img = this.document_.createElement('img'); | 903 var img = this.document_.createElement('img'); |
788 img.src = task.iconUrl; | 904 img.src = task.iconUrl; |
789 | 905 |
790 button.appendChild(img); | 906 button.appendChild(img); |
791 button.appendChild(this.document_.createTextNode(task.title)); | 907 button.appendChild(this.document_.createTextNode(task.title)); |
792 | 908 |
793 this.taskButtons_.appendChild(button); | 909 this.taskButtons_.appendChild(button); |
794 } | 910 } |
795 }; | 911 }; |
796 | 912 |
797 FileManager.prototype.onTaskButtonClicked_ = function(event) { | 913 FileManager.prototype.onTaskButtonClicked_ = function(event) { |
798 chrome.fileBrowserPrivate.executeTask(event.srcElement.task.taskId, | 914 chrome.fileBrowserPrivate.executeTask(event.srcElement.task.taskId, |
799 this.selection.uris); | 915 this.selection.urls); |
800 } | 916 } |
801 | 917 |
802 /** | 918 /** |
803 * Update the breadcrumb display to reflect the current directory. | 919 * Update the breadcrumb display to reflect the current directory. |
804 */ | 920 */ |
805 FileManager.prototype.updateBreadcrumbs_ = function() { | 921 FileManager.prototype.updateBreadcrumbs_ = function() { |
806 var bc = this.dialogDom_.querySelector('.breadcrumbs'); | 922 var bc = this.dialogDom_.querySelector('.breadcrumbs'); |
807 bc.innerHTML = ''; | 923 bc.innerHTML = ''; |
808 | 924 |
809 var fullPath = this.currentDirEntry_.fullPath.replace(/\/$/, ''); | 925 var fullPath = this.currentDirEntry_.fullPath.replace(/\/$/, ''); |
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
860 this.previewFilename_.textContent = ''; | 976 this.previewFilename_.textContent = ''; |
861 return; | 977 return; |
862 } | 978 } |
863 | 979 |
864 var previewName = this.selection.leadEntry.name; | 980 var previewName = this.selection.leadEntry.name; |
865 if (this.currentDirEntry_.name == '') | 981 if (this.currentDirEntry_.name == '') |
866 previewName = this.getLabelForRootPath_(previewName); | 982 previewName = this.getLabelForRootPath_(previewName); |
867 | 983 |
868 this.previewFilename_.textContent = previewName; | 984 this.previewFilename_.textContent = previewName; |
869 | 985 |
870 var entry = this.selection.leadEntry; | 986 var iconType = getIconType(this.selection.leadEntry); |
| 987 if (iconType == 'image') { |
| 988 this.previewImage_.classList.add('transparent-background'); |
| 989 if (fileManager.selection.totalCount > 1) |
| 990 this.previewImage_.classList.add('multiple-selected'); |
| 991 } |
| 992 |
| 993 this.setIconSrc(this.selection.leadEntry, this.previewImage_); |
| 994 |
| 995 }; |
| 996 |
| 997 FileManager.prototype.setIconSrc = function(entry, img, callback) { |
871 var iconType = getIconType(entry); | 998 var iconType = getIconType(entry); |
872 if (iconType != 'image') { | 999 if (iconType != 'image') { |
873 // Not an image, display a canned clip-art graphic. | 1000 // Not an image, display a canned clip-art graphic. |
874 this.previewImage_.src = previewArt[iconType]; | 1001 img.src = previewArt[iconType]; |
875 } else { | 1002 } else { |
876 // File is an image, fetch the thumbnail. | 1003 // File is an image, fetch the thumbnail. |
877 | 1004 |
878 var fileManager = this; | 1005 img.src = entry.toURL(); |
879 | |
880 batchAsyncCall(entry, 'file', function(file) { | |
881 var reader = new FileReader(); | |
882 | |
883 reader.onerror = util.ferr('Error reading preview: ' + entry.fullPath); | |
884 reader.onloadend = function(e) { | |
885 fileManager.previewImage_.src = this.result; | |
886 fileManager.previewImage_.classList.add('transparent-background'); | |
887 if (fileManager.selection.totalCount > 1) | |
888 fileManager.previewImage_.classList.add('multiple-selected'); | |
889 }; | |
890 | |
891 reader.readAsDataURL(file); | |
892 }); | |
893 } | 1006 } |
| 1007 if (callback) |
| 1008 callback(); |
894 }; | 1009 }; |
895 | 1010 |
896 /** | 1011 /** |
897 * Change the current directory. | 1012 * Change the current directory. |
898 * | 1013 * |
899 * Dispatches the 'directory-changed' event when the directory is successfully | 1014 * Dispatches the 'directory-changed' event when the directory is successfully |
900 * changed. | 1015 * changed. |
901 * | 1016 * |
902 * @param {string} path The absolute path to the new directory. | 1017 * @param {string} path The absolute path to the new directory. |
903 */ | 1018 */ |
(...skipping 28 matching lines...) Expand all Loading... |
932 } | 1047 } |
933 }); | 1048 }); |
934 }; | 1049 }; |
935 | 1050 |
936 /** | 1051 /** |
937 * Invoked by the table dataModel after a sort completes. | 1052 * Invoked by the table dataModel after a sort completes. |
938 * | 1053 * |
939 * We use this hook to make sure selected files stay visible after a sort. | 1054 * We use this hook to make sure selected files stay visible after a sort. |
940 */ | 1055 */ |
941 FileManager.prototype.onDataModelSorted_ = function() { | 1056 FileManager.prototype.onDataModelSorted_ = function() { |
942 var i = this.table.selectionModel.leadIndex; | 1057 var i = this.currentList_.selectionModel.leadIndex; |
943 this.table.scrollIntoView(i); | 1058 this.currentList_.scrollIntoView(i); |
944 } | 1059 } |
945 | 1060 |
946 /** | 1061 /** |
947 * Update the selection summary UI when the selection summarization completes. | 1062 * Update the selection summary UI when the selection summarization completes. |
948 */ | 1063 */ |
949 FileManager.prototype.onSelectionSummarized_ = function() { | 1064 FileManager.prototype.onSelectionSummarized_ = function() { |
950 if (this.selection.totalCount == 0) { | 1065 if (this.selection.totalCount == 0) { |
951 this.previewSummary_.textContent = str('NOTHING_SELECTED'); | 1066 this.previewSummary_.textContent = str('NOTHING_SELECTED'); |
952 | 1067 |
953 } else if (this.selection.totalCount == 1) { | 1068 } else if (this.selection.totalCount == 1) { |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1005 this.okButton_.disabled = !selectable; | 1120 this.okButton_.disabled = !selectable; |
1006 this.updatePreview_(); | 1121 this.updatePreview_(); |
1007 }; | 1122 }; |
1008 | 1123 |
1009 /** | 1124 /** |
1010 * Handle a double-click event on an entry in the detail list. | 1125 * Handle a double-click event on an entry in the detail list. |
1011 * | 1126 * |
1012 * @param {Event} event The click event. | 1127 * @param {Event} event The click event. |
1013 */ | 1128 */ |
1014 FileManager.prototype.onDetailDoubleClick_ = function(event) { | 1129 FileManager.prototype.onDetailDoubleClick_ = function(event) { |
1015 var i = this.table.selectionModel.leadIndex; | 1130 var i = this.currentList_.selectionModel.leadIndex; |
1016 var entry = this.table.dataModel.item(i); | 1131 var entry = this.dataModel_.item(i); |
1017 | 1132 |
1018 if (entry.isDirectory) | 1133 if (entry.isDirectory) |
1019 return this.changeDirectory(entry.fullPath); | 1134 return this.changeDirectory(entry.fullPath); |
1020 | 1135 |
1021 if (!this.okButton_.disabled) | 1136 if (!this.okButton_.disabled) |
1022 this.onOk_(); | 1137 this.onOk_(); |
1023 | 1138 |
1024 }; | 1139 }; |
1025 | 1140 |
1026 /** | 1141 /** |
(...skipping 19 matching lines...) Expand all Loading... |
1046 * | 1161 * |
1047 * @param {function()} opt_callback Optional function to invoke when the | 1162 * @param {function()} opt_callback Optional function to invoke when the |
1048 * rescan is complete. | 1163 * rescan is complete. |
1049 */ | 1164 */ |
1050 FileManager.prototype.rescanDirectory_ = function(opt_callback) { | 1165 FileManager.prototype.rescanDirectory_ = function(opt_callback) { |
1051 var self = this; | 1166 var self = this; |
1052 var reader; | 1167 var reader; |
1053 | 1168 |
1054 function onReadSome(entries) { | 1169 function onReadSome(entries) { |
1055 if (entries.length == 0) { | 1170 if (entries.length == 0) { |
1056 if (self.table.dataModel.sortStatus.field != 'name') | 1171 if (self.dataModel_.sortStatus.field != 'name') |
1057 self.table.dataModel.updateIndex(0); | 1172 self.dataModel_.updateIndex(0); |
1058 | 1173 |
1059 if (opt_callback) | 1174 if (opt_callback) |
1060 opt_callback(); | 1175 opt_callback(); |
1061 return; | 1176 return; |
1062 } | 1177 } |
1063 | 1178 |
1064 // Splice takes the to-be-spliced-in array as individual parameters, | 1179 // Splice takes the to-be-spliced-in array as individual parameters, |
1065 // rather than as an array, so we need to perform some acrobatics... | 1180 // rather than as an array, so we need to perform some acrobatics... |
1066 var spliceArgs = [].slice.call(entries); | 1181 var spliceArgs = [].slice.call(entries); |
1067 | 1182 |
1068 // Hide files that start with a dot ('.'). | 1183 // Hide files that start with a dot ('.'). |
1069 // TODO(rginda): User should be able to override this. Support for other | 1184 // TODO(rginda): User should be able to override this. Support for other |
1070 // commonly hidden patterns might be nice too. | 1185 // commonly hidden patterns might be nice too. |
1071 spliceArgs = spliceArgs.filter(function(e) { | 1186 spliceArgs = spliceArgs.filter(function(e) { |
1072 return e.name.substr(0, 1) != '.'; | 1187 return e.name.substr(0, 1) != '.'; |
1073 }); | 1188 }); |
1074 | 1189 |
1075 spliceArgs.unshift(0, 0); // index, deleteCount | 1190 spliceArgs.unshift(0, 0); // index, deleteCount |
1076 self.table.dataModel.splice.apply(self.table.dataModel, spliceArgs); | 1191 self.dataModel_.splice.apply(self.dataModel_, spliceArgs); |
1077 | 1192 |
1078 // Keep reading until entries.length is 0. | 1193 // Keep reading until entries.length is 0. |
1079 reader.readEntries(onReadSome); | 1194 reader.readEntries(onReadSome); |
1080 }; | 1195 }; |
1081 | 1196 |
1082 // Clear the table first. | 1197 // Clear the table first. |
1083 this.table.dataModel.splice(0, this.table.dataModel.length); | 1198 this.dataModel_.splice(0, this.dataModel_.length); |
1084 | 1199 |
1085 this.updateBreadcrumbs_(); | 1200 this.updateBreadcrumbs_(); |
1086 | 1201 |
1087 if (this.currentDirEntry_.fullPath != '/') { | 1202 if (this.currentDirEntry_.fullPath != '/') { |
1088 // If not the root directory, just read the contents. | 1203 // If not the root directory, just read the contents. |
1089 reader = this.currentDirEntry_.createReader(); | 1204 reader = this.currentDirEntry_.createReader(); |
1090 reader.readEntries(onReadSome); | 1205 reader.readEntries(onReadSome); |
1091 return; | 1206 return; |
1092 } | 1207 } |
1093 | 1208 |
1094 // Otherwise, use the provided list of root subdirectories, since the | 1209 // Otherwise, use the provided list of root subdirectories, since the |
1095 // real local filesystem root directory (the one we use outside the | 1210 // real local filesystem root directory (the one we use outside the |
1096 // harness) can't be enumerated yet. | 1211 // harness) can't be enumerated yet. |
1097 var spliceArgs = [].slice.call(this.rootEntries_); | 1212 var spliceArgs = [].slice.call(this.rootEntries_); |
1098 spliceArgs.unshift(0, 0); // index, deleteCount | 1213 spliceArgs.unshift(0, 0); // index, deleteCount |
1099 self.table.dataModel.splice.apply(self.table.dataModel, spliceArgs); | 1214 self.dataModel_.splice.apply(self.dataModel_, spliceArgs); |
1100 self.table.dataModel.updateIndex(0); | 1215 self.dataModel_.updateIndex(0); |
1101 | 1216 |
1102 if (opt_callback) | 1217 if (opt_callback) |
1103 opt_callback(); | 1218 opt_callback(); |
1104 }; | 1219 }; |
1105 | 1220 |
1106 FileManager.prototype.onFilenameInputKeyUp_ = function(event) { | 1221 FileManager.prototype.onFilenameInputKeyUp_ = function(event) { |
1107 this.okButton_.disabled = this.filenameInput_.value.length == 0; | 1222 this.okButton_.disabled = this.filenameInput_.value.length == 0; |
1108 | 1223 |
1109 if (event.keyCode == 13 /* Enter */ && !this.okButton_.disabled) | 1224 if (event.keyCode == 13 /* Enter */ && !this.okButton_.disabled) |
1110 this.onOk_(); | 1225 this.onOk_(); |
(...skipping 27 matching lines...) Expand all Loading... |
1138 if (name.indexOf('/') == -1) | 1253 if (name.indexOf('/') == -1) |
1139 break; | 1254 break; |
1140 | 1255 |
1141 alert(strf('ERROR_INVALID_FOLDER_CHARACTER', '/')); | 1256 alert(strf('ERROR_INVALID_FOLDER_CHARACTER', '/')); |
1142 } | 1257 } |
1143 | 1258 |
1144 var self = this; | 1259 var self = this; |
1145 | 1260 |
1146 function onSuccess(dirEntry) { | 1261 function onSuccess(dirEntry) { |
1147 self.rescanDirectory_(function () { | 1262 self.rescanDirectory_(function () { |
1148 for (var i = 0; i < self.table.dataModel.length; i++) { | 1263 for (var i = 0; i < self.dataModel_.length; i++) { |
1149 if (self.table.dataModel.item(i).name == dirEntry.name) { | 1264 if (self.dataModel_.item(i).name == dirEntry.name) { |
1150 self.table.selectionModel.selectedIndex = i; | 1265 self.currentList_.selectionModel.selectedIndex = i; |
1151 self.table.scrollIndexIntoView(i); | 1266 self.currentList_.scrollIndexIntoView(i); |
1152 self.table.focus(); | 1267 self.currentList_.focus(); |
1153 return; | 1268 return; |
1154 } | 1269 } |
1155 }; | 1270 }; |
1156 }); | 1271 }); |
1157 } | 1272 } |
1158 | 1273 |
1159 function onError(err) { | 1274 function onError(err) { |
1160 window.alert(strf('ERROR_CREATING_FOLDER', name, | 1275 window.alert(strf('ERROR_CREATING_FOLDER', name, |
1161 util.getFileErrorMnemonic(err.code))); | 1276 util.getFileErrorMnemonic(err.code))); |
1162 } | 1277 } |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1229 var filename = this.filenameInput_.value; | 1344 var filename = this.filenameInput_.value; |
1230 if (!filename) | 1345 if (!filename) |
1231 throw new Error('Missing filename!'); | 1346 throw new Error('Missing filename!'); |
1232 | 1347 |
1233 chrome.fileBrowserPrivate.selectFile(currentPath + filename, 0); | 1348 chrome.fileBrowserPrivate.selectFile(currentPath + filename, 0); |
1234 window.close(); | 1349 window.close(); |
1235 return; | 1350 return; |
1236 } | 1351 } |
1237 | 1352 |
1238 var ary = []; | 1353 var ary = []; |
1239 var selectedIndexes = this.table.selectionModel.selectedIndexes; | 1354 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes; |
1240 | 1355 |
1241 // All other dialog types require at least one selected list item. | 1356 // All other dialog types require at least one selected list item. |
1242 // The logic to control whether or not the ok button is enabled should | 1357 // The logic to control whether or not the ok button is enabled should |
1243 // prevent us from ever getting here, but we sanity check to be sure. | 1358 // prevent us from ever getting here, but we sanity check to be sure. |
1244 if (!selectedIndexes.length) | 1359 if (!selectedIndexes.length) |
1245 throw new Error('Nothing selected!'); | 1360 throw new Error('Nothing selected!'); |
1246 | 1361 |
1247 for (var i = 0; i < selectedIndexes.length; i++) { | 1362 for (var i = 0; i < selectedIndexes.length; i++) { |
1248 var entry = this.table.dataModel.item(selectedIndexes[i]); | 1363 var entry = this.dataModel_.item(selectedIndexes[i]); |
1249 if (!entry) { | 1364 if (!entry) { |
1250 console.log('Error locating selected file at index: ' + i); | 1365 console.log('Error locating selected file at index: ' + i); |
1251 continue; | 1366 continue; |
1252 } | 1367 } |
1253 | 1368 |
1254 ary.push(currentPath + entry.name); | 1369 ary.push(currentPath + entry.name); |
1255 } | 1370 } |
1256 | 1371 |
1257 // Multi-file selection has no other restrictions. | 1372 // Multi-file selection has no other restrictions. |
1258 if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_MULTI_FILE) { | 1373 if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_MULTI_FILE) { |
(...skipping 12 matching lines...) Expand all Loading... |
1271 } else if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE) { | 1386 } else if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE) { |
1272 if (!this.selection.leadEntry.isFile) | 1387 if (!this.selection.leadEntry.isFile) |
1273 throw new Error('Selected entry is not a file!'); | 1388 throw new Error('Selected entry is not a file!'); |
1274 } | 1389 } |
1275 | 1390 |
1276 chrome.fileBrowserPrivate.selectFile(ary[0], 0); | 1391 chrome.fileBrowserPrivate.selectFile(ary[0], 0); |
1277 window.close(); | 1392 window.close(); |
1278 }; | 1393 }; |
1279 | 1394 |
1280 })(); | 1395 })(); |
OLD | NEW |