OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 'use strict'; | |
6 | |
7 /** | |
8 * FileManager constructor. | |
9 * | |
10 * FileManager objects encapsulate the functionality of the file selector | |
11 * dialogs, as well as the full screen file manager application (though the | |
12 * latter is not yet implemented). | |
13 * | |
14 * @constructor | |
15 */ | |
16 function FileManager() { | |
17 this.initializeQueue_ = new AsyncUtil.Group(); | |
18 | |
19 /** | |
20 * Current list type. | |
21 * @type {ListType} | |
22 * @private | |
23 */ | |
24 this.listType_ = null; | |
25 | |
26 /** | |
27 * True while a user is pressing <Tab>. | |
28 * This is used for identifying the trigger causing the filelist to | |
29 * be focused. | |
30 * @type {boolean} | |
31 * @private | |
32 */ | |
33 this.pressingTab_ = false; | |
34 | |
35 /** | |
36 * SelectionHandler. | |
37 * @type {SelectionHandler} | |
38 * @private | |
39 */ | |
40 this.selectionHandler_ = null; | |
41 | |
42 /** | |
43 * VolumeInfo of the current volume. | |
44 * @type {VolumeInfo} | |
45 * @private | |
46 */ | |
47 this.currentVolumeInfo_ = null; | |
48 } | |
49 | |
50 FileManager.prototype = { | |
51 __proto__: cr.EventTarget.prototype, | |
52 get directoryModel() { | |
53 return this.directoryModel_; | |
54 }, | |
55 get navigationList() { | |
56 return this.navigationList_; | |
57 }, | |
58 get document() { | |
59 return this.document_; | |
60 }, | |
61 get fileTransferController() { | |
62 return this.fileTransferController_; | |
63 }, | |
64 get backgroundPage() { | |
65 return this.backgroundPage_; | |
66 }, | |
67 get volumeManager() { | |
68 return this.volumeManager_; | |
69 }, | |
70 get ui() { | |
71 return this.ui_; | |
72 } | |
73 }; | |
74 | |
75 /** | |
76 * List of dialog types. | |
77 * | |
78 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except | |
79 * FULL_PAGE which is specific to this code. | |
80 * | |
81 * @enum {string} | |
82 */ | |
83 var DialogType = { | |
84 SELECT_FOLDER: 'folder', | |
85 SELECT_UPLOAD_FOLDER: 'upload-folder', | |
86 SELECT_SAVEAS_FILE: 'saveas-file', | |
87 SELECT_OPEN_FILE: 'open-file', | |
88 SELECT_OPEN_MULTI_FILE: 'open-multi-file', | |
89 FULL_PAGE: 'full-page' | |
90 }; | |
91 | |
92 /** | |
93 * @param {string} type Dialog type. | |
94 * @return {boolean} Whether the type is modal. | |
95 */ | |
96 DialogType.isModal = function(type) { | |
97 return type == DialogType.SELECT_FOLDER || | |
98 type == DialogType.SELECT_UPLOAD_FOLDER || | |
99 type == DialogType.SELECT_SAVEAS_FILE || | |
100 type == DialogType.SELECT_OPEN_FILE || | |
101 type == DialogType.SELECT_OPEN_MULTI_FILE; | |
102 }; | |
103 | |
104 /** | |
105 * @param {string} type Dialog type. | |
106 * @return {boolean} Whether the type is open dialog. | |
107 */ | |
108 DialogType.isOpenDialog = function(type) { | |
109 return type == DialogType.SELECT_OPEN_FILE || | |
110 type == DialogType.SELECT_OPEN_MULTI_FILE || | |
111 type == DialogType.SELECT_FOLDER || | |
112 type == DialogType.SELECT_UPLOAD_FOLDER; | |
113 }; | |
114 | |
115 /** | |
116 * @param {string} type Dialog type. | |
117 * @return {boolean} Whether the type is folder selection dialog. | |
118 */ | |
119 DialogType.isFolderDialog = function(type) { | |
120 return type == DialogType.SELECT_FOLDER || | |
121 type == DialogType.SELECT_UPLOAD_FOLDER; | |
122 }; | |
123 | |
124 /** | |
125 * Bottom margin of the list and tree for transparent preview panel. | |
126 * @const | |
127 */ | |
128 var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52; | |
129 | |
130 // Anonymous "namespace". | |
131 (function() { | |
132 | |
133 // Private variables and helper functions. | |
134 | |
135 /** | |
136 * Number of milliseconds in a day. | |
137 */ | |
138 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; | |
139 | |
140 /** | |
141 * Some UI elements react on a single click and standard double click handling | |
142 * leads to confusing results. We ignore a second click if it comes soon | |
143 * after the first. | |
144 */ | |
145 var DOUBLE_CLICK_TIMEOUT = 200; | |
146 | |
147 /** | |
148 * Update the element to display the information about remaining space for | |
149 * the storage. | |
150 * @param {!Element} spaceInnerBar Block element for a percentage bar | |
151 * representing the remaining space. | |
152 * @param {!Element} spaceInfoLabel Inline element to contain the message. | |
153 * @param {!Element} spaceOuterBar Block element around the percentage bar. | |
154 */ | |
155 var updateSpaceInfo = function( | |
156 sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) { | |
157 spaceInnerBar.removeAttribute('pending'); | |
158 if (sizeStatsResult) { | |
159 var sizeStr = util.bytesToString(sizeStatsResult.remainingSize); | |
160 spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr); | |
161 | |
162 var usedSpace = | |
163 sizeStatsResult.totalSize - sizeStatsResult.remainingSize; | |
164 spaceInnerBar.style.width = | |
165 (100 * usedSpace / sizeStatsResult.totalSize) + '%'; | |
166 | |
167 spaceOuterBar.hidden = false; | |
168 } else { | |
169 spaceOuterBar.hidden = true; | |
170 spaceInfoLabel.textContent = str('FAILED_SPACE_INFO'); | |
171 } | |
172 }; | |
173 | |
174 // Public statics. | |
175 | |
176 FileManager.ListType = { | |
177 DETAIL: 'detail', | |
178 THUMBNAIL: 'thumb' | |
179 }; | |
180 | |
181 FileManager.prototype.initPreferences_ = function(callback) { | |
182 var group = new AsyncUtil.Group(); | |
183 | |
184 // DRIVE preferences should be initialized before creating DirectoryModel | |
185 // to rebuild the roots list. | |
186 group.add(this.getPreferences_.bind(this)); | |
187 | |
188 // Get startup preferences. | |
189 this.viewOptions_ = {}; | |
190 group.add(function(done) { | |
191 util.platform.getPreference(this.startupPrefName_, function(value) { | |
192 // Load the global default options. | |
193 try { | |
194 this.viewOptions_ = JSON.parse(value); | |
195 } catch (ignore) {} | |
196 // Override with window-specific options. | |
197 if (window.appState && window.appState.viewOptions) { | |
198 for (var key in window.appState.viewOptions) { | |
199 if (window.appState.viewOptions.hasOwnProperty(key)) | |
200 this.viewOptions_[key] = window.appState.viewOptions[key]; | |
201 } | |
202 } | |
203 done(); | |
204 }.bind(this)); | |
205 }.bind(this)); | |
206 | |
207 group.run(callback); | |
208 }; | |
209 | |
210 /** | |
211 * One time initialization for the file system and related things. | |
212 * | |
213 * @param {function()} callback Completion callback. | |
214 * @private | |
215 */ | |
216 FileManager.prototype.initFileSystemUI_ = function(callback) { | |
217 this.table_.startBatchUpdates(); | |
218 this.grid_.startBatchUpdates(); | |
219 | |
220 this.initFileList_(); | |
221 this.setupCurrentDirectory_(); | |
222 | |
223 // PyAuto tests monitor this state by polling this variable | |
224 this.__defineGetter__('workerInitialized_', function() { | |
225 return this.metadataCache_.isInitialized(); | |
226 }.bind(this)); | |
227 | |
228 this.initDateTimeFormatters_(); | |
229 | |
230 var self = this; | |
231 | |
232 // Get the 'allowRedeemOffers' preference before launching | |
233 // FileListBannerController. | |
234 this.getPreferences_(function(pref) { | |
235 /** @type {boolean} */ | |
236 var showOffers = pref['allowRedeemOffers']; | |
237 self.bannersController_ = new FileListBannerController( | |
238 self.directoryModel_, self.volumeManager_, self.document_, | |
239 showOffers); | |
240 self.bannersController_.addEventListener('relayout', | |
241 self.onResize_.bind(self)); | |
242 }); | |
243 | |
244 var dm = this.directoryModel_; | |
245 dm.addEventListener('directory-changed', | |
246 this.onDirectoryChanged_.bind(this)); | |
247 dm.addEventListener('begin-update-files', function() { | |
248 self.currentList_.startBatchUpdates(); | |
249 }); | |
250 dm.addEventListener('end-update-files', function() { | |
251 self.restoreItemBeingRenamed_(); | |
252 self.currentList_.endBatchUpdates(); | |
253 }); | |
254 dm.addEventListener('scan-started', this.onScanStarted_.bind(this)); | |
255 dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this)); | |
256 dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this)); | |
257 dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this)); | |
258 dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this)); | |
259 dm.addEventListener('rescan-completed', | |
260 this.onRescanCompleted_.bind(this)); | |
261 | |
262 this.directoryTree_.addEventListener('change', function() { | |
263 this.ensureDirectoryTreeItemNotBehindPreviewPanel_(); | |
264 }.bind(this)); | |
265 | |
266 var stateChangeHandler = | |
267 this.onPreferencesChanged_.bind(this); | |
268 chrome.fileBrowserPrivate.onPreferencesChanged.addListener( | |
269 stateChangeHandler); | |
270 stateChangeHandler(); | |
271 | |
272 var driveConnectionChangedHandler = | |
273 this.onDriveConnectionChanged_.bind(this); | |
274 this.volumeManager_.addEventListener('drive-connection-changed', | |
275 driveConnectionChangedHandler); | |
276 driveConnectionChangedHandler(); | |
277 | |
278 // Set the initial focus. | |
279 this.refocus(); | |
280 // Set it as a fallback when there is no focus. | |
281 this.document_.addEventListener('focusout', function(e) { | |
282 setTimeout(function() { | |
283 // When there is no focus, the active element is the <body>. | |
284 if (this.document_.activeElement == this.document_.body) | |
285 this.refocus(); | |
286 }.bind(this), 0); | |
287 }.bind(this)); | |
288 | |
289 this.initDataTransferOperations_(); | |
290 | |
291 this.initContextMenus_(); | |
292 this.initCommands_(); | |
293 | |
294 this.updateFileTypeFilter_(); | |
295 | |
296 this.selectionHandler_.onFileSelectionChanged(); | |
297 | |
298 this.table_.endBatchUpdates(); | |
299 this.grid_.endBatchUpdates(); | |
300 | |
301 callback(); | |
302 }; | |
303 | |
304 /** | |
305 * If |item| in the directory tree is behind the preview panel, scrolls up the | |
306 * parent view and make the item visible. This should be called when: | |
307 * - the selected item is changed in the directory tree. | |
308 * - the visibility of the the preview panel is changed. | |
309 * | |
310 * @private | |
311 */ | |
312 FileManager.prototype.ensureDirectoryTreeItemNotBehindPreviewPanel_ = | |
313 function() { | |
314 var selectedSubTree = this.directoryTree_.selectedItem; | |
315 if (!selectedSubTree) | |
316 return; | |
317 var item = selectedSubTree.rowElement; | |
318 var parentView = this.directoryTree_; | |
319 | |
320 var itemRect = item.getBoundingClientRect(); | |
321 if (!itemRect) | |
322 return; | |
323 | |
324 var listRect = parentView.getBoundingClientRect(); | |
325 if (!listRect) | |
326 return; | |
327 | |
328 var previewPanel = this.dialogDom_.querySelector('.preview-panel'); | |
329 var previewPanelRect = previewPanel.getBoundingClientRect(); | |
330 var panelHeight = previewPanelRect ? previewPanelRect.height : 0; | |
331 | |
332 var itemBottom = itemRect.bottom; | |
333 var listBottom = listRect.bottom - panelHeight; | |
334 | |
335 if (itemBottom > listBottom) { | |
336 var scrollOffset = itemBottom - listBottom; | |
337 parentView.scrollTop += scrollOffset; | |
338 } | |
339 }; | |
340 | |
341 /** | |
342 * @private | |
343 */ | |
344 FileManager.prototype.initDateTimeFormatters_ = function() { | |
345 var use12hourClock = !this.preferences_['use24hourClock']; | |
346 this.table_.setDateTimeFormat(use12hourClock); | |
347 }; | |
348 | |
349 /** | |
350 * @private | |
351 */ | |
352 FileManager.prototype.initDataTransferOperations_ = function() { | |
353 this.fileOperationManager_ = | |
354 this.backgroundPage_.background.fileOperationManager; | |
355 | |
356 // CopyManager are required for 'Delete' operation in | |
357 // Open and Save dialogs. But drag-n-drop and copy-paste are not needed. | |
358 if (this.dialogType != DialogType.FULL_PAGE) return; | |
359 | |
360 // TODO(hidehiko): Extract FileOperationManager related code from | |
361 // FileManager to simplify it. | |
362 this.onCopyProgressBound_ = this.onCopyProgress_.bind(this); | |
363 this.fileOperationManager_.addEventListener( | |
364 'copy-progress', this.onCopyProgressBound_); | |
365 | |
366 this.onEntryChangedBound_ = this.onEntryChanged_.bind(this); | |
367 this.fileOperationManager_.addEventListener( | |
368 'entry-changed', this.onEntryChangedBound_); | |
369 | |
370 var controller = this.fileTransferController_ = | |
371 new FileTransferController(this.document_, | |
372 this.fileOperationManager_, | |
373 this.metadataCache_, | |
374 this.directoryModel_, | |
375 this.volumeManager_, | |
376 this.ui_.multiProfileShareDialog); | |
377 controller.attachDragSource(this.table_.list); | |
378 controller.attachFileListDropTarget(this.table_.list); | |
379 controller.attachDragSource(this.grid_); | |
380 controller.attachFileListDropTarget(this.grid_); | |
381 controller.attachTreeDropTarget(this.directoryTree_); | |
382 controller.attachNavigationListDropTarget(this.navigationList_, true); | |
383 controller.attachCopyPasteHandlers(); | |
384 controller.addEventListener('selection-copied', | |
385 this.blinkSelection.bind(this)); | |
386 controller.addEventListener('selection-cut', | |
387 this.blinkSelection.bind(this)); | |
388 controller.addEventListener('source-not-found', | |
389 this.onSourceNotFound_.bind(this)); | |
390 }; | |
391 | |
392 /** | |
393 * Handles an error that the source entry of file operation is not found. | |
394 * @private | |
395 */ | |
396 FileManager.prototype.onSourceNotFound_ = function(event) { | |
397 // Ensure this.sourceNotFoundErrorCount_ is integer. | |
398 this.sourceNotFoundErrorCount_ = ~~this.sourceNotFoundErrorCount_; | |
399 var item = new ProgressCenterItem(); | |
400 item.id = 'source-not-found-' + this.sourceNotFoundErrorCount_; | |
401 if (event.progressType === ProgressItemType.COPY) | |
402 item.message = strf('COPY_SOURCE_NOT_FOUND_ERROR', event.fileName); | |
403 else if (event.progressType === ProgressItemType.MOVE) | |
404 item.message = strf('MOVE_SOURCE_NOT_FOUND_ERROR', event.fileName); | |
405 item.state = ProgressItemState.ERROR; | |
406 this.backgroundPage_.background.progressCenter.updateItem(item); | |
407 this.sourceNotFoundErrorCount_++; | |
408 }; | |
409 | |
410 /** | |
411 * One-time initialization of context menus. | |
412 * @private | |
413 */ | |
414 FileManager.prototype.initContextMenus_ = function() { | |
415 this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu'); | |
416 cr.ui.Menu.decorate(this.fileContextMenu_); | |
417 | |
418 cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_); | |
419 cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'), | |
420 this.fileContextMenu_); | |
421 cr.ui.contextMenuHandler.setContextMenu( | |
422 this.document_.querySelector('.drive-welcome.page'), | |
423 this.fileContextMenu_); | |
424 | |
425 this.rootsContextMenu_ = | |
426 this.dialogDom_.querySelector('#roots-context-menu'); | |
427 cr.ui.Menu.decorate(this.rootsContextMenu_); | |
428 this.navigationList_.setContextMenu(this.rootsContextMenu_); | |
429 | |
430 this.directoryTreeContextMenu_ = | |
431 this.dialogDom_.querySelector('#directory-tree-context-menu'); | |
432 cr.ui.Menu.decorate(this.directoryTreeContextMenu_); | |
433 this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_; | |
434 | |
435 this.textContextMenu_ = | |
436 this.dialogDom_.querySelector('#text-context-menu'); | |
437 cr.ui.Menu.decorate(this.textContextMenu_); | |
438 | |
439 this.gearButton_ = this.dialogDom_.querySelector('#gear-button'); | |
440 this.gearButton_.addEventListener('menushow', | |
441 this.refreshRemainingSpace_.bind(this, | |
442 false /* Without loading caption. */)); | |
443 chrome.fileBrowserPrivate.onDesktopChanged.addListener(function() { | |
444 this.updateVisitDesktopMenus_(); | |
445 this.ui_.updateProfileBadge(); | |
446 }.bind(this)); | |
447 chrome.fileBrowserPrivate.onProfileAdded.addListener( | |
448 this.updateVisitDesktopMenus_.bind(this)); | |
449 this.updateVisitDesktopMenus_(); | |
450 | |
451 this.dialogDom_.querySelector('#gear-menu').menuItemSelector = | |
452 'menuitem, hr'; | |
453 cr.ui.decorate(this.gearButton_, cr.ui.MenuButton); | |
454 | |
455 if (this.dialogType == DialogType.FULL_PAGE) { | |
456 // This is to prevent the buttons from stealing focus on mouse down. | |
457 var preventFocus = function(event) { | |
458 event.preventDefault(); | |
459 }; | |
460 | |
461 var minimizeButton = this.dialogDom_.querySelector('#minimize-button'); | |
462 minimizeButton.addEventListener('click', this.onMinimize.bind(this)); | |
463 minimizeButton.addEventListener('mousedown', preventFocus); | |
464 | |
465 var maximizeButton = this.dialogDom_.querySelector('#maximize-button'); | |
466 maximizeButton.addEventListener('click', this.onMaximize.bind(this)); | |
467 maximizeButton.addEventListener('mousedown', preventFocus); | |
468 | |
469 var closeButton = this.dialogDom_.querySelector('#close-button'); | |
470 closeButton.addEventListener('click', this.onClose.bind(this)); | |
471 closeButton.addEventListener('mousedown', preventFocus); | |
472 } | |
473 | |
474 this.syncButton.checkable = true; | |
475 this.hostedButton.checkable = true; | |
476 this.detailViewButton_.checkable = true; | |
477 this.thumbnailViewButton_.checkable = true; | |
478 | |
479 if (util.platform.runningInBrowser()) { | |
480 // Suppresses the default context menu. | |
481 this.dialogDom_.addEventListener('contextmenu', function(e) { | |
482 e.preventDefault(); | |
483 e.stopPropagation(); | |
484 }); | |
485 } | |
486 }; | |
487 | |
488 FileManager.prototype.onMinimize = function() { | |
489 chrome.app.window.current().minimize(); | |
490 }; | |
491 | |
492 FileManager.prototype.onMaximize = function() { | |
493 var appWindow = chrome.app.window.current(); | |
494 if (appWindow.isMaximized()) | |
495 appWindow.restore(); | |
496 else | |
497 appWindow.maximize(); | |
498 }; | |
499 | |
500 FileManager.prototype.onClose = function() { | |
501 window.close(); | |
502 }; | |
503 | |
504 /** | |
505 * One-time initialization of commands. | |
506 * @private | |
507 */ | |
508 FileManager.prototype.initCommands_ = function() { | |
509 this.commandHandler = new CommandHandler(this); | |
510 | |
511 // TODO(hirono): Move the following block to the UI part. | |
512 var commandButtons = this.dialogDom_.querySelectorAll('button[command]'); | |
513 for (var j = 0; j < commandButtons.length; j++) | |
514 CommandButton.decorate(commandButtons[j]); | |
515 | |
516 var inputs = this.dialogDom_.querySelectorAll( | |
517 'input[type=text], input[type=search], textarea'); | |
518 for (var i = 0; i < inputs.length; i++) { | |
519 cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_); | |
520 this.registerInputCommands_(inputs[i]); | |
521 } | |
522 | |
523 cr.ui.contextMenuHandler.setContextMenu(this.renameInput_, | |
524 this.textContextMenu_); | |
525 this.registerInputCommands_(this.renameInput_); | |
526 this.document_.addEventListener('command', | |
527 this.setNoHover_.bind(this, true)); | |
528 }; | |
529 | |
530 /** | |
531 * Registers cut, copy, paste and delete commands on input element. | |
532 * | |
533 * @param {Node} node Text input element to register on. | |
534 * @private | |
535 */ | |
536 FileManager.prototype.registerInputCommands_ = function(node) { | |
537 CommandUtil.forceDefaultHandler(node, 'cut'); | |
538 CommandUtil.forceDefaultHandler(node, 'copy'); | |
539 CommandUtil.forceDefaultHandler(node, 'paste'); | |
540 CommandUtil.forceDefaultHandler(node, 'delete'); | |
541 node.addEventListener('keydown', function(e) { | |
542 var key = util.getKeyModifiers(e) + e.keyCode; | |
543 if (key === '190' /* '/' */ || key === '191' /* '.' */) { | |
544 // If this key event is propagated, this is handled search command, | |
545 // which calls 'preventDefault' method. | |
546 e.stopPropagation(); | |
547 } | |
548 }); | |
549 }; | |
550 | |
551 /** | |
552 * Entry point of the initialization. | |
553 * This method is called from main.js. | |
554 */ | |
555 FileManager.prototype.initializeCore = function() { | |
556 this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral'); | |
557 this.initializeQueue_.add(this.initBackgroundPage_.bind(this), | |
558 [], 'initBackgroundPage'); | |
559 this.initializeQueue_.add(this.initPreferences_.bind(this), | |
560 ['initGeneral'], 'initPreferences'); | |
561 this.initializeQueue_.add(this.initVolumeManager_.bind(this), | |
562 ['initGeneral', 'initBackgroundPage'], | |
563 'initVolumeManager'); | |
564 | |
565 this.initializeQueue_.run(); | |
566 window.addEventListener('pagehide', this.onUnload_.bind(this)); | |
567 }; | |
568 | |
569 FileManager.prototype.initializeUI = function(dialogDom, callback) { | |
570 this.dialogDom_ = dialogDom; | |
571 this.document_ = this.dialogDom_.ownerDocument; | |
572 | |
573 this.initializeQueue_.add( | |
574 this.initEssentialUI_.bind(this), | |
575 ['initGeneral', 'initBackgroundPage'], | |
576 'initEssentialUI'); | |
577 this.initializeQueue_.add(this.initAdditionalUI_.bind(this), | |
578 ['initEssentialUI'], 'initAdditionalUI'); | |
579 this.initializeQueue_.add( | |
580 this.initFileSystemUI_.bind(this), | |
581 ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI'); | |
582 | |
583 // Run again just in case if all pending closures have completed and the | |
584 // queue has stopped and monitor the completion. | |
585 this.initializeQueue_.run(callback); | |
586 }; | |
587 | |
588 /** | |
589 * Initializes general purpose basic things, which are used by other | |
590 * initializing methods. | |
591 * | |
592 * @param {function()} callback Completion callback. | |
593 * @private | |
594 */ | |
595 FileManager.prototype.initGeneral_ = function(callback) { | |
596 // Initialize the application state. | |
597 // TODO(mtomasz): Unify window.appState with location.search format. | |
598 if (window.appState) { | |
599 this.params_ = window.appState.params || {}; | |
600 this.initCurrentDirectoryURL_ = window.appState.currentDirectoryURL; | |
601 this.initSelectionURL_ = window.appState.selectionURL; | |
602 this.initTargetName_ = window.appState.targetName; | |
603 } else { | |
604 // Used by the select dialog only. | |
605 this.params_ = location.search ? | |
606 JSON.parse(decodeURIComponent(location.search.substr(1))) : | |
607 {}; | |
608 this.initCurrentDirectoryURL_ = this.params_.currentDirectoryURL; | |
609 this.initSelectionURL_ = this.params_.selectionURL; | |
610 this.initTargetName_ = this.params_.targetName; | |
611 } | |
612 | |
613 // Initialize the member variables that depend this.params_. | |
614 this.dialogType = this.params_.type || DialogType.FULL_PAGE; | |
615 this.startupPrefName_ = 'file-manager-' + this.dialogType; | |
616 this.fileTypes_ = this.params_.typeList || []; | |
617 | |
618 callback(); | |
619 }; | |
620 | |
621 /** | |
622 * Initialize the background page. | |
623 * @param {function()} callback Completion callback. | |
624 * @private | |
625 */ | |
626 FileManager.prototype.initBackgroundPage_ = function(callback) { | |
627 chrome.runtime.getBackgroundPage(function(backgroundPage) { | |
628 this.backgroundPage_ = backgroundPage; | |
629 this.backgroundPage_.background.ready(function() { | |
630 loadTimeData.data = this.backgroundPage_.background.stringData; | |
631 callback(); | |
632 }.bind(this)); | |
633 }.bind(this)); | |
634 }; | |
635 | |
636 /** | |
637 * Initializes the VolumeManager instance. | |
638 * @param {function()} callback Completion callback. | |
639 * @private | |
640 */ | |
641 FileManager.prototype.initVolumeManager_ = function(callback) { | |
642 // Auto resolving to local path does not work for folders (e.g., dialog for | |
643 // loading unpacked extensions). | |
644 var noLocalPathResolution = DialogType.isFolderDialog(this.params_.type); | |
645 | |
646 // If this condition is false, VolumeManagerWrapper hides all drive | |
647 // related event and data, even if Drive is enabled on preference. | |
648 // In other words, even if Drive is disabled on preference but Files.app | |
649 // should show Drive when it is re-enabled, then the value should be set to | |
650 // true. | |
651 // Note that the Drive enabling preference change is listened by | |
652 // DriveIntegrationService, so here we don't need to take care about it. | |
653 var driveEnabled = | |
654 !noLocalPathResolution || !this.params_.shouldReturnLocalPath; | |
655 this.volumeManager_ = new VolumeManagerWrapper( | |
656 driveEnabled, this.backgroundPage_); | |
657 callback(); | |
658 }; | |
659 | |
660 /** | |
661 * One time initialization of the Files.app's essential UI elements. These | |
662 * elements will be shown to the user. Only visible elements should be | |
663 * initialized here. Any heavy operation should be avoided. Files.app's | |
664 * window is shown at the end of this routine. | |
665 * | |
666 * @param {function()} callback Completion callback. | |
667 * @private | |
668 */ | |
669 FileManager.prototype.initEssentialUI_ = function(callback) { | |
670 // Record stats of dialog types. New values must NOT be inserted into the | |
671 // array enumerating the types. It must be in sync with | |
672 // FileDialogType enum in tools/metrics/histograms/histogram.xml. | |
673 metrics.recordEnum('Create', this.dialogType, | |
674 [DialogType.SELECT_FOLDER, | |
675 DialogType.SELECT_UPLOAD_FOLDER, | |
676 DialogType.SELECT_SAVEAS_FILE, | |
677 DialogType.SELECT_OPEN_FILE, | |
678 DialogType.SELECT_OPEN_MULTI_FILE, | |
679 DialogType.FULL_PAGE]); | |
680 | |
681 // Create the metadata cache. | |
682 this.metadataCache_ = MetadataCache.createFull(this.volumeManager_); | |
683 | |
684 // Create the root view of FileManager. | |
685 this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType); | |
686 this.fileTypeSelector_ = this.ui_.fileTypeSelector; | |
687 this.okButton_ = this.ui_.okButton; | |
688 this.cancelButton_ = this.ui_.cancelButton; | |
689 | |
690 // Show the window as soon as the UI pre-initialization is done. | |
691 if (this.dialogType == DialogType.FULL_PAGE && | |
692 !util.platform.runningInBrowser()) { | |
693 chrome.app.window.current().show(); | |
694 setTimeout(callback, 100); // Wait until the animation is finished. | |
695 } else { | |
696 callback(); | |
697 } | |
698 }; | |
699 | |
700 /** | |
701 * One-time initialization of dialogs. | |
702 * @private | |
703 */ | |
704 FileManager.prototype.initDialogs_ = function() { | |
705 // Initialize the dialog. | |
706 this.ui_.initDialogs(); | |
707 FileManagerDialogBase.setFileManager(this); | |
708 | |
709 // Obtains the dialog instances from FileManagerUI. | |
710 // TODO(hirono): Remove the properties from the FileManager class. | |
711 this.error = this.ui_.errorDialog; | |
712 this.alert = this.ui_.alertDialog; | |
713 this.confirm = this.ui_.confirmDialog; | |
714 this.prompt = this.ui_.promptDialog; | |
715 this.shareDialog_ = this.ui_.shareDialog; | |
716 this.defaultTaskPicker = this.ui_.defaultTaskPicker; | |
717 this.suggestAppsDialog = this.ui_.suggestAppsDialog; | |
718 }; | |
719 | |
720 /** | |
721 * One-time initialization of various DOM nodes. Loads the additional DOM | |
722 * elements visible to the user. Initialize here elements, which are expensive | |
723 * or hidden in the beginning. | |
724 * | |
725 * @param {function()} callback Completion callback. | |
726 * @private | |
727 */ | |
728 FileManager.prototype.initAdditionalUI_ = function(callback) { | |
729 this.initDialogs_(); | |
730 this.ui_.initAdditionalUI(); | |
731 | |
732 this.dialogDom_.addEventListener('drop', function(e) { | |
733 // Prevent opening an URL by dropping it onto the page. | |
734 e.preventDefault(); | |
735 }); | |
736 | |
737 this.dialogDom_.addEventListener('click', | |
738 this.onExternalLinkClick_.bind(this)); | |
739 // Cache nodes we'll be manipulating. | |
740 var dom = this.dialogDom_; | |
741 | |
742 this.filenameInput_ = dom.querySelector('#filename-input-box input'); | |
743 this.taskItems_ = dom.querySelector('#tasks'); | |
744 | |
745 this.table_ = dom.querySelector('.detail-table'); | |
746 this.grid_ = dom.querySelector('.thumbnail-grid'); | |
747 this.spinner_ = dom.querySelector('#list-container > .spinner-layer'); | |
748 this.showSpinner_(true); | |
749 | |
750 var fullPage = this.dialogType == DialogType.FULL_PAGE; | |
751 FileTable.decorate(this.table_, this.metadataCache_, fullPage); | |
752 FileGrid.decorate(this.grid_, this.metadataCache_, this.volumeManager_); | |
753 | |
754 this.previewPanel_ = new PreviewPanel( | |
755 dom.querySelector('.preview-panel'), | |
756 DialogType.isOpenDialog(this.dialogType) ? | |
757 PreviewPanel.VisibilityType.ALWAYS_VISIBLE : | |
758 PreviewPanel.VisibilityType.AUTO, | |
759 this.metadataCache_, | |
760 this.volumeManager_); | |
761 this.previewPanel_.addEventListener( | |
762 PreviewPanel.Event.VISIBILITY_CHANGE, | |
763 this.onPreviewPanelVisibilityChange_.bind(this)); | |
764 this.previewPanel_.initialize(); | |
765 | |
766 this.previewPanel_.breadcrumbs.addEventListener( | |
767 'pathclick', this.onBreadcrumbClick_.bind(this)); | |
768 | |
769 // Initialize progress center panel. | |
770 this.progressCenterPanel_ = new ProgressCenterPanel( | |
771 dom.querySelector('#progress-center')); | |
772 this.backgroundPage_.background.progressCenter.addPanel( | |
773 this.progressCenterPanel_); | |
774 | |
775 this.document_.addEventListener('keydown', this.onKeyDown_.bind(this)); | |
776 this.document_.addEventListener('keyup', this.onKeyUp_.bind(this)); | |
777 | |
778 this.renameInput_ = this.document_.createElement('input'); | |
779 this.renameInput_.className = 'rename'; | |
780 | |
781 this.renameInput_.addEventListener( | |
782 'keydown', this.onRenameInputKeyDown_.bind(this)); | |
783 this.renameInput_.addEventListener( | |
784 'blur', this.onRenameInputBlur_.bind(this)); | |
785 | |
786 // TODO(hirono): Rename the handler after creating the DialogFooter class. | |
787 this.filenameInput_.addEventListener( | |
788 'input', this.onFilenameInputInput_.bind(this)); | |
789 this.filenameInput_.addEventListener( | |
790 'keydown', this.onFilenameInputKeyDown_.bind(this)); | |
791 this.filenameInput_.addEventListener( | |
792 'focus', this.onFilenameInputFocus_.bind(this)); | |
793 | |
794 this.listContainer_ = this.dialogDom_.querySelector('#list-container'); | |
795 this.listContainer_.addEventListener( | |
796 'keydown', this.onListKeyDown_.bind(this)); | |
797 this.listContainer_.addEventListener( | |
798 'keypress', this.onListKeyPress_.bind(this)); | |
799 this.listContainer_.addEventListener( | |
800 'mousemove', this.onListMouseMove_.bind(this)); | |
801 | |
802 this.okButton_.addEventListener('click', this.onOk_.bind(this)); | |
803 this.onCancelBound_ = this.onCancel_.bind(this); | |
804 this.cancelButton_.addEventListener('click', this.onCancelBound_); | |
805 | |
806 this.decorateSplitter( | |
807 this.dialogDom_.querySelector('#navigation-list-splitter')); | |
808 this.decorateSplitter( | |
809 this.dialogDom_.querySelector('#middlebar-splitter')); | |
810 | |
811 this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container'); | |
812 | |
813 this.syncButton = this.dialogDom_.querySelector('#drive-sync-settings'); | |
814 this.syncButton.addEventListener('click', this.onDrivePrefClick_.bind( | |
815 this, 'cellularDisabled', false /* not inverted */)); | |
816 | |
817 this.hostedButton = this.dialogDom_.querySelector('#drive-hosted-settings'); | |
818 this.hostedButton.addEventListener('click', this.onDrivePrefClick_.bind( | |
819 this, 'hostedFilesDisabled', true /* inverted */)); | |
820 | |
821 this.detailViewButton_ = | |
822 this.dialogDom_.querySelector('#detail-view'); | |
823 this.detailViewButton_.addEventListener('activate', | |
824 this.onDetailViewButtonClick_.bind(this)); | |
825 | |
826 this.thumbnailViewButton_ = | |
827 this.dialogDom_.querySelector('#thumbnail-view'); | |
828 this.thumbnailViewButton_.addEventListener('activate', | |
829 this.onThumbnailViewButtonClick_.bind(this)); | |
830 | |
831 cr.ui.ComboButton.decorate(this.taskItems_); | |
832 this.taskItems_.showMenu = function(shouldSetFocus) { | |
833 // Prevent the empty menu from opening. | |
834 if (!this.menu.length) | |
835 return; | |
836 cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus); | |
837 }; | |
838 this.taskItems_.addEventListener('select', | |
839 this.onTaskItemClicked_.bind(this)); | |
840 | |
841 this.dialogDom_.ownerDocument.defaultView.addEventListener( | |
842 'resize', this.onResize_.bind(this)); | |
843 | |
844 this.filePopup_ = null; | |
845 | |
846 this.searchBoxWrapper_ = this.ui_.searchBox.element; | |
847 this.searchBox_ = this.ui_.searchBox.inputElement; | |
848 this.searchBox_.addEventListener( | |
849 'input', this.onSearchBoxUpdate_.bind(this)); | |
850 this.ui_.searchBox.clearButton.addEventListener( | |
851 'click', this.onSearchClearButtonClick_.bind(this)); | |
852 | |
853 this.lastSearchQuery_ = ''; | |
854 | |
855 this.autocompleteList_ = this.ui_.searchBox.autocompleteList; | |
856 this.autocompleteList_.requestSuggestions = | |
857 this.requestAutocompleteSuggestions_.bind(this); | |
858 | |
859 // Instead, open the suggested item when Enter key is pressed or | |
860 // mouse-clicked. | |
861 this.autocompleteList_.handleEnterKeydown = function(event) { | |
862 this.openAutocompleteSuggestion_(); | |
863 this.lastAutocompleteQuery_ = ''; | |
864 this.autocompleteList_.suggestions = []; | |
865 }.bind(this); | |
866 this.autocompleteList_.addEventListener('mousedown', function(event) { | |
867 this.openAutocompleteSuggestion_(); | |
868 this.lastAutocompleteQuery_ = ''; | |
869 this.autocompleteList_.suggestions = []; | |
870 }.bind(this)); | |
871 | |
872 this.defaultActionMenuItem_ = | |
873 this.dialogDom_.querySelector('#default-action'); | |
874 | |
875 this.openWithCommand_ = | |
876 this.dialogDom_.querySelector('#open-with'); | |
877 | |
878 this.driveBuyMoreStorageCommand_ = | |
879 this.dialogDom_.querySelector('#drive-buy-more-space'); | |
880 | |
881 this.defaultActionMenuItem_.addEventListener('activate', | |
882 this.dispatchSelectionAction_.bind(this)); | |
883 | |
884 this.initFileTypeFilter_(); | |
885 | |
886 util.addIsFocusedMethod(); | |
887 | |
888 // Populate the static localized strings. | |
889 i18nTemplate.process(this.document_, loadTimeData); | |
890 | |
891 // Arrange the file list. | |
892 this.table_.normalizeColumns(); | |
893 this.table_.redraw(); | |
894 | |
895 callback(); | |
896 }; | |
897 | |
898 /** | |
899 * @param {Event} event Click event. | |
900 * @private | |
901 */ | |
902 FileManager.prototype.onBreadcrumbClick_ = function(event) { | |
903 this.directoryModel_.changeDirectoryEntry(event.entry); | |
904 }; | |
905 | |
906 /** | |
907 * Constructs table and grid (heavy operation). | |
908 * @private | |
909 **/ | |
910 FileManager.prototype.initFileList_ = function() { | |
911 // Always sharing the data model between the detail/thumb views confuses | |
912 // them. Instead we maintain this bogus data model, and hook it up to the | |
913 // view that is not in use. | |
914 this.emptyDataModel_ = new cr.ui.ArrayDataModel([]); | |
915 this.emptySelectionModel_ = new cr.ui.ListSelectionModel(); | |
916 | |
917 var singleSelection = | |
918 this.dialogType == DialogType.SELECT_OPEN_FILE || | |
919 this.dialogType == DialogType.SELECT_FOLDER || | |
920 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER || | |
921 this.dialogType == DialogType.SELECT_SAVEAS_FILE; | |
922 | |
923 this.fileFilter_ = new FileFilter( | |
924 this.metadataCache_, | |
925 false /* Don't show dot files by default. */); | |
926 | |
927 this.fileWatcher_ = new FileWatcher(this.metadataCache_); | |
928 this.fileWatcher_.addEventListener( | |
929 'watcher-metadata-changed', | |
930 this.onWatcherMetadataChanged_.bind(this)); | |
931 | |
932 this.directoryModel_ = new DirectoryModel( | |
933 singleSelection, | |
934 this.fileFilter_, | |
935 this.fileWatcher_, | |
936 this.metadataCache_, | |
937 this.volumeManager_); | |
938 | |
939 this.folderShortcutsModel_ = new FolderShortcutsDataModel( | |
940 this.volumeManager_); | |
941 | |
942 this.selectionHandler_ = new FileSelectionHandler(this); | |
943 | |
944 var dataModel = this.directoryModel_.getFileList(); | |
945 | |
946 this.table_.setupCompareFunctions(dataModel); | |
947 | |
948 dataModel.addEventListener('permuted', | |
949 this.updateStartupPrefs_.bind(this)); | |
950 | |
951 this.directoryModel_.getFileListSelection().addEventListener('change', | |
952 this.selectionHandler_.onFileSelectionChanged.bind( | |
953 this.selectionHandler_)); | |
954 | |
955 this.initList_(this.grid_); | |
956 this.initList_(this.table_.list); | |
957 | |
958 var fileListFocusBound = this.onFileListFocus_.bind(this); | |
959 this.table_.list.addEventListener('focus', fileListFocusBound); | |
960 this.grid_.addEventListener('focus', fileListFocusBound); | |
961 | |
962 var dragStartBound = this.onDragStart_.bind(this); | |
963 this.table_.list.addEventListener('dragstart', dragStartBound); | |
964 this.grid_.addEventListener('dragstart', dragStartBound); | |
965 | |
966 var dragEndBound = this.onDragEnd_.bind(this); | |
967 this.table_.list.addEventListener('dragend', dragEndBound); | |
968 this.grid_.addEventListener('dragend', dragEndBound); | |
969 // This event is published by DragSelector because drag end event is not | |
970 // published at the end of drag selection. | |
971 this.table_.list.addEventListener('dragselectionend', dragEndBound); | |
972 this.grid_.addEventListener('dragselectionend', dragEndBound); | |
973 | |
974 // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just | |
975 // attach the directory model. | |
976 this.initNavigationList_(); | |
977 | |
978 this.table_.addEventListener('column-resize-end', | |
979 this.updateStartupPrefs_.bind(this)); | |
980 | |
981 // Restore preferences. | |
982 this.directoryModel_.getFileList().sort( | |
983 this.viewOptions_.sortField || 'modificationTime', | |
984 this.viewOptions_.sortDirection || 'desc'); | |
985 if (this.viewOptions_.columns) { | |
986 var cm = this.table_.columnModel; | |
987 for (var i = 0; i < cm.totalSize; i++) { | |
988 if (this.viewOptions_.columns[i] > 0) | |
989 cm.setWidth(i, this.viewOptions_.columns[i]); | |
990 } | |
991 } | |
992 this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL); | |
993 | |
994 this.textSearchState_ = {text: '', date: new Date()}; | |
995 this.closeOnUnmount_ = (this.params_.action == 'auto-open'); | |
996 | |
997 if (this.closeOnUnmount_) { | |
998 this.volumeManager_.addEventListener('externally-unmounted', | |
999 this.onExternallyUnmounted_.bind(this)); | |
1000 } | |
1001 | |
1002 // Update metadata to change 'Today' and 'Yesterday' dates. | |
1003 var today = new Date(); | |
1004 today.setHours(0); | |
1005 today.setMinutes(0); | |
1006 today.setSeconds(0); | |
1007 today.setMilliseconds(0); | |
1008 setTimeout(this.dailyUpdateModificationTime_.bind(this), | |
1009 today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000); | |
1010 }; | |
1011 | |
1012 /** | |
1013 * @private | |
1014 */ | |
1015 FileManager.prototype.initNavigationList_ = function() { | |
1016 this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree'); | |
1017 DirectoryTree.decorate(this.directoryTree_, | |
1018 this.directoryModel_, | |
1019 this.volumeManager_); | |
1020 | |
1021 this.navigationList_ = this.dialogDom_.querySelector('#navigation-list'); | |
1022 NavigationList.decorate(this.navigationList_, | |
1023 this.volumeManager_, | |
1024 this.directoryModel_); | |
1025 this.navigationList_.fileManager = this; | |
1026 this.navigationList_.dataModel = new NavigationListModel( | |
1027 this.volumeManager_, this.folderShortcutsModel_); | |
1028 }; | |
1029 | |
1030 /** | |
1031 * @private | |
1032 */ | |
1033 FileManager.prototype.updateMiddleBarVisibility_ = function() { | |
1034 var entry = this.directoryModel_.getCurrentDirEntry(); | |
1035 if (!entry) | |
1036 return; | |
1037 | |
1038 var driveVolume = this.volumeManager_.getVolumeInfo(entry); | |
1039 var visible = driveVolume && !driveVolume.error && | |
1040 driveVolume.volumeType === util.VolumeType.DRIVE; | |
1041 this.dialogDom_. | |
1042 querySelector('.dialog-middlebar-contents').hidden = !visible; | |
1043 this.dialogDom_.querySelector('#middlebar-splitter').hidden = !visible; | |
1044 this.onResize_(); | |
1045 }; | |
1046 | |
1047 /** | |
1048 * @private | |
1049 */ | |
1050 FileManager.prototype.updateStartupPrefs_ = function() { | |
1051 var sortStatus = this.directoryModel_.getFileList().sortStatus; | |
1052 var prefs = { | |
1053 sortField: sortStatus.field, | |
1054 sortDirection: sortStatus.direction, | |
1055 columns: [], | |
1056 listType: this.listType_ | |
1057 }; | |
1058 var cm = this.table_.columnModel; | |
1059 for (var i = 0; i < cm.totalSize; i++) { | |
1060 prefs.columns.push(cm.getWidth(i)); | |
1061 } | |
1062 // Save the global default. | |
1063 util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs)); | |
1064 | |
1065 // Save the window-specific preference. | |
1066 if (window.appState) { | |
1067 window.appState.viewOptions = prefs; | |
1068 util.saveAppState(); | |
1069 } | |
1070 }; | |
1071 | |
1072 FileManager.prototype.refocus = function() { | |
1073 var targetElement; | |
1074 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) | |
1075 targetElement = this.filenameInput_; | |
1076 else | |
1077 targetElement = this.currentList_; | |
1078 | |
1079 // Hack: if the tabIndex is disabled, we can assume a modal dialog is | |
1080 // shown. Focus to a button on the dialog instead. | |
1081 if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1) | |
1082 targetElement = document.querySelector('button:not([tabIndex="-1"])'); | |
1083 | |
1084 if (targetElement) | |
1085 targetElement.focus(); | |
1086 }; | |
1087 | |
1088 /** | |
1089 * File list focus handler. Used to select the top most element on the list | |
1090 * if nothing was selected. | |
1091 * | |
1092 * @private | |
1093 */ | |
1094 FileManager.prototype.onFileListFocus_ = function() { | |
1095 // If the file list is focused by <Tab>, select the first item if no item | |
1096 // is selected. | |
1097 if (this.pressingTab_) { | |
1098 if (this.getSelection() && this.getSelection().totalCount == 0) | |
1099 this.directoryModel_.selectIndex(0); | |
1100 } | |
1101 }; | |
1102 | |
1103 /** | |
1104 * Index of selected item in the typeList of the dialog params. | |
1105 * | |
1106 * @return {number} 1-based index of selected type or 0 if no type selected. | |
1107 * @private | |
1108 */ | |
1109 FileManager.prototype.getSelectedFilterIndex_ = function() { | |
1110 var index = Number(this.fileTypeSelector_.selectedIndex); | |
1111 if (index < 0) // Nothing selected. | |
1112 return 0; | |
1113 if (this.params_.includeAllFiles) // Already 1-based. | |
1114 return index; | |
1115 return index + 1; // Convert to 1-based; | |
1116 }; | |
1117 | |
1118 FileManager.prototype.setListType = function(type) { | |
1119 if (type && type == this.listType_) | |
1120 return; | |
1121 | |
1122 this.table_.list.startBatchUpdates(); | |
1123 this.grid_.startBatchUpdates(); | |
1124 | |
1125 // TODO(dzvorygin): style.display and dataModel setting order shouldn't | |
1126 // cause any UI bugs. Currently, the only right way is first to set display | |
1127 // style and only then set dataModel. | |
1128 | |
1129 if (type == FileManager.ListType.DETAIL) { | |
1130 this.table_.dataModel = this.directoryModel_.getFileList(); | |
1131 this.table_.selectionModel = this.directoryModel_.getFileListSelection(); | |
1132 this.table_.hidden = false; | |
1133 this.grid_.hidden = true; | |
1134 this.grid_.selectionModel = this.emptySelectionModel_; | |
1135 this.grid_.dataModel = this.emptyDataModel_; | |
1136 this.table_.hidden = false; | |
1137 /** @type {cr.ui.List} */ | |
1138 this.currentList_ = this.table_.list; | |
1139 this.detailViewButton_.setAttribute('checked', ''); | |
1140 this.thumbnailViewButton_.removeAttribute('checked'); | |
1141 this.detailViewButton_.setAttribute('disabled', ''); | |
1142 this.thumbnailViewButton_.removeAttribute('disabled'); | |
1143 } else if (type == FileManager.ListType.THUMBNAIL) { | |
1144 this.grid_.dataModel = this.directoryModel_.getFileList(); | |
1145 this.grid_.selectionModel = this.directoryModel_.getFileListSelection(); | |
1146 this.grid_.hidden = false; | |
1147 this.table_.hidden = true; | |
1148 this.table_.selectionModel = this.emptySelectionModel_; | |
1149 this.table_.dataModel = this.emptyDataModel_; | |
1150 this.grid_.hidden = false; | |
1151 /** @type {cr.ui.List} */ | |
1152 this.currentList_ = this.grid_; | |
1153 this.thumbnailViewButton_.setAttribute('checked', ''); | |
1154 this.detailViewButton_.removeAttribute('checked'); | |
1155 this.thumbnailViewButton_.setAttribute('disabled', ''); | |
1156 this.detailViewButton_.removeAttribute('disabled'); | |
1157 } else { | |
1158 throw new Error('Unknown list type: ' + type); | |
1159 } | |
1160 | |
1161 this.listType_ = type; | |
1162 this.updateStartupPrefs_(); | |
1163 this.onResize_(); | |
1164 | |
1165 this.table_.list.endBatchUpdates(); | |
1166 this.grid_.endBatchUpdates(); | |
1167 }; | |
1168 | |
1169 /** | |
1170 * Initialize the file list table or grid. | |
1171 * | |
1172 * @param {cr.ui.List} list The list. | |
1173 * @private | |
1174 */ | |
1175 FileManager.prototype.initList_ = function(list) { | |
1176 // Overriding the default role 'list' to 'listbox' for better accessibility | |
1177 // on ChromeOS. | |
1178 list.setAttribute('role', 'listbox'); | |
1179 list.addEventListener('click', this.onDetailClick_.bind(this)); | |
1180 list.id = 'file-list'; | |
1181 }; | |
1182 | |
1183 /** | |
1184 * @private | |
1185 */ | |
1186 FileManager.prototype.onCopyProgress_ = function(event) { | |
1187 if (event.reason == 'ERROR' && | |
1188 event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR && | |
1189 event.error.data.toDrive && | |
1190 event.error.data.name == util.FileError.QUOTA_EXCEEDED_ERR) { | |
1191 this.alert.showHtml( | |
1192 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'), | |
1193 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE', | |
1194 decodeURIComponent( | |
1195 event.error.data.sourceFileUrl.split('/').pop()), | |
1196 str('GOOGLE_DRIVE_BUY_STORAGE_URL'))); | |
1197 } | |
1198 }; | |
1199 | |
1200 /** | |
1201 * Handler of file manager operations. Called when an entry has been | |
1202 * changed. | |
1203 * This updates directory model to reflect operation result immediately (not | |
1204 * waiting for directory update event). Also, preloads thumbnails for the | |
1205 * images of new entries. | |
1206 * See also FileOperationManager.EventRouter. | |
1207 * | |
1208 * @param {Event} event An event for the entry change. | |
1209 * @private | |
1210 */ | |
1211 FileManager.prototype.onEntryChanged_ = function(event) { | |
1212 var kind = event.kind; | |
1213 var entry = event.entry; | |
1214 this.directoryModel_.onEntryChanged(kind, entry); | |
1215 this.selectionHandler_.onFileSelectionChanged(); | |
1216 | |
1217 if (kind === util.EntryChangedKind.CREATED && FileType.isImage(entry)) { | |
1218 // Preload a thumbnail if the new copied entry an image. | |
1219 var locationInfo = this.volumeManager_.getLocationInfo(entry); | |
1220 if (!locationInfo) | |
1221 return; | |
1222 this.metadataCache_.get(entry, 'thumbnail|drive', function(metadata) { | |
1223 var thumbnailLoader_ = new ThumbnailLoader( | |
1224 entry, | |
1225 ThumbnailLoader.LoaderType.CANVAS, | |
1226 metadata, | |
1227 undefined, // Media type. | |
1228 // TODO(mtomasz): Use Entry instead of paths. | |
1229 locationInfo.isDriveBased ? | |
1230 ThumbnailLoader.UseEmbedded.USE_EMBEDDED : | |
1231 ThumbnailLoader.UseEmbedded.NO_EMBEDDED, | |
1232 10); // Very low priority. | |
1233 thumbnailLoader_.loadDetachedImage(function(success) {}); | |
1234 }); | |
1235 } | |
1236 }; | |
1237 | |
1238 /** | |
1239 * Fills the file type list or hides it. | |
1240 * @private | |
1241 */ | |
1242 FileManager.prototype.initFileTypeFilter_ = function() { | |
1243 if (this.params_.includeAllFiles) { | |
1244 var option = this.document_.createElement('option'); | |
1245 option.innerText = str('ALL_FILES_FILTER'); | |
1246 this.fileTypeSelector_.appendChild(option); | |
1247 option.value = 0; | |
1248 } | |
1249 | |
1250 for (var i = 0; i !== this.fileTypes_.length; i++) { | |
1251 var fileType = this.fileTypes_[i]; | |
1252 var option = this.document_.createElement('option'); | |
1253 var description = fileType.description; | |
1254 if (!description) { | |
1255 // See if all the extensions in the group have the same description. | |
1256 for (var j = 0; j !== fileType.extensions.length; j++) { | |
1257 var currentDescription = FileType.typeToString( | |
1258 FileType.getTypeForName('.' + fileType.extensions[j])); | |
1259 if (!description) // Set the first time. | |
1260 description = currentDescription; | |
1261 else if (description != currentDescription) { | |
1262 // No single description, fall through to the extension list. | |
1263 description = null; | |
1264 break; | |
1265 } | |
1266 } | |
1267 | |
1268 if (!description) | |
1269 // Convert ['jpg', 'png'] to '*.jpg, *.png'. | |
1270 description = fileType.extensions.map(function(s) { | |
1271 return '*.' + s; | |
1272 }).join(', '); | |
1273 } | |
1274 option.innerText = description; | |
1275 | |
1276 option.value = i + 1; | |
1277 | |
1278 if (fileType.selected) | |
1279 option.selected = true; | |
1280 | |
1281 this.fileTypeSelector_.appendChild(option); | |
1282 } | |
1283 | |
1284 var options = this.fileTypeSelector_.querySelectorAll('option'); | |
1285 if (options.length >= 2) { | |
1286 // There is in fact no choice, show the selector. | |
1287 this.fileTypeSelector_.hidden = false; | |
1288 | |
1289 this.fileTypeSelector_.addEventListener('change', | |
1290 this.updateFileTypeFilter_.bind(this)); | |
1291 } | |
1292 }; | |
1293 | |
1294 /** | |
1295 * Filters file according to the selected file type. | |
1296 * @private | |
1297 */ | |
1298 FileManager.prototype.updateFileTypeFilter_ = function() { | |
1299 this.fileFilter_.removeFilter('fileType'); | |
1300 var selectedIndex = this.getSelectedFilterIndex_(); | |
1301 if (selectedIndex > 0) { // Specific filter selected. | |
1302 var regexp = new RegExp('.*(' + | |
1303 this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i'); | |
1304 var filter = function(entry) { | |
1305 return entry.isDirectory || regexp.test(entry.name); | |
1306 }; | |
1307 this.fileFilter_.addFilter('fileType', filter); | |
1308 } | |
1309 }; | |
1310 | |
1311 /** | |
1312 * Resize details and thumb views to fit the new window size. | |
1313 * @private | |
1314 */ | |
1315 FileManager.prototype.onResize_ = function() { | |
1316 if (this.listType_ == FileManager.ListType.THUMBNAIL) | |
1317 this.grid_.relayout(); | |
1318 else | |
1319 this.table_.relayout(); | |
1320 | |
1321 // May not be available during initialization. | |
1322 if (this.directoryTree_) | |
1323 this.directoryTree_.relayout(); | |
1324 | |
1325 // TODO(mtomasz, yoshiki): Initialize navigation list earlier, before | |
1326 // file system is available. | |
1327 if (this.navigationList_) | |
1328 this.navigationList_.redraw(); | |
1329 | |
1330 this.previewPanel_.breadcrumbs.truncate(); | |
1331 }; | |
1332 | |
1333 /** | |
1334 * Handles local metadata changes in the currect directory. | |
1335 * @param {Event} event Change event. | |
1336 * @private | |
1337 */ | |
1338 FileManager.prototype.onWatcherMetadataChanged_ = function(event) { | |
1339 this.updateMetadataInUI_( | |
1340 event.metadataType, event.entries, event.properties); | |
1341 }; | |
1342 | |
1343 /** | |
1344 * Resize details and thumb views to fit the new window size. | |
1345 * @private | |
1346 */ | |
1347 FileManager.prototype.onPreviewPanelVisibilityChange_ = function() { | |
1348 // This method may be called on initialization. Some object may not be | |
1349 // initialized. | |
1350 | |
1351 var panelHeight = this.previewPanel_.visible ? | |
1352 this.previewPanel_.height : 0; | |
1353 if (this.grid_) | |
1354 this.grid_.setBottomMarginForPanel(panelHeight); | |
1355 if (this.table_) | |
1356 this.table_.setBottomMarginForPanel(panelHeight); | |
1357 | |
1358 if (this.directoryTree_) { | |
1359 this.directoryTree_.setBottomMarginForPanel(panelHeight); | |
1360 this.ensureDirectoryTreeItemNotBehindPreviewPanel_(); | |
1361 } | |
1362 }; | |
1363 | |
1364 /** | |
1365 * Invoked when the drag is started on the list or the grid. | |
1366 * @private | |
1367 */ | |
1368 FileManager.prototype.onDragStart_ = function() { | |
1369 // On open file dialog, the preview panel is always shown. | |
1370 if (DialogType.isOpenDialog(this.dialogType)) | |
1371 return; | |
1372 this.previewPanel_.visibilityType = | |
1373 PreviewPanel.VisibilityType.ALWAYS_HIDDEN; | |
1374 }; | |
1375 | |
1376 /** | |
1377 * Invoked when the drag is ended on the list or the grid. | |
1378 * @private | |
1379 */ | |
1380 FileManager.prototype.onDragEnd_ = function() { | |
1381 // On open file dialog, the preview panel is always shown. | |
1382 if (DialogType.isOpenDialog(this.dialogType)) | |
1383 return; | |
1384 this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO; | |
1385 }; | |
1386 | |
1387 /** | |
1388 * Sets up the current directory during initialization. | |
1389 * @private | |
1390 */ | |
1391 FileManager.prototype.setupCurrentDirectory_ = function() { | |
1392 var tracker = this.directoryModel_.createDirectoryChangeTracker(); | |
1393 var queue = new AsyncUtil.Queue(); | |
1394 | |
1395 // Wait until the volume manager is initialized. | |
1396 queue.run(function(callback) { | |
1397 tracker.start(); | |
1398 this.volumeManager_.ensureInitialized(callback); | |
1399 }.bind(this)); | |
1400 | |
1401 var nextCurrentDirEntry; | |
1402 var selectionEntry; | |
1403 | |
1404 // Resolve the selectionURL to selectionEntry or to currentDirectoryEntry | |
1405 // in case of being a display root. | |
1406 queue.run(function(callback) { | |
1407 if (!this.initSelectionURL_) { | |
1408 callback(); | |
1409 return; | |
1410 } | |
1411 webkitResolveLocalFileSystemURL( | |
1412 this.initSelectionURL_, | |
1413 function(inEntry) { | |
1414 var locationInfo = this.volumeManager_.getLocationInfo(inEntry); | |
1415 // If location information is not available, then the volume is | |
1416 // no longer (or never) available. | |
1417 if (!locationInfo) { | |
1418 callback(); | |
1419 return; | |
1420 } | |
1421 // If the selection is root, then use it as a current directory | |
1422 // instead. This is because, selecting a root entry is done as | |
1423 // opening it. | |
1424 if (locationInfo.isRootEntry) | |
1425 nextCurrentDirEntry = inEntry; | |
1426 else | |
1427 selectionEntry = inEntry; | |
1428 callback(); | |
1429 }.bind(this), callback); | |
1430 }.bind(this)); | |
1431 // Resolve the currentDirectoryURL to currentDirectoryEntry (if not done | |
1432 // by the previous step). | |
1433 queue.run(function(callback) { | |
1434 if (nextCurrentDirEntry || !this.initCurrentDirectoryURL_) { | |
1435 callback(); | |
1436 return; | |
1437 } | |
1438 webkitResolveLocalFileSystemURL( | |
1439 this.initCurrentDirectoryURL_, | |
1440 function(inEntry) { | |
1441 var locationInfo = this.volumeManager_.getLocationInfo(inEntry); | |
1442 if (!locationInfo) { | |
1443 callback(); | |
1444 return; | |
1445 } | |
1446 nextCurrentDirEntry = inEntry; | |
1447 callback(); | |
1448 }.bind(this), callback); | |
1449 // TODO(mtomasz): Implement reopening on special search, when fake | |
1450 // entries are converted to directory providers. | |
1451 }.bind(this)); | |
1452 | |
1453 // If the directory to be changed to is not available, then first fallback | |
1454 // to the parent of the selection entry. | |
1455 queue.run(function(callback) { | |
1456 if (nextCurrentDirEntry || !selectionEntry) { | |
1457 callback(); | |
1458 return; | |
1459 } | |
1460 selectionEntry.getParent(function(inEntry) { | |
1461 nextCurrentDirEntry = inEntry; | |
1462 callback(); | |
1463 }.bind(this)); | |
1464 }.bind(this)); | |
1465 | |
1466 // If the directory to be changed to is still not resolved, then fallback | |
1467 // to the default display root. | |
1468 queue.run(function(callback) { | |
1469 if (nextCurrentDirEntry) { | |
1470 callback(); | |
1471 return; | |
1472 } | |
1473 this.volumeManager_.getDefaultDisplayRoot(function(displayRoot) { | |
1474 nextCurrentDirEntry = displayRoot; | |
1475 callback(); | |
1476 }.bind(this)); | |
1477 }.bind(this)); | |
1478 | |
1479 // If selection failed to be resolved (eg. didn't exist, in case of saving | |
1480 // a file, or in case of a fallback of the current directory, then try to | |
1481 // resolve again using the target name. | |
1482 queue.run(function(callback) { | |
1483 if (selectionEntry || !nextCurrentDirEntry || !this.initTargetName_) { | |
1484 callback(); | |
1485 return; | |
1486 } | |
1487 // Try to resolve as a file first. If it fails, then as a directory. | |
1488 nextCurrentDirEntry.getFile( | |
1489 this.initTargetName_, | |
1490 {}, | |
1491 function(targetEntry) { | |
1492 selectionEntry = targetEntry; | |
1493 callback(); | |
1494 }, function() { | |
1495 // Failed to resolve as a file | |
1496 nextCurrentDirEntry.getDirectory( | |
1497 this.initTargetName_, | |
1498 {}, | |
1499 function(targetEntry) { | |
1500 selectionEntry = targetEntry; | |
1501 callback(); | |
1502 }, function() { | |
1503 // Failed to resolve as either file or directory. | |
1504 callback(); | |
1505 }); | |
1506 }.bind(this)); | |
1507 }.bind(this)); | |
1508 | |
1509 | |
1510 // Finalize. | |
1511 queue.run(function(callback) { | |
1512 // Check directory change. | |
1513 tracker.stop(); | |
1514 if (tracker.hasChanged) { | |
1515 callback(); | |
1516 return; | |
1517 } | |
1518 // Finish setup current directory. | |
1519 this.finishSetupCurrentDirectory_( | |
1520 nextCurrentDirEntry, | |
1521 selectionEntry, | |
1522 this.initTargetName_); | |
1523 callback(); | |
1524 }.bind(this)); | |
1525 }; | |
1526 | |
1527 /** | |
1528 * @param {DirectoryEntry} directoryEntry Directory to be opened. | |
1529 * @param {Entry=} opt_selectionEntry Entry to be selected. | |
1530 * @param {string=} opt_suggestedName Suggested name for a non-existing\ | |
1531 * selection. | |
1532 * @private | |
1533 */ | |
1534 FileManager.prototype.finishSetupCurrentDirectory_ = function( | |
1535 directoryEntry, opt_selectionEntry, opt_suggestedName) { | |
1536 // Open the directory, and select the selection (if passed). | |
1537 if (util.isFakeEntry(directoryEntry)) { | |
1538 this.directoryModel_.specialSearch(directoryEntry, ''); | |
1539 } else { | |
1540 this.directoryModel_.changeDirectoryEntry(directoryEntry, function() { | |
1541 if (opt_selectionEntry) | |
1542 this.directoryModel_.selectEntry(opt_selectionEntry); | |
1543 }.bind(this)); | |
1544 } | |
1545 | |
1546 if (this.dialogType === DialogType.FULL_PAGE) { | |
1547 // In the FULL_PAGE mode if the restored URL points to a file we might | |
1548 // have to invoke a task after selecting it. | |
1549 if (this.params_.action === 'select') | |
1550 return; | |
1551 | |
1552 var task = null; | |
1553 // Handle restoring after crash, or the gallery action. | |
1554 // TODO(mtomasz): Use the gallery action instead of just the gallery | |
1555 // field. | |
1556 if (this.params_.gallery || | |
1557 this.params_.action === 'gallery' || | |
1558 this.params_.action === 'gallery-video') { | |
1559 if (!opt_selectionEntry) { | |
1560 // Non-existent file or a directory. | |
1561 // Reloading while the Gallery is open with empty or multiple | |
1562 // selection. Open the Gallery when the directory is scanned. | |
1563 task = function() { | |
1564 new FileTasks(this, this.params_).openGallery([]); | |
1565 }.bind(this); | |
1566 } else { | |
1567 // The file or the directory exists. | |
1568 task = function() { | |
1569 new FileTasks(this, this.params_).openGallery([opt_selectionEntry]); | |
1570 }.bind(this); | |
1571 } | |
1572 } else { | |
1573 // TODO(mtomasz): Implement remounting archives after crash. | |
1574 // See: crbug.com/333139 | |
1575 } | |
1576 | |
1577 // If there is a task to be run, run it after the scan is completed. | |
1578 if (task) { | |
1579 var listener = function() { | |
1580 if (!util.isSameEntry(this.directoryModel_.getCurrentDirEntry(), | |
1581 directoryEntry)) { | |
1582 // Opened on a different URL. Probably fallbacked. Therefore, | |
1583 // do not invoke a task. | |
1584 return; | |
1585 } | |
1586 this.directoryModel_.removeEventListener( | |
1587 'scan-completed', listener); | |
1588 task(); | |
1589 }.bind(this); | |
1590 this.directoryModel_.addEventListener('scan-completed', listener); | |
1591 } | |
1592 } else if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) { | |
1593 this.filenameInput_.value = opt_suggestedName || ''; | |
1594 this.selectTargetNameInFilenameInput_(); | |
1595 } | |
1596 }; | |
1597 | |
1598 /** | |
1599 * @private | |
1600 */ | |
1601 FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() { | |
1602 var entries = this.directoryModel_.getFileList().slice(); | |
1603 var directoryEntry = this.directoryModel_.getCurrentDirEntry(); | |
1604 if (!directoryEntry) | |
1605 return; | |
1606 // We don't pass callback here. When new metadata arrives, we have an | |
1607 // observer registered to update the UI. | |
1608 | |
1609 // TODO(dgozman): refresh content metadata only when modificationTime | |
1610 // changed. | |
1611 var isFakeEntry = util.isFakeEntry(directoryEntry); | |
1612 var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries); | |
1613 if (!isFakeEntry) | |
1614 this.metadataCache_.clearRecursively(directoryEntry, '*'); | |
1615 this.metadataCache_.get(getEntries, 'filesystem', null); | |
1616 | |
1617 if (this.isOnDrive()) | |
1618 this.metadataCache_.get(getEntries, 'drive', null); | |
1619 | |
1620 var visibleItems = this.currentList_.items; | |
1621 var visibleEntries = []; | |
1622 for (var i = 0; i < visibleItems.length; i++) { | |
1623 var index = this.currentList_.getIndexOfListItem(visibleItems[i]); | |
1624 var entry = this.directoryModel_.getFileList().item(index); | |
1625 // The following check is a workaround for the bug in list: sometimes item | |
1626 // does not have listIndex, and therefore is not found in the list. | |
1627 if (entry) visibleEntries.push(entry); | |
1628 } | |
1629 this.metadataCache_.get(visibleEntries, 'thumbnail', null); | |
1630 }; | |
1631 | |
1632 /** | |
1633 * @private | |
1634 */ | |
1635 FileManager.prototype.dailyUpdateModificationTime_ = function() { | |
1636 var entries = this.directoryModel_.getFileList().slice(); | |
1637 this.metadataCache_.get( | |
1638 entries, | |
1639 'filesystem', | |
1640 this.updateMetadataInUI_.bind(this, 'filesystem', entries)); | |
1641 | |
1642 setTimeout(this.dailyUpdateModificationTime_.bind(this), | |
1643 MILLISECONDS_IN_DAY); | |
1644 }; | |
1645 | |
1646 /** | |
1647 * @param {string} type Type of metadata changed. | |
1648 * @param {Array.<Entry>} entries Array of entries. | |
1649 * @param {Object.<string, Object>} props Map from entry URLs to metadata | |
1650 * props. | |
1651 * @private | |
1652 */ | |
1653 FileManager.prototype.updateMetadataInUI_ = function( | |
1654 type, entries, properties) { | |
1655 if (this.listType_ == FileManager.ListType.DETAIL) | |
1656 this.table_.updateListItemsMetadata(type, properties); | |
1657 else | |
1658 this.grid_.updateListItemsMetadata(type, properties); | |
1659 // TODO: update bottom panel thumbnails. | |
1660 }; | |
1661 | |
1662 /** | |
1663 * Restore the item which is being renamed while refreshing the file list. Do | |
1664 * nothing if no item is being renamed or such an item disappeared. | |
1665 * | |
1666 * While refreshing file list it gets repopulated with new file entries. | |
1667 * There is not a big difference whether DOM items stay the same or not. | |
1668 * Except for the item that the user is renaming. | |
1669 * | |
1670 * @private | |
1671 */ | |
1672 FileManager.prototype.restoreItemBeingRenamed_ = function() { | |
1673 if (!this.isRenamingInProgress()) | |
1674 return; | |
1675 | |
1676 var dm = this.directoryModel_; | |
1677 var leadIndex = dm.getFileListSelection().leadIndex; | |
1678 if (leadIndex < 0) | |
1679 return; | |
1680 | |
1681 var leadEntry = dm.getFileList().item(leadIndex); | |
1682 if (!util.isSameEntry(this.renameInput_.currentEntry, leadEntry)) | |
1683 return; | |
1684 | |
1685 var leadListItem = this.findListItemForNode_(this.renameInput_); | |
1686 if (this.currentList_ == this.table_.list) { | |
1687 this.table_.updateFileMetadata(leadListItem, leadEntry); | |
1688 } | |
1689 this.currentList_.restoreLeadItem(leadListItem); | |
1690 }; | |
1691 | |
1692 /** | |
1693 * TODO(mtomasz): Move this to a utility function working on the root type. | |
1694 * @return {boolean} True if the current directory content is from Google | |
1695 * Drive. | |
1696 */ | |
1697 FileManager.prototype.isOnDrive = function() { | |
1698 var rootType = this.directoryModel_.getCurrentRootType(); | |
1699 return rootType === RootType.DRIVE || | |
1700 rootType === RootType.DRIVE_SHARED_WITH_ME || | |
1701 rootType === RootType.DRIVE_RECENT || | |
1702 rootType === RootType.DRIVE_OFFLINE; | |
1703 }; | |
1704 | |
1705 /** | |
1706 * Overrides default handling for clicks on hyperlinks. | |
1707 * In a packaged apps links with targer='_blank' open in a new tab by | |
1708 * default, other links do not open at all. | |
1709 * | |
1710 * @param {Event} event Click event. | |
1711 * @private | |
1712 */ | |
1713 FileManager.prototype.onExternalLinkClick_ = function(event) { | |
1714 if (event.target.tagName != 'A' || !event.target.href) | |
1715 return; | |
1716 | |
1717 if (this.dialogType != DialogType.FULL_PAGE) | |
1718 this.onCancel_(); | |
1719 }; | |
1720 | |
1721 /** | |
1722 * Task combobox handler. | |
1723 * | |
1724 * @param {Object} event Event containing task which was clicked. | |
1725 * @private | |
1726 */ | |
1727 FileManager.prototype.onTaskItemClicked_ = function(event) { | |
1728 var selection = this.getSelection(); | |
1729 if (!selection.tasks) return; | |
1730 | |
1731 if (event.item.task) { | |
1732 // Task field doesn't exist on change-default dropdown item. | |
1733 selection.tasks.execute(event.item.task.taskId); | |
1734 } else { | |
1735 var extensions = []; | |
1736 | |
1737 for (var i = 0; i < selection.entries.length; i++) { | |
1738 var match = /\.(\w+)$/g.exec(selection.entries[i].toURL()); | |
1739 if (match) { | |
1740 var ext = match[1].toUpperCase(); | |
1741 if (extensions.indexOf(ext) == -1) { | |
1742 extensions.push(ext); | |
1743 } | |
1744 } | |
1745 } | |
1746 | |
1747 var format = ''; | |
1748 | |
1749 if (extensions.length == 1) { | |
1750 format = extensions[0]; | |
1751 } | |
1752 | |
1753 // Change default was clicked. We should open "change default" dialog. | |
1754 selection.tasks.showTaskPicker(this.defaultTaskPicker, | |
1755 loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'), | |
1756 strf('CHANGE_DEFAULT_CAPTION', format), | |
1757 this.onDefaultTaskDone_.bind(this)); | |
1758 } | |
1759 }; | |
1760 | |
1761 /** | |
1762 * Sets the given task as default, when this task is applicable. | |
1763 * | |
1764 * @param {Object} task Task to set as default. | |
1765 * @private | |
1766 */ | |
1767 FileManager.prototype.onDefaultTaskDone_ = function(task) { | |
1768 // TODO(dgozman): move this method closer to tasks. | |
1769 var selection = this.getSelection(); | |
1770 chrome.fileBrowserPrivate.setDefaultTask( | |
1771 task.taskId, | |
1772 util.entriesToURLs(selection.entries), | |
1773 selection.mimeTypes); | |
1774 selection.tasks = new FileTasks(this); | |
1775 selection.tasks.init(selection.entries, selection.mimeTypes); | |
1776 selection.tasks.display(this.taskItems_); | |
1777 this.refreshCurrentDirectoryMetadata_(); | |
1778 this.selectionHandler_.onFileSelectionChanged(); | |
1779 }; | |
1780 | |
1781 /** | |
1782 * @private | |
1783 */ | |
1784 FileManager.prototype.onPreferencesChanged_ = function() { | |
1785 var self = this; | |
1786 this.getPreferences_(function(prefs) { | |
1787 self.initDateTimeFormatters_(); | |
1788 self.refreshCurrentDirectoryMetadata_(); | |
1789 | |
1790 if (prefs.cellularDisabled) | |
1791 self.syncButton.setAttribute('checked', ''); | |
1792 else | |
1793 self.syncButton.removeAttribute('checked'); | |
1794 | |
1795 if (self.hostedButton.hasAttribute('checked') != | |
1796 prefs.hostedFilesDisabled && self.isOnDrive()) { | |
1797 self.directoryModel_.rescan(); | |
1798 } | |
1799 | |
1800 if (!prefs.hostedFilesDisabled) | |
1801 self.hostedButton.setAttribute('checked', ''); | |
1802 else | |
1803 self.hostedButton.removeAttribute('checked'); | |
1804 }, | |
1805 true /* refresh */); | |
1806 }; | |
1807 | |
1808 FileManager.prototype.onDriveConnectionChanged_ = function() { | |
1809 var connection = this.volumeManager_.getDriveConnectionState(); | |
1810 if (this.commandHandler) | |
1811 this.commandHandler.updateAvailability(); | |
1812 if (this.dialogContainer_) | |
1813 this.dialogContainer_.setAttribute('connection', connection.type); | |
1814 this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR); | |
1815 this.suggestAppsDialog.onDriveConnectionChanged(connection.type); | |
1816 }; | |
1817 | |
1818 /** | |
1819 * Tells whether the current directory is read only. | |
1820 * TODO(mtomasz): Remove and use EntryLocation directly. | |
1821 * @return {boolean} True if read only, false otherwise. | |
1822 */ | |
1823 FileManager.prototype.isOnReadonlyDirectory = function() { | |
1824 return this.directoryModel_.isReadOnly(); | |
1825 }; | |
1826 | |
1827 /** | |
1828 * @param {Event} Unmount event. | |
1829 * @private | |
1830 */ | |
1831 FileManager.prototype.onExternallyUnmounted_ = function(event) { | |
1832 if (event.volumeInfo === this.currentVolumeInfo_) { | |
1833 if (this.closeOnUnmount_) { | |
1834 // If the file manager opened automatically when a usb drive inserted, | |
1835 // user have never changed current volume (that implies the current | |
1836 // directory is still on the device) then close this window. | |
1837 window.close(); | |
1838 } | |
1839 } | |
1840 }; | |
1841 | |
1842 /** | |
1843 * Shows a modal-like file viewer/editor on top of the File Manager UI. | |
1844 * | |
1845 * @param {HTMLElement} popup Popup element. | |
1846 * @param {function()} closeCallback Function to call after the popup is | |
1847 * closed. | |
1848 */ | |
1849 FileManager.prototype.openFilePopup = function(popup, closeCallback) { | |
1850 this.closeFilePopup(); | |
1851 this.filePopup_ = popup; | |
1852 this.filePopupCloseCallback_ = closeCallback; | |
1853 this.dialogDom_.insertBefore( | |
1854 this.filePopup_, this.dialogDom_.querySelector('#iframe-drag-area')); | |
1855 this.filePopup_.focus(); | |
1856 this.document_.body.setAttribute('overlay-visible', ''); | |
1857 this.document_.querySelector('#iframe-drag-area').hidden = false; | |
1858 }; | |
1859 | |
1860 /** | |
1861 * Closes the modal-like file viewer/editor popup. | |
1862 */ | |
1863 FileManager.prototype.closeFilePopup = function() { | |
1864 if (this.filePopup_) { | |
1865 this.document_.body.removeAttribute('overlay-visible'); | |
1866 this.document_.querySelector('#iframe-drag-area').hidden = true; | |
1867 // The window resize would not be processed properly while the relevant | |
1868 // divs had 'display:none', force resize after the layout fired. | |
1869 setTimeout(this.onResize_.bind(this), 0); | |
1870 if (this.filePopup_.contentWindow && | |
1871 this.filePopup_.contentWindow.unload) { | |
1872 this.filePopup_.contentWindow.unload(); | |
1873 } | |
1874 | |
1875 if (this.filePopupCloseCallback_) { | |
1876 this.filePopupCloseCallback_(); | |
1877 this.filePopupCloseCallback_ = null; | |
1878 } | |
1879 | |
1880 // These operations have to be in the end, otherwise v8 crashes on an | |
1881 // assert. See: crbug.com/224174. | |
1882 this.dialogDom_.removeChild(this.filePopup_); | |
1883 this.filePopup_ = null; | |
1884 } | |
1885 }; | |
1886 | |
1887 /** | |
1888 * Updates visibility of the draggable app region in the modal-like file | |
1889 * viewer/editor. | |
1890 * | |
1891 * @param {boolean} visible True for visible, false otherwise. | |
1892 */ | |
1893 FileManager.prototype.onFilePopupAppRegionChanged = function(visible) { | |
1894 if (!this.filePopup_) | |
1895 return; | |
1896 | |
1897 this.document_.querySelector('#iframe-drag-area').hidden = !visible; | |
1898 }; | |
1899 | |
1900 /** | |
1901 * @return {Array.<Entry>} List of all entries in the current directory. | |
1902 */ | |
1903 FileManager.prototype.getAllEntriesInCurrentDirectory = function() { | |
1904 return this.directoryModel_.getFileList().slice(); | |
1905 }; | |
1906 | |
1907 FileManager.prototype.isRenamingInProgress = function() { | |
1908 return !!this.renameInput_.currentEntry; | |
1909 }; | |
1910 | |
1911 /** | |
1912 * @private | |
1913 */ | |
1914 FileManager.prototype.focusCurrentList_ = function() { | |
1915 if (this.listType_ == FileManager.ListType.DETAIL) | |
1916 this.table_.focus(); | |
1917 else // this.listType_ == FileManager.ListType.THUMBNAIL) | |
1918 this.grid_.focus(); | |
1919 }; | |
1920 | |
1921 /** | |
1922 * Return DirectoryEntry of the current directory or null. | |
1923 * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns | |
1924 * null if the directory model is not ready or the current directory is | |
1925 * not set. | |
1926 */ | |
1927 FileManager.prototype.getCurrentDirectoryEntry = function() { | |
1928 return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry(); | |
1929 }; | |
1930 | |
1931 /** | |
1932 * Deletes the selected file and directories recursively. | |
1933 */ | |
1934 FileManager.prototype.deleteSelection = function() { | |
1935 // TODO(mtomasz): Remove this temporary dialog. crbug.com/167364 | |
1936 var entries = this.getSelection().entries; | |
1937 var message = entries.length == 1 ? | |
1938 strf('GALLERY_CONFIRM_DELETE_ONE', entries[0].name) : | |
1939 strf('GALLERY_CONFIRM_DELETE_SOME', entries.length); | |
1940 this.confirm.show(message, function() { | |
1941 this.fileOperationManager_.deleteEntries(entries); | |
1942 }.bind(this)); | |
1943 }; | |
1944 | |
1945 /** | |
1946 * Shows the share dialog for the selected file or directory. | |
1947 */ | |
1948 FileManager.prototype.shareSelection = function() { | |
1949 var entries = this.getSelection().entries; | |
1950 if (entries.length != 1) { | |
1951 console.warn('Unable to share multiple items at once.'); | |
1952 return; | |
1953 } | |
1954 // Add the overlapped class to prevent the applicaiton window from | |
1955 // captureing mouse events. | |
1956 this.shareDialog_.show(entries[0], function(result) { | |
1957 if (result == ShareDialog.Result.NETWORK_ERROR) | |
1958 this.error.show(str('SHARE_ERROR')); | |
1959 }.bind(this)); | |
1960 }; | |
1961 | |
1962 /** | |
1963 * Creates a folder shortcut. | |
1964 * @param {Entry} entry A shortcut which refers to |entry| to be created. | |
1965 */ | |
1966 FileManager.prototype.createFolderShortcut = function(entry) { | |
1967 // Duplicate entry. | |
1968 if (this.folderShortcutExists(entry)) | |
1969 return; | |
1970 | |
1971 this.folderShortcutsModel_.add(entry); | |
1972 }; | |
1973 | |
1974 /** | |
1975 * Checkes if the shortcut which refers to the given folder exists or not. | |
1976 * @param {Entry} entry Entry of the folder to be checked. | |
1977 */ | |
1978 FileManager.prototype.folderShortcutExists = function(entry) { | |
1979 return this.folderShortcutsModel_.exists(entry); | |
1980 }; | |
1981 | |
1982 /** | |
1983 * Removes the folder shortcut. | |
1984 * @param {Entry} entry The shortcut which refers to |entry| is to be removed. | |
1985 */ | |
1986 FileManager.prototype.removeFolderShortcut = function(entry) { | |
1987 this.folderShortcutsModel_.remove(entry); | |
1988 }; | |
1989 | |
1990 /** | |
1991 * Blinks the selection. Used to give feedback when copying or cutting the | |
1992 * selection. | |
1993 */ | |
1994 FileManager.prototype.blinkSelection = function() { | |
1995 var selection = this.getSelection(); | |
1996 if (!selection || selection.totalCount == 0) | |
1997 return; | |
1998 | |
1999 for (var i = 0; i < selection.entries.length; i++) { | |
2000 var selectedIndex = selection.indexes[i]; | |
2001 var listItem = this.currentList_.getListItemByIndex(selectedIndex); | |
2002 if (listItem) | |
2003 this.blinkListItem_(listItem); | |
2004 } | |
2005 }; | |
2006 | |
2007 /** | |
2008 * @param {Element} listItem List item element. | |
2009 * @private | |
2010 */ | |
2011 FileManager.prototype.blinkListItem_ = function(listItem) { | |
2012 listItem.classList.add('blink'); | |
2013 setTimeout(function() { | |
2014 listItem.classList.remove('blink'); | |
2015 }, 100); | |
2016 }; | |
2017 | |
2018 /** | |
2019 * @private | |
2020 */ | |
2021 FileManager.prototype.selectTargetNameInFilenameInput_ = function() { | |
2022 var input = this.filenameInput_; | |
2023 input.focus(); | |
2024 var selectionEnd = input.value.lastIndexOf('.'); | |
2025 if (selectionEnd == -1) { | |
2026 input.select(); | |
2027 } else { | |
2028 input.selectionStart = 0; | |
2029 input.selectionEnd = selectionEnd; | |
2030 } | |
2031 }; | |
2032 | |
2033 /** | |
2034 * Handles mouse click or tap. | |
2035 * | |
2036 * @param {Event} event The click event. | |
2037 * @private | |
2038 */ | |
2039 FileManager.prototype.onDetailClick_ = function(event) { | |
2040 if (this.isRenamingInProgress()) { | |
2041 // Don't pay attention to clicks during a rename. | |
2042 return; | |
2043 } | |
2044 | |
2045 var listItem = this.findListItemForEvent_(event); | |
2046 var selection = this.getSelection(); | |
2047 if (!listItem || !listItem.selected || selection.totalCount != 1) { | |
2048 return; | |
2049 } | |
2050 | |
2051 // React on double click, but only if both clicks hit the same item. | |
2052 // TODO(mtomasz): Simplify it, and use a double click handler if possible. | |
2053 var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined; | |
2054 this.lastClickedItem_ = listItem; | |
2055 | |
2056 if (event.detail != clickNumber) | |
2057 return; | |
2058 | |
2059 var entry = selection.entries[0]; | |
2060 if (entry.isDirectory) { | |
2061 this.onDirectoryAction_(entry); | |
2062 } else { | |
2063 this.dispatchSelectionAction_(); | |
2064 } | |
2065 }; | |
2066 | |
2067 /** | |
2068 * @private | |
2069 */ | |
2070 FileManager.prototype.dispatchSelectionAction_ = function() { | |
2071 if (this.dialogType == DialogType.FULL_PAGE) { | |
2072 var selection = this.getSelection(); | |
2073 var tasks = selection.tasks; | |
2074 var urls = selection.urls; | |
2075 var mimeTypes = selection.mimeTypes; | |
2076 if (tasks) | |
2077 tasks.executeDefault(); | |
2078 return true; | |
2079 } | |
2080 if (!this.okButton_.disabled) { | |
2081 this.onOk_(); | |
2082 return true; | |
2083 } | |
2084 return false; | |
2085 }; | |
2086 | |
2087 /** | |
2088 * Opens the suggest file dialog. | |
2089 * | |
2090 * @param {Entry} entry Entry of the file. | |
2091 * @param {function()} onSuccess Success callback. | |
2092 * @param {function()} onCancelled User-cancelled callback. | |
2093 * @param {function()} onFailure Failure callback. | |
2094 * @private | |
2095 */ | |
2096 FileManager.prototype.openSuggestAppsDialog = | |
2097 function(entry, onSuccess, onCancelled, onFailure) { | |
2098 if (!url) { | |
2099 onFailure(); | |
2100 return; | |
2101 } | |
2102 | |
2103 this.metadataCache_.get([entry], 'drive', function(props) { | |
2104 if (!props || !props[0] || !props[0].contentMimeType) { | |
2105 onFailure(); | |
2106 return; | |
2107 } | |
2108 | |
2109 var basename = entry.name; | |
2110 var splitted = PathUtil.splitExtension(basename); | |
2111 var filename = splitted[0]; | |
2112 var extension = splitted[1]; | |
2113 var mime = props[0].contentMimeType; | |
2114 | |
2115 // Returns with failure if the file has neither extension nor mime. | |
2116 if (!extension || !mime) { | |
2117 onFailure(); | |
2118 return; | |
2119 } | |
2120 | |
2121 var onDialogClosed = function(result) { | |
2122 switch (result) { | |
2123 case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL: | |
2124 onSuccess(); | |
2125 break; | |
2126 case SuggestAppsDialog.Result.FAILED: | |
2127 onFailure(); | |
2128 break; | |
2129 default: | |
2130 onCancelled(); | |
2131 } | |
2132 }; | |
2133 | |
2134 if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) { | |
2135 this.suggestAppsDialog.showByFilename(filename, onDialogClosed); | |
2136 } else { | |
2137 this.suggestAppsDialog.showByExtensionAndMime( | |
2138 extension, mime, onDialogClosed); | |
2139 } | |
2140 }.bind(this)); | |
2141 }; | |
2142 | |
2143 /** | |
2144 * Called when a dialog is shown or hidden. | |
2145 * @param {boolean} flag True if a dialog is shown, false if hidden. | |
2146 */ | |
2147 FileManager.prototype.onDialogShownOrHidden = function(show) { | |
2148 if (show) { | |
2149 // If a dialog is shown, activate the window. | |
2150 var appWindow = chrome.app.window.current(); | |
2151 if (appWindow) | |
2152 appWindow.focus(); | |
2153 } | |
2154 | |
2155 // Set/unset a flag to disable dragging on the title area. | |
2156 this.dialogContainer_.classList.toggle('disable-header-drag', show); | |
2157 }; | |
2158 | |
2159 /** | |
2160 * Executes directory action (i.e. changes directory). | |
2161 * | |
2162 * @param {DirectoryEntry} entry Directory entry to which directory should be | |
2163 * changed. | |
2164 * @private | |
2165 */ | |
2166 FileManager.prototype.onDirectoryAction_ = function(entry) { | |
2167 return this.directoryModel_.changeDirectoryEntry(entry); | |
2168 }; | |
2169 | |
2170 /** | |
2171 * Update the window title. | |
2172 * @private | |
2173 */ | |
2174 FileManager.prototype.updateTitle_ = function() { | |
2175 if (this.dialogType != DialogType.FULL_PAGE) | |
2176 return; | |
2177 | |
2178 if (!this.currentVolumeInfo_) | |
2179 return; | |
2180 | |
2181 this.document_.title = this.currentVolumeInfo_.label; | |
2182 }; | |
2183 | |
2184 /** | |
2185 * Update the gear menu. | |
2186 * @private | |
2187 */ | |
2188 FileManager.prototype.updateGearMenu_ = function() { | |
2189 var hideItemsForDrive = !this.isOnDrive(); | |
2190 this.syncButton.hidden = hideItemsForDrive; | |
2191 this.hostedButton.hidden = hideItemsForDrive; | |
2192 this.document_.getElementById('drive-separator').hidden = | |
2193 hideItemsForDrive; | |
2194 this.refreshRemainingSpace_(true); // Show loading caption. | |
2195 }; | |
2196 | |
2197 /** | |
2198 * Update menus that move the window to the other profile's desktop. | |
2199 * TODO(hirono): Add the GearMenu class and make it a member of the class. | |
2200 * TODO(hirono): Handle the case where a profile is added while the menu is | |
2201 * opened. | |
2202 * @private | |
2203 */ | |
2204 FileManager.prototype.updateVisitDesktopMenus_ = function() { | |
2205 var gearMenu = this.document_.querySelector('#gear-menu'); | |
2206 var separator = | |
2207 this.document_.querySelector('#multi-profile-separator'); | |
2208 | |
2209 // Remove existing menu items. | |
2210 var oldItems = | |
2211 this.document_.querySelectorAll('#gear-menu .visit-desktop'); | |
2212 for (var i = 0; i < oldItems.length; i++) { | |
2213 gearMenu.removeChild(oldItems[i]); | |
2214 } | |
2215 separator.hidden = true; | |
2216 | |
2217 if (this.dialogType !== DialogType.FULL_PAGE) | |
2218 return; | |
2219 | |
2220 // Obtain the profile information. | |
2221 chrome.fileBrowserPrivate.getProfiles(function(profiles, | |
2222 currentId, | |
2223 displayedId) { | |
2224 // Check if the menus are needed or not. | |
2225 var insertingPosition = separator.nextSibling; | |
2226 if (profiles.length === 1 && profiles[0].profileId === displayedId) | |
2227 return; | |
2228 | |
2229 separator.hidden = false; | |
2230 for (var i = 0; i < profiles.length; i++) { | |
2231 var profile = profiles[i]; | |
2232 if (profile.profileId === displayedId) | |
2233 continue; | |
2234 var item = this.document_.createElement('menuitem'); | |
2235 cr.ui.MenuItem.decorate(item); | |
2236 gearMenu.insertBefore(item, insertingPosition); | |
2237 item.className = 'visit-desktop'; | |
2238 item.label = strf('VISIT_DESKTOP_OF_USER', | |
2239 profile.displayName, | |
2240 profile.profileId); | |
2241 item.addEventListener('activate', function(inProfile, event) { | |
2242 // Stop propagate and hide the menu manually, in order to prevent the | |
2243 // focus from being back to the button. (cf. http://crbug.com/248479) | |
2244 event.stopPropagation(); | |
2245 this.gearButton_.hideMenu(); | |
2246 this.gearButton_.blur(); | |
2247 chrome.fileBrowserPrivate.visitDesktop(inProfile.profileId); | |
2248 }.bind(this, profile)); | |
2249 } | |
2250 }.bind(this)); | |
2251 }; | |
2252 | |
2253 /** | |
2254 * Refreshes space info of the current volume. | |
2255 * @param {boolean} showLoadingCaption Whether show loading caption or not. | |
2256 * @private | |
2257 */ | |
2258 FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) { | |
2259 if (!this.currentVolumeInfo_) | |
2260 return; | |
2261 | |
2262 var volumeSpaceInfoLabel = | |
2263 this.dialogDom_.querySelector('#volume-space-info-label'); | |
2264 var volumeSpaceInnerBar = | |
2265 this.dialogDom_.querySelector('#volume-space-info-bar'); | |
2266 var volumeSpaceOuterBar = | |
2267 this.dialogDom_.querySelector('#volume-space-info-bar').parentNode; | |
2268 | |
2269 volumeSpaceInnerBar.setAttribute('pending', ''); | |
2270 | |
2271 if (showLoadingCaption) { | |
2272 volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO'); | |
2273 volumeSpaceInnerBar.style.width = '100%'; | |
2274 } | |
2275 | |
2276 var currentVolumeInfo = this.currentVolumeInfo_; | |
2277 chrome.fileBrowserPrivate.getSizeStats( | |
2278 currentVolumeInfo.volumeId, function(result) { | |
2279 var volumeInfo = this.volumeManager_.getVolumeInfo( | |
2280 this.directoryModel_.getCurrentDirEntry()); | |
2281 if (currentVolumeInfo !== this.currentVolumeInfo_) | |
2282 return; | |
2283 updateSpaceInfo(result, | |
2284 volumeSpaceInnerBar, | |
2285 volumeSpaceInfoLabel, | |
2286 volumeSpaceOuterBar); | |
2287 }.bind(this)); | |
2288 }; | |
2289 | |
2290 /** | |
2291 * Update the UI when the current directory changes. | |
2292 * | |
2293 * @param {Event} event The directory-changed event. | |
2294 * @private | |
2295 */ | |
2296 FileManager.prototype.onDirectoryChanged_ = function(event) { | |
2297 var newCurrentVolumeInfo = this.volumeManager_.getVolumeInfo( | |
2298 event.newDirEntry); | |
2299 | |
2300 // If volume has changed, then update the gear menu. | |
2301 if (this.currentVolumeInfo_ !== newCurrentVolumeInfo) { | |
2302 this.updateGearMenu_(); | |
2303 // If the volume has changed, and it was previously set, then do not | |
2304 // close on unmount anymore. | |
2305 if (this.currentVolumeInfo_) | |
2306 this.closeOnUnmount_ = false; | |
2307 } | |
2308 | |
2309 // Remember the current volume info. | |
2310 this.currentVolumeInfo_ = newCurrentVolumeInfo; | |
2311 | |
2312 this.selectionHandler_.onFileSelectionChanged(); | |
2313 this.ui_.searchBox.clear(); | |
2314 // TODO(mtomasz): Consider remembering the selection. | |
2315 util.updateAppState( | |
2316 this.getCurrentDirectoryEntry() ? | |
2317 this.getCurrentDirectoryEntry().toURL() : '', | |
2318 '' /* selectionURL */, | |
2319 '' /* opt_param */); | |
2320 | |
2321 if (this.commandHandler) | |
2322 this.commandHandler.updateAvailability(); | |
2323 | |
2324 this.updateUnformattedVolumeStatus_(); | |
2325 this.updateTitle_(); | |
2326 | |
2327 var currentEntry = this.getCurrentDirectoryEntry(); | |
2328 this.previewPanel_.currentEntry = util.isFakeEntry(currentEntry) ? | |
2329 null : currentEntry; | |
2330 }; | |
2331 | |
2332 FileManager.prototype.updateUnformattedVolumeStatus_ = function() { | |
2333 var volumeInfo = this.volumeManager_.getVolumeInfo( | |
2334 this.directoryModel_.getCurrentDirEntry()); | |
2335 | |
2336 if (volumeInfo && volumeInfo.error) { | |
2337 this.dialogDom_.setAttribute('unformatted', ''); | |
2338 | |
2339 var errorNode = this.dialogDom_.querySelector('#format-panel > .error'); | |
2340 if (volumeInfo.error == util.VolumeError.UNSUPPORTED_FILESYSTEM) { | |
2341 errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING'); | |
2342 } else { | |
2343 errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING'); | |
2344 } | |
2345 | |
2346 // Update 'canExecute' for format command so the format button's disabled | |
2347 // property is properly set. | |
2348 if (this.commandHandler) | |
2349 this.commandHandler.updateAvailability(); | |
2350 } else { | |
2351 this.dialogDom_.removeAttribute('unformatted'); | |
2352 } | |
2353 }; | |
2354 | |
2355 FileManager.prototype.findListItemForEvent_ = function(event) { | |
2356 return this.findListItemForNode_(event.touchedElement || event.srcElement); | |
2357 }; | |
2358 | |
2359 FileManager.prototype.findListItemForNode_ = function(node) { | |
2360 var item = this.currentList_.getListItemAncestor(node); | |
2361 // TODO(serya): list should check that. | |
2362 return item && this.currentList_.isItem(item) ? item : null; | |
2363 }; | |
2364 | |
2365 /** | |
2366 * Unload handler for the page. | |
2367 * @private | |
2368 */ | |
2369 FileManager.prototype.onUnload_ = function() { | |
2370 if (this.directoryModel_) | |
2371 this.directoryModel_.dispose(); | |
2372 if (this.volumeManager_) | |
2373 this.volumeManager_.dispose(); | |
2374 if (this.filePopup_ && | |
2375 this.filePopup_.contentWindow && | |
2376 this.filePopup_.contentWindow.unload) | |
2377 this.filePopup_.contentWindow.unload(true /* exiting */); | |
2378 if (this.progressCenterPanel_) | |
2379 this.backgroundPage_.background.progressCenter.removePanel( | |
2380 this.progressCenterPanel_); | |
2381 if (this.fileOperationManager_) { | |
2382 if (this.onCopyProgressBound_) { | |
2383 this.fileOperationManager_.removeEventListener( | |
2384 'copy-progress', this.onCopyProgressBound_); | |
2385 } | |
2386 if (this.onEntryChangedBound_) { | |
2387 this.fileOperationManager_.removeEventListener( | |
2388 'entry-changed', this.onEntryChangedBound_); | |
2389 } | |
2390 } | |
2391 window.closing = true; | |
2392 if (this.backgroundPage_) | |
2393 this.backgroundPage_.background.tryClose(); | |
2394 }; | |
2395 | |
2396 FileManager.prototype.initiateRename = function() { | |
2397 var item = this.currentList_.ensureLeadItemExists(); | |
2398 if (!item) | |
2399 return; | |
2400 var label = item.querySelector('.filename-label'); | |
2401 var input = this.renameInput_; | |
2402 | |
2403 input.value = label.textContent; | |
2404 item.setAttribute('renaming', ''); | |
2405 label.parentNode.appendChild(input); | |
2406 input.focus(); | |
2407 var selectionEnd = input.value.lastIndexOf('.'); | |
2408 if (selectionEnd == -1) { | |
2409 input.select(); | |
2410 } else { | |
2411 input.selectionStart = 0; | |
2412 input.selectionEnd = selectionEnd; | |
2413 } | |
2414 | |
2415 // This has to be set late in the process so we don't handle spurious | |
2416 // blur events. | |
2417 input.currentEntry = this.currentList_.dataModel.item(item.listIndex); | |
2418 this.table_.startBatchUpdates(); | |
2419 this.grid_.startBatchUpdates(); | |
2420 }; | |
2421 | |
2422 /** | |
2423 * @type {Event} Key event. | |
2424 * @private | |
2425 */ | |
2426 FileManager.prototype.onRenameInputKeyDown_ = function(event) { | |
2427 if (!this.isRenamingInProgress()) | |
2428 return; | |
2429 | |
2430 // Do not move selection or lead item in list during rename. | |
2431 if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') { | |
2432 event.stopPropagation(); | |
2433 } | |
2434 | |
2435 switch (util.getKeyModifiers(event) + event.keyIdentifier) { | |
2436 case 'U+001B': // Escape | |
2437 this.cancelRename_(); | |
2438 event.preventDefault(); | |
2439 break; | |
2440 | |
2441 case 'Enter': | |
2442 this.commitRename_(); | |
2443 event.preventDefault(); | |
2444 break; | |
2445 } | |
2446 }; | |
2447 | |
2448 /** | |
2449 * @type {Event} Blur event. | |
2450 * @private | |
2451 */ | |
2452 FileManager.prototype.onRenameInputBlur_ = function(event) { | |
2453 if (this.isRenamingInProgress() && !this.renameInput_.validation_) | |
2454 this.commitRename_(); | |
2455 }; | |
2456 | |
2457 /** | |
2458 * @private | |
2459 */ | |
2460 FileManager.prototype.commitRename_ = function() { | |
2461 var input = this.renameInput_; | |
2462 var entry = input.currentEntry; | |
2463 var newName = input.value; | |
2464 | |
2465 if (newName == entry.name) { | |
2466 this.cancelRename_(); | |
2467 return; | |
2468 } | |
2469 | |
2470 var renamedItemElement = this.findListItemForNode_(this.renameInput_); | |
2471 var nameNode = renamedItemElement.querySelector('.filename-label'); | |
2472 | |
2473 input.validation_ = true; | |
2474 var validationDone = function(valid) { | |
2475 input.validation_ = false; | |
2476 | |
2477 if (!valid) { | |
2478 // Cancel rename if it fails to restore focus from alert dialog. | |
2479 // Otherwise, just cancel the commitment and continue to rename. | |
2480 if (this.document_.activeElement != input) | |
2481 this.cancelRename_(); | |
2482 return; | |
2483 } | |
2484 | |
2485 // Validation succeeded. Do renaming. | |
2486 this.renameInput_.currentEntry = null; | |
2487 if (this.renameInput_.parentNode) | |
2488 this.renameInput_.parentNode.removeChild(this.renameInput_); | |
2489 renamedItemElement.setAttribute('renaming', 'provisional'); | |
2490 | |
2491 // Optimistically apply new name immediately to avoid flickering in | |
2492 // case of success. | |
2493 nameNode.textContent = newName; | |
2494 | |
2495 util.rename( | |
2496 entry, newName, | |
2497 function(newEntry) { | |
2498 this.directoryModel_.onRenameEntry(entry, newEntry); | |
2499 renamedItemElement.removeAttribute('renaming'); | |
2500 this.table_.endBatchUpdates(); | |
2501 this.grid_.endBatchUpdates(); | |
2502 }.bind(this), | |
2503 function(error) { | |
2504 // Write back to the old name. | |
2505 nameNode.textContent = entry.name; | |
2506 renamedItemElement.removeAttribute('renaming'); | |
2507 this.table_.endBatchUpdates(); | |
2508 this.grid_.endBatchUpdates(); | |
2509 | |
2510 // Show error dialog. | |
2511 var message; | |
2512 if (error.name == util.FileError.PATH_EXISTS_ERR || | |
2513 error.name == util.FileError.TYPE_MISMATCH_ERR) { | |
2514 // Check the existing entry is file or not. | |
2515 // 1) If the entry is a file: | |
2516 // a) If we get PATH_EXISTS_ERR, a file exists. | |
2517 // b) If we get TYPE_MISMATCH_ERR, a directory exists. | |
2518 // 2) If the entry is a directory: | |
2519 // a) If we get PATH_EXISTS_ERR, a directory exists. | |
2520 // b) If we get TYPE_MISMATCH_ERR, a file exists. | |
2521 message = strf( | |
2522 (entry.isFile && error.name == | |
2523 util.FileError.PATH_EXISTS_ERR) || | |
2524 (!entry.isFile && error.name == | |
2525 util.FileError.TYPE_MISMATCH_ERR) ? | |
2526 'FILE_ALREADY_EXISTS' : | |
2527 'DIRECTORY_ALREADY_EXISTS', | |
2528 newName); | |
2529 } else { | |
2530 message = strf('ERROR_RENAMING', entry.name, | |
2531 util.getFileErrorString(error.name)); | |
2532 } | |
2533 | |
2534 this.alert.show(message); | |
2535 }.bind(this)); | |
2536 }; | |
2537 | |
2538 // TODO(haruki): this.getCurrentDirectoryEntry() might not return the actual | |
2539 // parent if the directory content is a search result. Fix it to do proper | |
2540 // validation. | |
2541 this.validateFileName_(this.getCurrentDirectoryEntry(), | |
2542 newName, | |
2543 validationDone.bind(this)); | |
2544 }; | |
2545 | |
2546 /** | |
2547 * @private | |
2548 */ | |
2549 FileManager.prototype.cancelRename_ = function() { | |
2550 this.renameInput_.currentEntry = null; | |
2551 | |
2552 var item = this.findListItemForNode_(this.renameInput_); | |
2553 if (item) | |
2554 item.removeAttribute('renaming'); | |
2555 | |
2556 var parent = this.renameInput_.parentNode; | |
2557 if (parent) | |
2558 parent.removeChild(this.renameInput_); | |
2559 | |
2560 this.table_.endBatchUpdates(); | |
2561 this.grid_.endBatchUpdates(); | |
2562 }; | |
2563 | |
2564 /** | |
2565 * @param {Event} Key event. | |
2566 * @private | |
2567 */ | |
2568 FileManager.prototype.onFilenameInputInput_ = function() { | |
2569 this.selectionHandler_.updateOkButton(); | |
2570 }; | |
2571 | |
2572 /** | |
2573 * @param {Event} Key event. | |
2574 * @private | |
2575 */ | |
2576 FileManager.prototype.onFilenameInputKeyDown_ = function(event) { | |
2577 if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */) | |
2578 this.okButton_.click(); | |
2579 }; | |
2580 | |
2581 /** | |
2582 * @param {Event} Focus event. | |
2583 * @private | |
2584 */ | |
2585 FileManager.prototype.onFilenameInputFocus_ = function(event) { | |
2586 var input = this.filenameInput_; | |
2587 | |
2588 // On focus we want to select everything but the extension, but | |
2589 // Chrome will select-all after the focus event completes. We | |
2590 // schedule a timeout to alter the focus after that happens. | |
2591 setTimeout(function() { | |
2592 var selectionEnd = input.value.lastIndexOf('.'); | |
2593 if (selectionEnd == -1) { | |
2594 input.select(); | |
2595 } else { | |
2596 input.selectionStart = 0; | |
2597 input.selectionEnd = selectionEnd; | |
2598 } | |
2599 }, 0); | |
2600 }; | |
2601 | |
2602 /** | |
2603 * @private | |
2604 */ | |
2605 FileManager.prototype.onScanStarted_ = function() { | |
2606 if (this.scanInProgress_) { | |
2607 this.table_.list.endBatchUpdates(); | |
2608 this.grid_.endBatchUpdates(); | |
2609 } | |
2610 | |
2611 if (this.commandHandler) | |
2612 this.commandHandler.updateAvailability(); | |
2613 this.table_.list.startBatchUpdates(); | |
2614 this.grid_.startBatchUpdates(); | |
2615 this.scanInProgress_ = true; | |
2616 | |
2617 this.scanUpdatedAtLeastOnceOrCompleted_ = false; | |
2618 if (this.scanCompletedTimer_) { | |
2619 clearTimeout(this.scanCompletedTimer_); | |
2620 this.scanCompletedTimer_ = null; | |
2621 } | |
2622 | |
2623 if (this.scanUpdatedTimer_) { | |
2624 clearTimeout(this.scanUpdatedTimer_); | |
2625 this.scanUpdatedTimer_ = null; | |
2626 } | |
2627 | |
2628 if (this.spinner_.hidden) { | |
2629 this.cancelSpinnerTimeout_(); | |
2630 this.showSpinnerTimeout_ = | |
2631 setTimeout(this.showSpinner_.bind(this, true), 500); | |
2632 } | |
2633 }; | |
2634 | |
2635 /** | |
2636 * @private | |
2637 */ | |
2638 FileManager.prototype.onScanCompleted_ = function() { | |
2639 if (!this.scanInProgress_) { | |
2640 console.error('Scan-completed event recieved. But scan is not started.'); | |
2641 return; | |
2642 } | |
2643 | |
2644 if (this.commandHandler) | |
2645 this.commandHandler.updateAvailability(); | |
2646 this.hideSpinnerLater_(); | |
2647 | |
2648 if (this.scanUpdatedTimer_) { | |
2649 clearTimeout(this.scanUpdatedTimer_); | |
2650 this.scanUpdatedTimer_ = null; | |
2651 } | |
2652 | |
2653 // To avoid flickering postpone updating the ui by a small amount of time. | |
2654 // There is a high chance, that metadata will be received within 50 ms. | |
2655 this.scanCompletedTimer_ = setTimeout(function() { | |
2656 // Check if batch updates are already finished by onScanUpdated_(). | |
2657 if (!this.scanUpdatedAtLeastOnceOrCompleted_) { | |
2658 this.scanUpdatedAtLeastOnceOrCompleted_ = true; | |
2659 this.updateMiddleBarVisibility_(); | |
2660 } | |
2661 | |
2662 this.scanInProgress_ = false; | |
2663 this.table_.list.endBatchUpdates(); | |
2664 this.grid_.endBatchUpdates(); | |
2665 this.scanCompletedTimer_ = null; | |
2666 }.bind(this), 50); | |
2667 }; | |
2668 | |
2669 /** | |
2670 * @private | |
2671 */ | |
2672 FileManager.prototype.onScanUpdated_ = function() { | |
2673 if (!this.scanInProgress_) { | |
2674 console.error('Scan-updated event recieved. But scan is not started.'); | |
2675 return; | |
2676 } | |
2677 | |
2678 if (this.scanUpdatedTimer_ || this.scanCompletedTimer_) | |
2679 return; | |
2680 | |
2681 // Show contents incrementally by finishing batch updated, but only after | |
2682 // 200ms elapsed, to avoid flickering when it is not necessary. | |
2683 this.scanUpdatedTimer_ = setTimeout(function() { | |
2684 // We need to hide the spinner only once. | |
2685 if (!this.scanUpdatedAtLeastOnceOrCompleted_) { | |
2686 this.scanUpdatedAtLeastOnceOrCompleted_ = true; | |
2687 this.hideSpinnerLater_(); | |
2688 this.updateMiddleBarVisibility_(); | |
2689 } | |
2690 | |
2691 // Update the UI. | |
2692 if (this.scanInProgress_) { | |
2693 this.table_.list.endBatchUpdates(); | |
2694 this.grid_.endBatchUpdates(); | |
2695 this.table_.list.startBatchUpdates(); | |
2696 this.grid_.startBatchUpdates(); | |
2697 } | |
2698 this.scanUpdatedTimer_ = null; | |
2699 }.bind(this), 200); | |
2700 }; | |
2701 | |
2702 /** | |
2703 * @private | |
2704 */ | |
2705 FileManager.prototype.onScanCancelled_ = function() { | |
2706 if (!this.scanInProgress_) { | |
2707 console.error('Scan-cancelled event recieved. But scan is not started.'); | |
2708 return; | |
2709 } | |
2710 | |
2711 if (this.commandHandler) | |
2712 this.commandHandler.updateAvailability(); | |
2713 this.hideSpinnerLater_(); | |
2714 if (this.scanCompletedTimer_) { | |
2715 clearTimeout(this.scanCompletedTimer_); | |
2716 this.scanCompletedTimer_ = null; | |
2717 } | |
2718 if (this.scanUpdatedTimer_) { | |
2719 clearTimeout(this.scanUpdatedTimer_); | |
2720 this.scanUpdatedTimer_ = null; | |
2721 } | |
2722 // Finish unfinished batch updates. | |
2723 if (!this.scanUpdatedAtLeastOnceOrCompleted_) { | |
2724 this.scanUpdatedAtLeastOnceOrCompleted_ = true; | |
2725 this.updateMiddleBarVisibility_(); | |
2726 } | |
2727 | |
2728 this.scanInProgress_ = false; | |
2729 this.table_.list.endBatchUpdates(); | |
2730 this.grid_.endBatchUpdates(); | |
2731 }; | |
2732 | |
2733 /** | |
2734 * Handle the 'rescan-completed' from the DirectoryModel. | |
2735 * @private | |
2736 */ | |
2737 FileManager.prototype.onRescanCompleted_ = function() { | |
2738 this.selectionHandler_.onFileSelectionChanged(); | |
2739 }; | |
2740 | |
2741 /** | |
2742 * @private | |
2743 */ | |
2744 FileManager.prototype.cancelSpinnerTimeout_ = function() { | |
2745 if (this.showSpinnerTimeout_) { | |
2746 clearTimeout(this.showSpinnerTimeout_); | |
2747 this.showSpinnerTimeout_ = null; | |
2748 } | |
2749 }; | |
2750 | |
2751 /** | |
2752 * @private | |
2753 */ | |
2754 FileManager.prototype.hideSpinnerLater_ = function() { | |
2755 this.cancelSpinnerTimeout_(); | |
2756 this.showSpinner_(false); | |
2757 }; | |
2758 | |
2759 /** | |
2760 * @param {boolean} on True to show, false to hide. | |
2761 * @private | |
2762 */ | |
2763 FileManager.prototype.showSpinner_ = function(on) { | |
2764 if (on && this.directoryModel_ && this.directoryModel_.isScanning()) | |
2765 this.spinner_.hidden = false; | |
2766 | |
2767 if (!on && (!this.directoryModel_ || | |
2768 !this.directoryModel_.isScanning() || | |
2769 this.directoryModel_.getFileList().length != 0)) { | |
2770 this.spinner_.hidden = true; | |
2771 } | |
2772 }; | |
2773 | |
2774 FileManager.prototype.createNewFolder = function() { | |
2775 var defaultName = str('DEFAULT_NEW_FOLDER_NAME'); | |
2776 | |
2777 // Find a name that doesn't exist in the data model. | |
2778 var files = this.directoryModel_.getFileList(); | |
2779 var hash = {}; | |
2780 for (var i = 0; i < files.length; i++) { | |
2781 var name = files.item(i).name; | |
2782 // Filtering names prevents from conflicts with prototype's names | |
2783 // and '__proto__'. | |
2784 if (name.substring(0, defaultName.length) == defaultName) | |
2785 hash[name] = 1; | |
2786 } | |
2787 | |
2788 var baseName = defaultName; | |
2789 var separator = ''; | |
2790 var suffix = ''; | |
2791 var index = ''; | |
2792 | |
2793 var advance = function() { | |
2794 separator = ' ('; | |
2795 suffix = ')'; | |
2796 index++; | |
2797 }; | |
2798 | |
2799 var current = function() { | |
2800 return baseName + separator + index + suffix; | |
2801 }; | |
2802 | |
2803 // Accessing hasOwnProperty is safe since hash properties filtered. | |
2804 while (hash.hasOwnProperty(current())) { | |
2805 advance(); | |
2806 } | |
2807 | |
2808 var self = this; | |
2809 var list = self.currentList_; | |
2810 var tryCreate = function() { | |
2811 self.directoryModel_.createDirectory(current(), | |
2812 onSuccess, onError); | |
2813 }; | |
2814 | |
2815 var onSuccess = function(entry) { | |
2816 metrics.recordUserAction('CreateNewFolder'); | |
2817 list.selectedItem = entry; | |
2818 self.initiateRename(); | |
2819 }; | |
2820 | |
2821 var onError = function(error) { | |
2822 self.alert.show(strf('ERROR_CREATING_FOLDER', current(), | |
2823 util.getFileErrorString(error.name))); | |
2824 }; | |
2825 | |
2826 tryCreate(); | |
2827 }; | |
2828 | |
2829 /** | |
2830 * @param {Event} event Click event. | |
2831 * @private | |
2832 */ | |
2833 FileManager.prototype.onDetailViewButtonClick_ = function(event) { | |
2834 // Stop propagate and hide the menu manually, in order to prevent the focus | |
2835 // from being back to the button. (cf. http://crbug.com/248479) | |
2836 event.stopPropagation(); | |
2837 this.gearButton_.hideMenu(); | |
2838 this.gearButton_.blur(); | |
2839 this.setListType(FileManager.ListType.DETAIL); | |
2840 }; | |
2841 | |
2842 /** | |
2843 * @param {Event} event Click event. | |
2844 * @private | |
2845 */ | |
2846 FileManager.prototype.onThumbnailViewButtonClick_ = function(event) { | |
2847 // Stop propagate and hide the menu manually, in order to prevent the focus | |
2848 // from being back to the button. (cf. http://crbug.com/248479) | |
2849 event.stopPropagation(); | |
2850 this.gearButton_.hideMenu(); | |
2851 this.gearButton_.blur(); | |
2852 this.setListType(FileManager.ListType.THUMBNAIL); | |
2853 }; | |
2854 | |
2855 /** | |
2856 * KeyDown event handler for the document. | |
2857 * @param {Event} event Key event. | |
2858 * @private | |
2859 */ | |
2860 FileManager.prototype.onKeyDown_ = function(event) { | |
2861 if (event.keyCode === 9) // Tab | |
2862 this.pressingTab_ = true; | |
2863 | |
2864 if (event.srcElement === this.renameInput_) { | |
2865 // Ignore keydown handler in the rename input box. | |
2866 return; | |
2867 } | |
2868 | |
2869 switch (util.getKeyModifiers(event) + event.keyCode) { | |
2870 case 'Ctrl-190': // Ctrl-. => Toggle filter files. | |
2871 this.fileFilter_.setFilterHidden( | |
2872 !this.fileFilter_.isFilterHiddenOn()); | |
2873 event.preventDefault(); | |
2874 return; | |
2875 | |
2876 case '27': // Escape => Cancel dialog. | |
2877 if (this.dialogType != DialogType.FULL_PAGE) { | |
2878 // If there is nothing else for ESC to do, then cancel the dialog. | |
2879 event.preventDefault(); | |
2880 this.cancelButton_.click(); | |
2881 } | |
2882 break; | |
2883 } | |
2884 }; | |
2885 | |
2886 /** | |
2887 * KeyUp event handler for the document. | |
2888 * @param {Event} event Key event. | |
2889 * @private | |
2890 */ | |
2891 FileManager.prototype.onKeyUp_ = function(event) { | |
2892 if (event.keyCode === 9) // Tab | |
2893 this.pressingTab_ = false; | |
2894 }; | |
2895 | |
2896 /** | |
2897 * KeyDown event handler for the div#list-container element. | |
2898 * @param {Event} event Key event. | |
2899 * @private | |
2900 */ | |
2901 FileManager.prototype.onListKeyDown_ = function(event) { | |
2902 if (event.srcElement.tagName == 'INPUT') { | |
2903 // Ignore keydown handler in the rename input box. | |
2904 return; | |
2905 } | |
2906 | |
2907 switch (util.getKeyModifiers(event) + event.keyCode) { | |
2908 case '8': // Backspace => Up one directory. | |
2909 event.preventDefault(); | |
2910 // TODO(mtomasz): Use Entry.getParent() instead. | |
2911 if (!this.getCurrentDirectoryEntry()) | |
2912 break; | |
2913 var currentEntry = this.getCurrentDirectoryEntry(); | |
2914 var locationInfo = this.volumeManager_.getLocationInfo(currentEntry); | |
2915 // TODO(mtomasz): There may be a tiny race in here. | |
2916 if (locationInfo && !locationInfo.isRootEntry && | |
2917 !locationInfo.isSpecialSearchRoot) { | |
2918 currentEntry.getParent(function(parentEntry) { | |
2919 this.directoryModel_.changeDirectoryEntry(parentEntry); | |
2920 }.bind(this), function() { /* Ignore errors. */}); | |
2921 } | |
2922 break; | |
2923 | |
2924 case '13': // Enter => Change directory or perform default action. | |
2925 // TODO(dgozman): move directory action to dispatchSelectionAction. | |
2926 var selection = this.getSelection(); | |
2927 if (selection.totalCount == 1 && | |
2928 selection.entries[0].isDirectory && | |
2929 !DialogType.isFolderDialog(this.dialogType)) { | |
2930 event.preventDefault(); | |
2931 this.onDirectoryAction_(selection.entries[0]); | |
2932 } else if (this.dispatchSelectionAction_()) { | |
2933 event.preventDefault(); | |
2934 } | |
2935 break; | |
2936 } | |
2937 | |
2938 switch (event.keyIdentifier) { | |
2939 case 'Home': | |
2940 case 'End': | |
2941 case 'Up': | |
2942 case 'Down': | |
2943 case 'Left': | |
2944 case 'Right': | |
2945 // When navigating with keyboard we hide the distracting mouse hover | |
2946 // highlighting until the user moves the mouse again. | |
2947 this.setNoHover_(true); | |
2948 break; | |
2949 } | |
2950 }; | |
2951 | |
2952 /** | |
2953 * Suppress/restore hover highlighting in the list container. | |
2954 * @param {boolean} on True to temporarity hide hover state. | |
2955 * @private | |
2956 */ | |
2957 FileManager.prototype.setNoHover_ = function(on) { | |
2958 if (on) { | |
2959 this.listContainer_.classList.add('nohover'); | |
2960 } else { | |
2961 this.listContainer_.classList.remove('nohover'); | |
2962 } | |
2963 }; | |
2964 | |
2965 /** | |
2966 * KeyPress event handler for the div#list-container element. | |
2967 * @param {Event} event Key event. | |
2968 * @private | |
2969 */ | |
2970 FileManager.prototype.onListKeyPress_ = function(event) { | |
2971 if (event.srcElement.tagName == 'INPUT') { | |
2972 // Ignore keypress handler in the rename input box. | |
2973 return; | |
2974 } | |
2975 | |
2976 if (event.ctrlKey || event.metaKey || event.altKey) | |
2977 return; | |
2978 | |
2979 var now = new Date(); | |
2980 var char = String.fromCharCode(event.charCode).toLowerCase(); | |
2981 var text = now - this.textSearchState_.date > 1000 ? '' : | |
2982 this.textSearchState_.text; | |
2983 this.textSearchState_ = {text: text + char, date: now}; | |
2984 | |
2985 this.doTextSearch_(); | |
2986 }; | |
2987 | |
2988 /** | |
2989 * Mousemove event handler for the div#list-container element. | |
2990 * @param {Event} event Mouse event. | |
2991 * @private | |
2992 */ | |
2993 FileManager.prototype.onListMouseMove_ = function(event) { | |
2994 // The user grabbed the mouse, restore the hover highlighting. | |
2995 this.setNoHover_(false); | |
2996 }; | |
2997 | |
2998 /** | |
2999 * Performs a 'text search' - selects a first list entry with name | |
3000 * starting with entered text (case-insensitive). | |
3001 * @private | |
3002 */ | |
3003 FileManager.prototype.doTextSearch_ = function() { | |
3004 var text = this.textSearchState_.text; | |
3005 if (!text) | |
3006 return; | |
3007 | |
3008 var dm = this.directoryModel_.getFileList(); | |
3009 for (var index = 0; index < dm.length; ++index) { | |
3010 var name = dm.item(index).name; | |
3011 if (name.substring(0, text.length).toLowerCase() == text) { | |
3012 this.currentList_.selectionModel.selectedIndexes = [index]; | |
3013 return; | |
3014 } | |
3015 } | |
3016 | |
3017 this.textSearchState_.text = ''; | |
3018 }; | |
3019 | |
3020 /** | |
3021 * Handle a click of the cancel button. Closes the window. | |
3022 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811 | |
3023 * | |
3024 * @param {Event} event The click event. | |
3025 * @private | |
3026 */ | |
3027 FileManager.prototype.onCancel_ = function(event) { | |
3028 chrome.fileBrowserPrivate.cancelDialog(); | |
3029 window.close(); | |
3030 }; | |
3031 | |
3032 /** | |
3033 * Resolves selected file urls returned from an Open dialog. | |
3034 * | |
3035 * For drive files this involves some special treatment. | |
3036 * Starts getting drive files if needed. | |
3037 * | |
3038 * @param {Array.<string>} fileUrls Drive URLs. | |
3039 * @param {function(Array.<string>)} callback To be called with fixed URLs. | |
3040 * @private | |
3041 */ | |
3042 FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) { | |
3043 if (this.isOnDrive()) { | |
3044 chrome.fileBrowserPrivate.getDriveFiles( | |
3045 fileUrls, | |
3046 function(localPaths) { | |
3047 callback(fileUrls); | |
3048 }); | |
3049 } else { | |
3050 callback(fileUrls); | |
3051 } | |
3052 }; | |
3053 | |
3054 /** | |
3055 * Closes this modal dialog with some files selected. | |
3056 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811 | |
3057 * @param {Object} selection Contains urls, filterIndex and multiple fields. | |
3058 * @private | |
3059 */ | |
3060 FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) { | |
3061 var self = this; | |
3062 function callback() { | |
3063 window.close(); | |
3064 } | |
3065 if (selection.multiple) { | |
3066 chrome.fileBrowserPrivate.selectFiles( | |
3067 selection.urls, this.params_.shouldReturnLocalPath, callback); | |
3068 } else { | |
3069 var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE); | |
3070 chrome.fileBrowserPrivate.selectFile( | |
3071 selection.urls[0], selection.filterIndex, forOpening, | |
3072 this.params_.shouldReturnLocalPath, callback); | |
3073 } | |
3074 }; | |
3075 | |
3076 /** | |
3077 * Tries to close this modal dialog with some files selected. | |
3078 * Performs preprocessing if needed (e.g. for Drive). | |
3079 * @param {Object} selection Contains urls, filterIndex and multiple fields. | |
3080 * @private | |
3081 */ | |
3082 FileManager.prototype.selectFilesAndClose_ = function(selection) { | |
3083 if (!this.isOnDrive() || | |
3084 this.dialogType == DialogType.SELECT_SAVEAS_FILE) { | |
3085 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0); | |
3086 return; | |
3087 } | |
3088 | |
3089 var shade = this.document_.createElement('div'); | |
3090 shade.className = 'shade'; | |
3091 var footer = this.dialogDom_.querySelector('.button-panel'); | |
3092 var progress = footer.querySelector('.progress-track'); | |
3093 progress.style.width = '0%'; | |
3094 var cancelled = false; | |
3095 | |
3096 var progressMap = {}; | |
3097 var filesStarted = 0; | |
3098 var filesTotal = selection.urls.length; | |
3099 for (var index = 0; index < selection.urls.length; index++) { | |
3100 progressMap[selection.urls[index]] = -1; | |
3101 } | |
3102 var lastPercent = 0; | |
3103 var bytesTotal = 0; | |
3104 var bytesDone = 0; | |
3105 | |
3106 var onFileTransfersUpdated = function(statusList) { | |
3107 for (var index = 0; index < statusList.length; index++) { | |
3108 var status = statusList[index]; | |
3109 var escaped = encodeURI(status.fileUrl); | |
3110 if (!(escaped in progressMap)) continue; | |
3111 if (status.total == -1) continue; | |
3112 | |
3113 var old = progressMap[escaped]; | |
3114 if (old == -1) { | |
3115 // -1 means we don't know file size yet. | |
3116 bytesTotal += status.total; | |
3117 filesStarted++; | |
3118 old = 0; | |
3119 } | |
3120 bytesDone += status.processed - old; | |
3121 progressMap[escaped] = status.processed; | |
3122 } | |
3123 | |
3124 var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal; | |
3125 // For files we don't have information about, assume the progress is zero. | |
3126 percent = percent * filesStarted / filesTotal * 100; | |
3127 // Do not decrease the progress. This may happen, if first downloaded | |
3128 // file is small, and the second one is large. | |
3129 lastPercent = Math.max(lastPercent, percent); | |
3130 progress.style.width = lastPercent + '%'; | |
3131 }.bind(this); | |
3132 | |
3133 var setup = function() { | |
3134 this.document_.querySelector('.dialog-container').appendChild(shade); | |
3135 setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100); | |
3136 footer.setAttribute('progress', 'progress'); | |
3137 this.cancelButton_.removeEventListener('click', this.onCancelBound_); | |
3138 this.cancelButton_.addEventListener('click', onCancel); | |
3139 chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener( | |
3140 onFileTransfersUpdated); | |
3141 }.bind(this); | |
3142 | |
3143 var cleanup = function() { | |
3144 shade.parentNode.removeChild(shade); | |
3145 footer.removeAttribute('progress'); | |
3146 this.cancelButton_.removeEventListener('click', onCancel); | |
3147 this.cancelButton_.addEventListener('click', this.onCancelBound_); | |
3148 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener( | |
3149 onFileTransfersUpdated); | |
3150 }.bind(this); | |
3151 | |
3152 var onCancel = function() { | |
3153 cancelled = true; | |
3154 // According to API cancel may fail, but there is no proper UI to reflect | |
3155 // this. So, we just silently assume that everything is cancelled. | |
3156 chrome.fileBrowserPrivate.cancelFileTransfers( | |
3157 selection.urls, function(response) {}); | |
3158 cleanup(); | |
3159 }.bind(this); | |
3160 | |
3161 var onResolved = function(resolvedUrls) { | |
3162 if (cancelled) return; | |
3163 cleanup(); | |
3164 selection.urls = resolvedUrls; | |
3165 // Call next method on a timeout, as it's unsafe to | |
3166 // close a window from a callback. | |
3167 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0); | |
3168 }.bind(this); | |
3169 | |
3170 var onProperties = function(properties) { | |
3171 for (var i = 0; i < properties.length; i++) { | |
3172 if (!properties[i] || properties[i].present) { | |
3173 // For files already in GCache, we don't get any transfer updates. | |
3174 filesTotal--; | |
3175 } | |
3176 } | |
3177 this.resolveSelectResults_(selection.urls, onResolved); | |
3178 }.bind(this); | |
3179 | |
3180 setup(); | |
3181 | |
3182 // TODO(mtomasz): Use Entry instead of URLs, if possible. | |
3183 util.URLsToEntries(selection.urls, function(entries) { | |
3184 this.metadataCache_.get(entries, 'drive', onProperties); | |
3185 }.bind(this)); | |
3186 }; | |
3187 | |
3188 /** | |
3189 * Handle a click of the ok button. | |
3190 * | |
3191 * The ok button has different UI labels depending on the type of dialog, but | |
3192 * in code it's always referred to as 'ok'. | |
3193 * | |
3194 * @param {Event} event The click event. | |
3195 * @private | |
3196 */ | |
3197 FileManager.prototype.onOk_ = function(event) { | |
3198 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) { | |
3199 // Save-as doesn't require a valid selection from the list, since | |
3200 // we're going to take the filename from the text input. | |
3201 var filename = this.filenameInput_.value; | |
3202 if (!filename) | |
3203 throw new Error('Missing filename!'); | |
3204 | |
3205 var directory = this.getCurrentDirectoryEntry(); | |
3206 this.validateFileName_(directory, filename, function(isValid) { | |
3207 if (!isValid) | |
3208 return; | |
3209 | |
3210 if (util.isFakeEntry(directory)) { | |
3211 // Can't save a file into a fake directory. | |
3212 return; | |
3213 } | |
3214 | |
3215 var selectFileAndClose = function() { | |
3216 // TODO(mtomasz): Clean this up by avoiding constructing a URL | |
3217 // via string concatenation. | |
3218 var currentDirUrl = directory.toURL(); | |
3219 if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/') | |
3220 currentDirUrl += '/'; | |
3221 this.selectFilesAndClose_({ | |
3222 urls: [currentDirUrl + encodeURIComponent(filename)], | |
3223 multiple: false, | |
3224 filterIndex: this.getSelectedFilterIndex_(filename) | |
3225 }); | |
3226 }.bind(this); | |
3227 | |
3228 directory.getFile( | |
3229 filename, {create: false}, | |
3230 function(entry) { | |
3231 // An existing file is found. Show confirmation dialog to | |
3232 // overwrite it. If the user select "OK" on the dialog, save it. | |
3233 this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename), | |
3234 selectFileAndClose); | |
3235 }.bind(this), | |
3236 function(error) { | |
3237 if (error.name == util.FileError.NOT_FOUND_ERR) { | |
3238 // The file does not exist, so it should be ok to create a | |
3239 // new file. | |
3240 selectFileAndClose(); | |
3241 return; | |
3242 } | |
3243 if (error.name == util.FileError.TYPE_MISMATCH_ERR) { | |
3244 // An directory is found. | |
3245 // Do not allow to overwrite directory. | |
3246 this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename)); | |
3247 return; | |
3248 } | |
3249 | |
3250 // Unexpected error. | |
3251 console.error('File save failed: ' + error.code); | |
3252 }.bind(this)); | |
3253 }.bind(this)); | |
3254 return; | |
3255 } | |
3256 | |
3257 var files = []; | |
3258 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes; | |
3259 | |
3260 if (DialogType.isFolderDialog(this.dialogType) && | |
3261 selectedIndexes.length == 0) { | |
3262 var url = this.getCurrentDirectoryEntry().toURL(); | |
3263 var singleSelection = { | |
3264 urls: [url], | |
3265 multiple: false, | |
3266 filterIndex: this.getSelectedFilterIndex_() | |
3267 }; | |
3268 this.selectFilesAndClose_(singleSelection); | |
3269 return; | |
3270 } | |
3271 | |
3272 // All other dialog types require at least one selected list item. | |
3273 // The logic to control whether or not the ok button is enabled should | |
3274 // prevent us from ever getting here, but we sanity check to be sure. | |
3275 if (!selectedIndexes.length) | |
3276 throw new Error('Nothing selected!'); | |
3277 | |
3278 var dm = this.directoryModel_.getFileList(); | |
3279 for (var i = 0; i < selectedIndexes.length; i++) { | |
3280 var entry = dm.item(selectedIndexes[i]); | |
3281 if (!entry) { | |
3282 console.error('Error locating selected file at index: ' + i); | |
3283 continue; | |
3284 } | |
3285 | |
3286 files.push(entry.toURL()); | |
3287 } | |
3288 | |
3289 // Multi-file selection has no other restrictions. | |
3290 if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) { | |
3291 var multipleSelection = { | |
3292 urls: files, | |
3293 multiple: true | |
3294 }; | |
3295 this.selectFilesAndClose_(multipleSelection); | |
3296 return; | |
3297 } | |
3298 | |
3299 // Everything else must have exactly one. | |
3300 if (files.length > 1) | |
3301 throw new Error('Too many files selected!'); | |
3302 | |
3303 var selectedEntry = dm.item(selectedIndexes[0]); | |
3304 | |
3305 if (DialogType.isFolderDialog(this.dialogType)) { | |
3306 if (!selectedEntry.isDirectory) | |
3307 throw new Error('Selected entry is not a folder!'); | |
3308 } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) { | |
3309 if (!selectedEntry.isFile) | |
3310 throw new Error('Selected entry is not a file!'); | |
3311 } | |
3312 | |
3313 var singleSelection = { | |
3314 urls: [files[0]], | |
3315 multiple: false, | |
3316 filterIndex: this.getSelectedFilterIndex_() | |
3317 }; | |
3318 this.selectFilesAndClose_(singleSelection); | |
3319 }; | |
3320 | |
3321 /** | |
3322 * Verifies the user entered name for file or folder to be created or | |
3323 * renamed to. Name restrictions must correspond to File API restrictions | |
3324 * (see DOMFilePath::isValidPath). Curernt WebKit implementation is | |
3325 * out of date (spec is | |
3326 * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to | |
3327 * be fixed. Shows message box if the name is invalid. | |
3328 * | |
3329 * It also verifies if the name length is in the limit of the filesystem. | |
3330 * | |
3331 * @param {DirectoryEntry} parentEntry The URL of the parent directory entry. | |
3332 * @param {string} name New file or folder name. | |
3333 * @param {function} onDone Function to invoke when user closes the | |
3334 * warning box or immediatelly if file name is correct. If the name was | |
3335 * valid it is passed true, and false otherwise. | |
3336 * @private | |
3337 */ | |
3338 FileManager.prototype.validateFileName_ = function( | |
3339 parentEntry, name, onDone) { | |
3340 var msg; | |
3341 var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name); | |
3342 if (testResult) { | |
3343 msg = strf('ERROR_INVALID_CHARACTER', testResult[0]); | |
3344 } else if (/^\s*$/i.test(name)) { | |
3345 msg = str('ERROR_WHITESPACE_NAME'); | |
3346 } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) { | |
3347 msg = str('ERROR_RESERVED_NAME'); | |
3348 } else if (this.fileFilter_.isFilterHiddenOn() && name[0] == '.') { | |
3349 msg = str('ERROR_HIDDEN_NAME'); | |
3350 } | |
3351 | |
3352 if (msg) { | |
3353 this.alert.show(msg, function() { | |
3354 onDone(false); | |
3355 }); | |
3356 return; | |
3357 } | |
3358 | |
3359 var self = this; | |
3360 chrome.fileBrowserPrivate.validatePathNameLength( | |
3361 parentEntry.toURL(), name, function(valid) { | |
3362 if (!valid) { | |
3363 self.alert.show(str('ERROR_LONG_NAME'), | |
3364 function() { onDone(false); }); | |
3365 } else { | |
3366 onDone(true); | |
3367 } | |
3368 }); | |
3369 }; | |
3370 | |
3371 /** | |
3372 * Handler invoked on preference setting in drive context menu. | |
3373 * | |
3374 * @param {string} pref The preference to alter. | |
3375 * @param {boolean} inverted Invert the value if true. | |
3376 * @param {Event} event The click event. | |
3377 * @private | |
3378 */ | |
3379 FileManager.prototype.onDrivePrefClick_ = function(pref, inverted, event) { | |
3380 var newValue = !event.target.hasAttribute('checked'); | |
3381 if (newValue) | |
3382 event.target.setAttribute('checked', 'checked'); | |
3383 else | |
3384 event.target.removeAttribute('checked'); | |
3385 | |
3386 var changeInfo = {}; | |
3387 changeInfo[pref] = inverted ? !newValue : newValue; | |
3388 chrome.fileBrowserPrivate.setPreferences(changeInfo); | |
3389 }; | |
3390 | |
3391 /** | |
3392 * Invoked when the search box is changed. | |
3393 * | |
3394 * @param {Event} event The changed event. | |
3395 * @private | |
3396 */ | |
3397 FileManager.prototype.onSearchBoxUpdate_ = function(event) { | |
3398 var searchString = this.searchBox_.value; | |
3399 | |
3400 if (this.isOnDrive()) { | |
3401 // When the search text is changed, finishes the search and showes back | |
3402 // the last directory by passing an empty string to | |
3403 // {@code DirectoryModel.search()}. | |
3404 if (this.directoryModel_.isSearching() && | |
3405 this.lastSearchQuery_ != searchString) { | |
3406 this.doSearch(''); | |
3407 } | |
3408 | |
3409 // On drive, incremental search is not invoked since we have an auto- | |
3410 // complete suggestion instead. | |
3411 return; | |
3412 } | |
3413 | |
3414 this.search_(searchString); | |
3415 }; | |
3416 | |
3417 /** | |
3418 * Handle the search clear button click. | |
3419 * @private | |
3420 */ | |
3421 FileManager.prototype.onSearchClearButtonClick_ = function() { | |
3422 this.ui_.searchBox.clear(); | |
3423 this.onSearchBoxUpdate_(); | |
3424 }; | |
3425 | |
3426 /** | |
3427 * Search files and update the list with the search result. | |
3428 * | |
3429 * @param {string} searchString String to be searched with. | |
3430 * @private | |
3431 */ | |
3432 FileManager.prototype.search_ = function(searchString) { | |
3433 var noResultsDiv = this.document_.getElementById('no-search-results'); | |
3434 | |
3435 var reportEmptySearchResults = function() { | |
3436 if (this.directoryModel_.getFileList().length === 0) { | |
3437 // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags, | |
3438 // hence we escapes |searchString| here. | |
3439 var html = strf('SEARCH_NO_MATCHING_FILES_HTML', | |
3440 util.htmlEscape(searchString)); | |
3441 noResultsDiv.innerHTML = html; | |
3442 noResultsDiv.setAttribute('show', 'true'); | |
3443 } else { | |
3444 noResultsDiv.removeAttribute('show'); | |
3445 } | |
3446 }; | |
3447 | |
3448 var hideNoResultsDiv = function() { | |
3449 noResultsDiv.removeAttribute('show'); | |
3450 }; | |
3451 | |
3452 this.doSearch(searchString, | |
3453 reportEmptySearchResults.bind(this), | |
3454 hideNoResultsDiv.bind(this)); | |
3455 }; | |
3456 | |
3457 /** | |
3458 * Performs search and displays results. | |
3459 * | |
3460 * @param {string} query Query that will be searched for. | |
3461 * @param {function()=} opt_onSearchRescan Function that will be called when | |
3462 * the search directory is rescanned (i.e. search results are displayed). | |
3463 * @param {function()=} opt_onClearSearch Function to be called when search | |
3464 * state gets cleared. | |
3465 */ | |
3466 FileManager.prototype.doSearch = function( | |
3467 searchString, opt_onSearchRescan, opt_onClearSearch) { | |
3468 var onSearchRescan = opt_onSearchRescan || function() {}; | |
3469 var onClearSearch = opt_onClearSearch || function() {}; | |
3470 | |
3471 this.lastSearchQuery_ = searchString; | |
3472 this.directoryModel_.search(searchString, onSearchRescan, onClearSearch); | |
3473 }; | |
3474 | |
3475 /** | |
3476 * Requests autocomplete suggestions for files on Drive. | |
3477 * Once the suggestions are returned, the autocomplete popup will show up. | |
3478 * | |
3479 * @param {string} query The text to autocomplete from. | |
3480 * @private | |
3481 */ | |
3482 FileManager.prototype.requestAutocompleteSuggestions_ = function(query) { | |
3483 query = query.trimLeft(); | |
3484 | |
3485 // Only Drive supports auto-compelete | |
3486 if (!this.isOnDrive()) | |
3487 return; | |
3488 | |
3489 // Remember the most recent query. If there is an other request in progress, | |
3490 // then it's result will be discarded and it will call a new request for | |
3491 // this query. | |
3492 this.lastAutocompleteQuery_ = query; | |
3493 if (this.autocompleteSuggestionsBusy_) | |
3494 return; | |
3495 | |
3496 // The autocomplete list should be resized and repositioned here as the | |
3497 // search box is resized when it's focused. | |
3498 this.autocompleteList_.syncWidthAndPositionToInput(); | |
3499 | |
3500 if (!query) { | |
3501 this.autocompleteList_.suggestions = []; | |
3502 return; | |
3503 } | |
3504 | |
3505 var headerItem = {isHeaderItem: true, searchQuery: query}; | |
3506 if (!this.autocompleteList_.dataModel || | |
3507 this.autocompleteList_.dataModel.length == 0) | |
3508 this.autocompleteList_.suggestions = [headerItem]; | |
3509 else | |
3510 // Updates only the head item to prevent a flickering on typing. | |
3511 this.autocompleteList_.dataModel.splice(0, 1, headerItem); | |
3512 | |
3513 this.autocompleteSuggestionsBusy_ = true; | |
3514 | |
3515 var searchParams = { | |
3516 'query': query, | |
3517 'types': 'ALL', | |
3518 'maxResults': 4 | |
3519 }; | |
3520 chrome.fileBrowserPrivate.searchDriveMetadata( | |
3521 searchParams, | |
3522 function(suggestions) { | |
3523 this.autocompleteSuggestionsBusy_ = false; | |
3524 | |
3525 // Discard results for previous requests and fire a new search | |
3526 // for the most recent query. | |
3527 if (query != this.lastAutocompleteQuery_) { | |
3528 this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_); | |
3529 return; | |
3530 } | |
3531 | |
3532 // Keeps the items in the suggestion list. | |
3533 this.autocompleteList_.suggestions = [headerItem].concat(suggestions); | |
3534 }.bind(this)); | |
3535 }; | |
3536 | |
3537 /** | |
3538 * Opens the currently selected suggestion item. | |
3539 * @private | |
3540 */ | |
3541 FileManager.prototype.openAutocompleteSuggestion_ = function() { | |
3542 var selectedItem = this.autocompleteList_.selectedItem; | |
3543 | |
3544 // If the entry is the search item or no entry is selected, just change to | |
3545 // the search result. | |
3546 if (!selectedItem || selectedItem.isHeaderItem) { | |
3547 var query = selectedItem ? | |
3548 selectedItem.searchQuery : this.searchBox_.value; | |
3549 this.search_(query); | |
3550 return; | |
3551 } | |
3552 | |
3553 var entry = selectedItem.entry; | |
3554 // If the entry is a directory, just change the directory. | |
3555 if (entry.isDirectory) { | |
3556 this.onDirectoryAction_(entry); | |
3557 return; | |
3558 } | |
3559 | |
3560 var entries = [entry]; | |
3561 var self = this; | |
3562 | |
3563 // To open a file, first get the mime type. | |
3564 this.metadataCache_.get(entries, 'drive', function(props) { | |
3565 var mimeType = props[0].contentMimeType || ''; | |
3566 var mimeTypes = [mimeType]; | |
3567 var openIt = function() { | |
3568 if (self.dialogType == DialogType.FULL_PAGE) { | |
3569 var tasks = new FileTasks(self); | |
3570 tasks.init(entries, mimeTypes); | |
3571 tasks.executeDefault(); | |
3572 } else { | |
3573 self.onOk_(); | |
3574 } | |
3575 }; | |
3576 | |
3577 // Change the current directory to the directory that contains the | |
3578 // selected file. Note that this is necessary for an image or a video, | |
3579 // which should be opened in the gallery mode, as the gallery mode | |
3580 // requires the entry to be in the current directory model. For | |
3581 // consistency, the current directory is always changed regardless of | |
3582 // the file type. | |
3583 entry.getParent(function(parentEntry) { | |
3584 var onDirectoryChanged = function(event) { | |
3585 self.directoryModel_.removeEventListener('scan-completed', | |
3586 onDirectoryChanged); | |
3587 self.directoryModel_.selectEntry(entry); | |
3588 openIt(); | |
3589 }; | |
3590 // changeDirectoryEntry() returns immediately. We should wait until the | |
3591 // directory scan is complete. | |
3592 self.directoryModel_.addEventListener('scan-completed', | |
3593 onDirectoryChanged); | |
3594 self.directoryModel_.changeDirectoryEntry( | |
3595 parentEntry, | |
3596 function() { | |
3597 // Remove the listner if the change directory failed. | |
3598 self.directoryModel_.removeEventListener('scan-completed', | |
3599 onDirectoryChanged); | |
3600 }); | |
3601 }); | |
3602 }); | |
3603 }; | |
3604 | |
3605 FileManager.prototype.decorateSplitter = function(splitterElement) { | |
3606 var self = this; | |
3607 | |
3608 var Splitter = cr.ui.Splitter; | |
3609 | |
3610 var customSplitter = cr.ui.define('div'); | |
3611 | |
3612 customSplitter.prototype = { | |
3613 __proto__: Splitter.prototype, | |
3614 | |
3615 handleSplitterDragStart: function(e) { | |
3616 Splitter.prototype.handleSplitterDragStart.apply(this, arguments); | |
3617 this.ownerDocument.documentElement.classList.add('col-resize'); | |
3618 }, | |
3619 | |
3620 handleSplitterDragMove: function(deltaX) { | |
3621 Splitter.prototype.handleSplitterDragMove.apply(this, arguments); | |
3622 self.onResize_(); | |
3623 }, | |
3624 | |
3625 handleSplitterDragEnd: function(e) { | |
3626 Splitter.prototype.handleSplitterDragEnd.apply(this, arguments); | |
3627 this.ownerDocument.documentElement.classList.remove('col-resize'); | |
3628 } | |
3629 }; | |
3630 | |
3631 customSplitter.decorate(splitterElement); | |
3632 }; | |
3633 | |
3634 /** | |
3635 * Updates default action menu item to match passed taskItem (icon, | |
3636 * label and action). | |
3637 * | |
3638 * @param {Object} defaultItem - taskItem to match. | |
3639 * @param {boolean} isMultiple - if multiple tasks available. | |
3640 */ | |
3641 FileManager.prototype.updateContextMenuActionItems = function(defaultItem, | |
3642 isMultiple) { | |
3643 if (defaultItem) { | |
3644 if (defaultItem.iconType) { | |
3645 this.defaultActionMenuItem_.style.backgroundImage = ''; | |
3646 this.defaultActionMenuItem_.setAttribute('file-type-icon', | |
3647 defaultItem.iconType); | |
3648 } else if (defaultItem.iconUrl) { | |
3649 this.defaultActionMenuItem_.style.backgroundImage = | |
3650 'url(' + defaultItem.iconUrl + ')'; | |
3651 } else { | |
3652 this.defaultActionMenuItem_.style.backgroundImage = ''; | |
3653 } | |
3654 | |
3655 this.defaultActionMenuItem_.label = defaultItem.title; | |
3656 this.defaultActionMenuItem_.disabled = !!defaultItem.disabled; | |
3657 this.defaultActionMenuItem_.taskId = defaultItem.taskId; | |
3658 } | |
3659 | |
3660 var defaultActionSeparator = | |
3661 this.dialogDom_.querySelector('#default-action-separator'); | |
3662 | |
3663 this.openWithCommand_.canExecuteChange(); | |
3664 this.openWithCommand_.setHidden(!(defaultItem && isMultiple)); | |
3665 this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled; | |
3666 | |
3667 this.defaultActionMenuItem_.hidden = !defaultItem; | |
3668 defaultActionSeparator.hidden = !defaultItem; | |
3669 }; | |
3670 | |
3671 /** | |
3672 * @return {FileSelection} Selection object. | |
3673 */ | |
3674 FileManager.prototype.getSelection = function() { | |
3675 return this.selectionHandler_.selection; | |
3676 }; | |
3677 | |
3678 /** | |
3679 * @return {ArrayDataModel} File list. | |
3680 */ | |
3681 FileManager.prototype.getFileList = function() { | |
3682 return this.directoryModel_.getFileList(); | |
3683 }; | |
3684 | |
3685 /** | |
3686 * @return {cr.ui.List} Current list object. | |
3687 */ | |
3688 FileManager.prototype.getCurrentList = function() { | |
3689 return this.currentList_; | |
3690 }; | |
3691 | |
3692 /** | |
3693 * Retrieve the preferences of the files.app. This method caches the result | |
3694 * and returns it unless opt_update is true. | |
3695 * @param {function(Object.<string, *>)} callback Callback to get the | |
3696 * preference. | |
3697 * @param {boolean=} opt_update If is's true, don't use the cache and | |
3698 * retrieve latest preference. Default is false. | |
3699 * @private | |
3700 */ | |
3701 FileManager.prototype.getPreferences_ = function(callback, opt_update) { | |
3702 if (!opt_update && this.preferences_ !== undefined) { | |
3703 callback(this.preferences_); | |
3704 return; | |
3705 } | |
3706 | |
3707 chrome.fileBrowserPrivate.getPreferences(function(prefs) { | |
3708 this.preferences_ = prefs; | |
3709 callback(prefs); | |
3710 }.bind(this)); | |
3711 }; | |
3712 })(); | |
OLD | NEW |