blob: 657a7f2ca1328135b2e82bc63622099f505cba66 [file] [log] [blame]
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001// 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
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00005'use strict';
6
7/**
8 * This variable is checked in SelectFileDialogExtensionBrowserTest.
9 * @type {number}
10 */
11window.JSErrorCount = 0;
12
13/**
14 * Count uncaught exceptions.
15 */
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010016window.onerror = function() { window.JSErrorCount++; };
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000017
Torne (Richard Coles)58218062012-11-14 11:43:16 +000018/**
19 * FileManager constructor.
20 *
21 * FileManager objects encapsulate the functionality of the file selector
22 * dialogs, as well as the full screen file manager application (though the
23 * latter is not yet implemented).
24 *
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000025 * @constructor
Torne (Richard Coles)58218062012-11-14 11:43:16 +000026 */
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +010027function FileManager() {
28 this.initializeQueue_ = new AsyncUtil.Group();
Torne (Richard Coles)58218062012-11-14 11:43:16 +000029}
30
31/**
32 * Maximum delay in milliseconds for updating thumbnails in the bottom panel
33 * to mitigate flickering. If images load faster then the delay they replace
34 * old images smoothly. On the other hand we don't want to keep old images
35 * too long.
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +010036 *
37 * @type {number}
38 * @const
Torne (Richard Coles)58218062012-11-14 11:43:16 +000039 */
40FileManager.THUMBNAIL_SHOW_DELAY = 100;
41
42FileManager.prototype = {
43 __proto__: cr.EventTarget.prototype
44};
45
46/**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000047 * Unload the file manager.
48 * Used by background.js (when running in the packaged mode).
49 */
50function unload() {
51 fileManager.onBeforeUnload_();
52 fileManager.onUnload_();
53}
54
55/**
Torne (Richard Coles)58218062012-11-14 11:43:16 +000056 * List of dialog types.
57 *
58 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
59 * FULL_PAGE which is specific to this code.
60 *
61 * @enum {string}
62 */
63var DialogType = {
64 SELECT_FOLDER: 'folder',
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010065 SELECT_UPLOAD_FOLDER: 'upload-folder',
Torne (Richard Coles)58218062012-11-14 11:43:16 +000066 SELECT_SAVEAS_FILE: 'saveas-file',
67 SELECT_OPEN_FILE: 'open-file',
68 SELECT_OPEN_MULTI_FILE: 'open-multi-file',
69 FULL_PAGE: 'full-page'
70};
71
72/**
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +010073 * TextMeasure constructor.
74 *
75 * TextMeasure is a measure for text that returns the width of text. This
76 * class has a dummy span element. When measuring the width of text, it sets
77 * the text to the element and obtains the element's size by
78 * getBoundingClientRect.
79 *
80 * @constructor
81 * @param {HTMLElement} element Element that has styles of measured text. The
82 * width of text is mesures like as it is rendered in this element.
83 */
84var TextMeasure = function(element) {
85 var doc = element.ownerDocument;
86 this.dummySpan_ = doc.createElement('span');
87 this.dummySpan_ = doc.getElementsByTagName('body')[0].
88 appendChild(this.dummySpan_);
89 this.dummySpan_.style.position = 'absolute';
90 this.dummySpan_.style.visibility = 'hidden';
91 var styles = window.getComputedStyle(element, '');
92 var stylesToBeCopied = [
93 'fontSize',
94 'fontStyle',
95 'fontWeight',
96 'fontFamily',
97 'letterSpacing'
98 ];
99 for (var i = 0; i < stylesToBeCopied.length; i++) {
100 this.dummySpan_.style[stylesToBeCopied[i]] = styles[stylesToBeCopied[i]];
101 }
102};
103
104/**
105 * Measures the widht of text.
106 *
107 * @param {string} text Text that is measured the width.
108 * @return {number} Width of the specified text.
109 */
110TextMeasure.prototype.getWidth = function(text) {
111 this.dummySpan_.innerText = text;
112 var rect = this.dummySpan_.getBoundingClientRect();
113 return rect ? rect.width : 0;
114};
115
116/**
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000117 * @param {string} type Dialog type.
118 * @return {boolean} Whether the type is modal.
119 */
120DialogType.isModal = function(type) {
121 return type == DialogType.SELECT_FOLDER ||
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100122 type == DialogType.SELECT_UPLOAD_FOLDER ||
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000123 type == DialogType.SELECT_SAVEAS_FILE ||
124 type == DialogType.SELECT_OPEN_FILE ||
125 type == DialogType.SELECT_OPEN_MULTI_FILE;
126};
127
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100128/**
129 * Bottom magrin of the list and tree for transparent preview panel.
130 * @const
131 */
132var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52;
133
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000134// Anonymous "namespace".
135(function() {
136
137 // Private variables and helper functions.
138
139 /**
140 * Location of the page to buy more storage for Google Drive.
141 */
142 FileManager.GOOGLE_DRIVE_BUY_STORAGE =
143 'https://www.google.com/settings/storage';
144
145 /**
146 * Location of Google Drive specific help.
147 */
148 FileManager.GOOGLE_DRIVE_HELP =
149 'https://support.google.com/chromeos/?p=filemanager_drivehelp';
150
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000151 /**
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000152 * Location of Google Drive specific help.
153 */
154 FileManager.GOOGLE_DRIVE_ROOT = 'https://drive.google.com';
155
156 /**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000157 * Location of Files App specific help.
158 */
159 FileManager.FILES_APP_HELP =
160 'https://support.google.com/chromeos/?p=gsg_files_app';
161
162 /**
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000163 * Number of milliseconds in a day.
164 */
165 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
166
167 /**
168 * Some UI elements react on a single click and standard double click handling
169 * leads to confusing results. We ignore a second click if it comes soon
170 * after the first.
171 */
172 var DOUBLE_CLICK_TIMEOUT = 200;
173
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000174 var removeChildren = function(element) {
175 element.textContent = '';
176 };
177
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000178 /**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000179 * Update the elemenst to display the information about remainig space for
180 * the storage.
181 * @param {!Element} spaceInnerBar Block element for a percentage bar
182 * representing the remaining space.
183 * @param {!Element} spaceInfoLabel Inline element to contain the message.
184 * @param {!Element} spaceOuterBar Block element around the percentage bar.
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000185 */
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000186 var updateSpaceInfo = function(
187 sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) {
188 spaceInnerBar.removeAttribute('pending');
189 if (sizeStatsResult) {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100190 var sizeStr = util.bytesToString(sizeStatsResult.remainingSize);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000191 spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000192
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000193 var usedSpace =
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100194 sizeStatsResult.totalSize - sizeStatsResult.remainingSize;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000195 spaceInnerBar.style.width =
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100196 (100 * usedSpace / sizeStatsResult.totalSize) + '%';
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000197
Ben Murdocheb525c52013-07-10 11:40:50 +0100198 spaceOuterBar.hidden = false;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000199 } else {
Ben Murdocheb525c52013-07-10 11:40:50 +0100200 spaceOuterBar.hidden = true;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000201 spaceInfoLabel.textContent = str('FAILED_SPACE_INFO');
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000202 }
203 };
204
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000205 // Public statics.
206
207 FileManager.ListType = {
208 DETAIL: 'detail',
209 THUMBNAIL: 'thumb'
210 };
211
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100212 FileManager.prototype.initPreferences_ = function(callback) {
213 var group = new AsyncUtil.Group();
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000214
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000215 // DRIVE preferences should be initialized before creating DirectoryModel
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100216 // to rebuild the roots list.
217 group.add(this.getPreferences_.bind(this));
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000218
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100219 // Get startup preferences.
220 this.viewOptions_ = {};
221 group.add(function(done) {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100222 this.dialogType = this.params_.type || DialogType.FULL_PAGE;
223 this.startupPrefName_ = 'file-manager-' + this.dialogType;
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100224 util.platform.getPreference(this.startupPrefName_, function(value) {
225 // Load the global default options.
226 try {
227 this.viewOptions_ = JSON.parse(value);
228 } catch (ignore) {}
229 // Override with window-specific options.
230 if (window.appState && window.appState.viewOptions) {
231 for (var key in window.appState.viewOptions) {
232 if (window.appState.viewOptions.hasOwnProperty(key))
233 this.viewOptions_[key] = window.appState.viewOptions[key];
234 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000235 }
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100236 done();
237 }.bind(this));
238 }.bind(this));
239
240 // Get the command line option.
241 group.add(function(done) {
242 chrome.commandLinePrivate.hasSwitch(
Ben Murdocheb525c52013-07-10 11:40:50 +0100243 'file-manager-show-checkboxes', function(flag) {
244 this.showCheckboxes_ = flag;
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100245 done();
246 }.bind(this));
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000247 }.bind(this));
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000248
Ben Murdoch32409262013-08-07 11:04:47 +0100249 // Removes the user data which is no longer used.
250 // TODO(yoshiki): Remove this in M31 http://crbug.com/268784/
251 chrome.storage.local.remove('folder-shortcuts-list');
252
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100253 group.run(callback);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000254 };
255
256 /**
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100257 * Request local file system, resolve roots and init_ after that.
258 * Warning, you can't use DOM nor any external scripts here, since it may not
259 * be loaded yet. Functions in util.* and metrics.* are available and can
260 * be used.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000261 *
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100262 * @param {function()} callback Completion callback.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000263 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000264 */
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100265 FileManager.prototype.initFileSystem_ = function(callback) {
266 util.installFileErrorToString();
267
268 metrics.startInterval('Load.FileSystem');
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100269 chrome.fileBrowserPrivate.requestFileSystem(function(filesystem) {
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100270 metrics.recordInterval('Load.FileSystem');
271 this.filesystem_ = filesystem;
272 callback();
273 }.bind(this));
274
275 // Mount Drive if enabled.
276 if (this.isDriveEnabled())
277 this.volumeManager_.mountDrive(function() {}, function() {});
278 };
279
280 /**
281 * One time initialization for the file system and related things.
282 *
283 * @param {function()} callback Completion callback.
284 * @private
285 */
286 FileManager.prototype.initFileSystemUI_ = function(callback) {
287 this.table_.startBatchUpdates();
288 this.grid_.startBatchUpdates();
289
290 this.initFileList_();
291 this.setupCurrentDirectory_(true /* page loading */);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000292
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000293 // PyAuto tests monitor this state by polling this variable
294 this.__defineGetter__('workerInitialized_', function() {
295 return this.metadataCache_.isInitialized();
296 }.bind(this));
297
298 this.initDateTimeFormatters_();
299
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000300 var self = this;
301
302 // Get the 'allowRedeemOffers' preference before launching
303 // FileListBannerController.
304 this.getPreferences_(function(pref) {
305 /** @type {boolean} */
306 var showOffers = pref['allowRedeemOffers'];
307 self.bannersController_ = new FileListBannerController(
308 self.directoryModel_, self.volumeManager_, self.document_,
309 showOffers);
310 self.bannersController_.addEventListener('relayout',
311 self.onResize_.bind(self));
312 });
313
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000314 var dm = this.directoryModel_;
315 dm.addEventListener('directory-changed',
316 this.onDirectoryChanged_.bind(this));
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000317 dm.addEventListener('begin-update-files', function() {
318 self.currentList_.startBatchUpdates();
319 });
320 dm.addEventListener('end-update-files', function() {
321 self.restoreItemBeingRenamed_();
322 self.currentList_.endBatchUpdates();
323 });
324 dm.addEventListener('scan-started', this.onScanStarted_.bind(this));
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100325 dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this));
326 dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this));
327 dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this));
328 dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this));
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000329 dm.addEventListener('rescan-completed',
330 this.refreshCurrentDirectoryMetadata_.bind(this));
331
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100332 /**
333 * If |item| in |parentView| is behind the preview panel, scrolls up the
334 * parent view and make the item visible.
335 *
336 * @param {HTMLElement} item Item to be visible in the parent.
337 * @param {HTMLElement} parentView View contains |selectedItem|.
338 */
339 var ensureItemNotBehindPreviewPanel = function(item, parentView) {
340 var itemRect = item.getBoundingClientRect();
341 if (!itemRect)
342 return;
343 var itemBottom = itemRect.bottom;
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100344
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100345 var previewPanel = this.dialogDom_.querySelector('.preview-panel');
346 var previewPanelRects = previewPanel.getBoundingClientRect();
347 var panelHeight = previewPanelRects ? previewPanelRects.height : 0;
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100348
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100349 var listRect = parentView.getBoundingClientRect();
350 if (!listRect)
351 return;
352 var listBottom = listRect.bottom - panelHeight;
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100353
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100354 if (itemBottom > listBottom) {
355 var scrollOffset = itemBottom - listBottom;
356 parentView.scrollTop += scrollOffset;
357 }
358 }.bind(this);
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100359
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100360 var sm = this.directoryModel_.getFileListSelection();
361 sm.addEventListener('change', function() {
362 if (sm.selectedIndexes.length != 1)
363 return;
364 var view = (this.listType_ == FileManager.ListType.DETAIL) ?
365 this.table_.list : this.grid_;
366 var selectedItem = view.getListItemByIndex(sm.selectedIndex);
367 if (!selectedItem)
368 return;
369 ensureItemNotBehindPreviewPanel(selectedItem, view);
370 }.bind(this));
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100371
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100372 this.directoryTree_.addEventListener('change', function() {
373 var selectedSubTree = this.directoryTree_.selectedItem;
374 if (!selectedSubTree)
375 return;
376 var selectedItem = selectedSubTree.rowElement;
377 ensureItemNotBehindPreviewPanel(selectedItem, this.directoryTree_);
378 }.bind(this));
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000379
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000380 var stateChangeHandler =
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000381 this.onPreferencesChanged_.bind(this);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000382 chrome.fileBrowserPrivate.onPreferencesChanged.addListener(
383 stateChangeHandler);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000384 stateChangeHandler();
385
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100386 var driveConnectionChangedHandler =
387 this.onDriveConnectionChanged_.bind(this);
388 this.volumeManager_.addEventListener('drive-connection-changed',
389 driveConnectionChangedHandler);
390 driveConnectionChangedHandler();
391
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100392 // Set the initial focus and set it as a fallback.
393 this.document_.addEventListener('focusout', function(e) {
394 if (!e.relatedTarget)
Ben Murdoch32409262013-08-07 11:04:47 +0100395 setTimeout(this.refocus.bind(this), 0);
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100396 }.bind(this));
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000397 this.refocus();
398
399 this.initDataTransferOperations_();
400
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000401 this.initContextMenus_();
402 this.initCommands_();
403
404 this.updateFileTypeFilter_();
405
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000406 this.selectionHandler_.onFileSelectionChanged();
407
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000408 this.table_.endBatchUpdates();
409 this.grid_.endBatchUpdates();
410
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100411 callback();
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000412 };
413
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000414 /**
415 * @private
416 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000417 FileManager.prototype.initDateTimeFormatters_ = function() {
418 var use12hourClock = !this.preferences_['use24hourClock'];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000419 this.table_.setDateTimeFormat(use12hourClock);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000420 };
421
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000422 /**
423 * @private
424 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000425 FileManager.prototype.initDataTransferOperations_ = function() {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100426 this.copyManager_ = new FileCopyManagerWrapper.getInstance();
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000427
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100428 this.butterBar_ = new ButterBar(this.dialogDom_, this.copyManager_);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000429
430 // CopyManager and ButterBar are required for 'Delete' operation in
431 // Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
432 if (this.dialogType != DialogType.FULL_PAGE) return;
433
Ben Murdochd3868032013-07-31 10:55:33 +0100434 // TODO(hidehiko): Extract FileCopyManager related code from FileManager
435 // to simplify it.
436 this.onCopyProgressBound_ = this.onCopyProgress_.bind(this);
437 this.copyManager_.addEventListener(
438 'copy-progress', this.onCopyProgressBound_);
439
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100440 this.onCopyManagerEntryChangedBound_ =
441 this.onCopyManagerEntryChanged_.bind(this);
Ben Murdochd3868032013-07-31 10:55:33 +0100442 this.copyManager_.addEventListener(
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100443 'entry-changed', this.onCopyManagerEntryChangedBound_);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000444
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000445 var controller = this.fileTransferController_ =
446 new FileTransferController(this.document_,
447 this.copyManager_,
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100448 this.metadataCache_,
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000449 this.directoryModel_);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000450 controller.attachDragSource(this.table_.list);
Ben Murdoch558790d2013-07-30 15:19:42 +0100451 controller.attachFileListDropTarget(this.table_.list);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000452 controller.attachDragSource(this.grid_);
Ben Murdoch558790d2013-07-30 15:19:42 +0100453 controller.attachFileListDropTarget(this.grid_);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000454 controller.attachTreeDropTarget(this.directoryTree_);
Ben Murdoch32409262013-08-07 11:04:47 +0100455 controller.attachNavigationListDropTarget(this.navigationList_, true);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000456 controller.attachCopyPasteHandlers();
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000457 controller.addEventListener('selection-copied',
458 this.blinkSelection.bind(this));
459 controller.addEventListener('selection-cut',
460 this.blinkSelection.bind(this));
461 };
462
463 /**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000464 * One-time initialization of context menus.
465 * @private
466 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000467 FileManager.prototype.initContextMenus_ = function() {
468 this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu');
469 cr.ui.Menu.decorate(this.fileContextMenu_);
470
471 cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_);
472 cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'),
473 this.fileContextMenu_);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000474 cr.ui.contextMenuHandler.setContextMenu(
475 this.document_.querySelector('.drive-welcome.page'),
476 this.fileContextMenu_);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000477
478 this.rootsContextMenu_ =
479 this.dialogDom_.querySelector('#roots-context-menu');
480 cr.ui.Menu.decorate(this.rootsContextMenu_);
Ben Murdoch32409262013-08-07 11:04:47 +0100481 this.navigationList_.setContextMenu(this.rootsContextMenu_);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000482
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100483 this.directoryTreeContextMenu_ =
484 this.dialogDom_.querySelector('#directory-tree-context-menu');
485 cr.ui.Menu.decorate(this.directoryTreeContextMenu_);
486 this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_;
487
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000488 this.textContextMenu_ =
489 this.dialogDom_.querySelector('#text-context-menu');
490 cr.ui.Menu.decorate(this.textContextMenu_);
491
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000492 this.gearButton_ = this.dialogDom_.querySelector('#gear-button');
493 this.gearButton_.addEventListener('menushow',
494 this.refreshRemainingSpace_.bind(this,
495 false /* Without loading caption. */));
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100496 this.dialogDom_.querySelector('#gear-menu').menuItemSelector =
497 'menuitem, hr';
Ben Murdochbb1529c2013-08-08 10:24:53 +0100498 cr.ui.decorate(this.gearButton_, cr.ui.MenuButton);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100499
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100500 if (this.dialogType == DialogType.FULL_PAGE) {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100501 var maximizeButton = this.dialogDom_.querySelector('#maximize-button');
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +0100502 maximizeButton.addEventListener('click', this.onMaximize.bind(this));
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100503
504 var closeButton = this.dialogDom_.querySelector('#close-button');
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +0100505 closeButton.addEventListener('click', this.onClose.bind(this));
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100506 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000507
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000508 this.syncButton.checkable = true;
509 this.hostedButton.checkable = true;
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100510 this.detailViewButton_.checkable = true;
511 this.thumbnailViewButton_.checkable = true;
Ben Murdocheb525c52013-07-10 11:40:50 +0100512
513 if (util.platform.runningInBrowser()) {
514 // Supresses the default context menu.
515 this.dialogDom_.addEventListener('contextmenu', function(e) {
516 e.preventDefault();
517 e.stopPropagation();
518 });
519 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000520 };
521
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +0100522 FileManager.prototype.onMaximize = function() {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100523 // Do not maximize when running via chrome://files in a browser.
524 if (util.platform.runningInBrowser())
525 return;
526
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +0100527 var appWindow = chrome.app.window.current();
528 if (appWindow.isMaximized())
529 appWindow.restore();
530 else
531 appWindow.maximize();
532 };
533
534 FileManager.prototype.onClose = function() {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100535 // Do not close when running via chrome://files in a browser.
536 if (util.platform.runningInBrowser())
537 return;
538
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +0100539 window.close();
540 };
541
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000542 /**
543 * One-time initialization of commands.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000544 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000545 */
546 FileManager.prototype.initCommands_ = function() {
547 var commandButtons = this.dialogDom_.querySelectorAll('button[command]');
548 for (var j = 0; j < commandButtons.length; j++)
549 CommandButton.decorate(commandButtons[j]);
550
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000551 // TODO(dzvorygin): Here we use this hack, since 'hidden' is standard
552 // attribute and we can't use it's setter as usual.
553 cr.ui.Command.prototype.setHidden = function(value) {
554 this.__lookupSetter__('hidden').call(this, value);
555 };
556
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000557 var commands = this.dialogDom_.querySelectorAll('command');
558 for (var i = 0; i < commands.length; i++)
559 cr.ui.Command.decorate(commands[i]);
560
561 var doc = this.document_;
562
Ben Murdochd3868032013-07-31 10:55:33 +0100563 CommandUtil.registerCommand(this.dialogContainer_, 'newfolder',
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000564 Commands.newFolderCommand, this, this.directoryModel_);
565
Ben Murdochd3868032013-07-31 10:55:33 +0100566 CommandUtil.registerCommand(this.dialogContainer_, 'newwindow',
Ben Murdocheb525c52013-07-10 11:40:50 +0100567 Commands.newWindowCommand, this, this.directoryModel_);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000568
Ben Murdochd3868032013-07-31 10:55:33 +0100569 CommandUtil.registerCommand(this.dialogContainer_, 'change-default-app',
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000570 Commands.changeDefaultAppCommand, this);
571
Ben Murdoch32409262013-08-07 11:04:47 +0100572 CommandUtil.registerCommand(this.navigationList_, 'unmount',
Ben Murdochbb1529c2013-08-08 10:24:53 +0100573 Commands.unmountCommand, this);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100574
Ben Murdoch32409262013-08-07 11:04:47 +0100575 CommandUtil.registerCommand(this.navigationList_, 'import-photos',
576 Commands.importCommand, this.navigationList_);
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +0100577
Ben Murdochd3868032013-07-31 10:55:33 +0100578 CommandUtil.registerCommand(this.dialogContainer_, 'format',
Ben Murdochbb1529c2013-08-08 10:24:53 +0100579 Commands.formatCommand, this,
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100580 this.directoryModel_);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000581
Ben Murdochd3868032013-07-31 10:55:33 +0100582 CommandUtil.registerCommand(this.dialogContainer_, 'delete',
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000583 Commands.deleteFileCommand, this);
584
Ben Murdochd3868032013-07-31 10:55:33 +0100585 CommandUtil.registerCommand(this.dialogContainer_, 'rename',
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000586 Commands.renameFileCommand, this);
587
Ben Murdochd3868032013-07-31 10:55:33 +0100588 CommandUtil.registerCommand(this.dialogContainer_, 'volume-help',
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000589 Commands.volumeHelpCommand, this);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000590
Ben Murdochd3868032013-07-31 10:55:33 +0100591 CommandUtil.registerCommand(this.dialogContainer_, 'drive-buy-more-space',
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000592 Commands.driveBuySpaceCommand, this);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000593
Ben Murdochd3868032013-07-31 10:55:33 +0100594 CommandUtil.registerCommand(this.dialogContainer_,
595 'drive-clear-local-cache', Commands.driveClearCacheCommand, this);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000596
Ben Murdochd3868032013-07-31 10:55:33 +0100597 CommandUtil.registerCommand(this.dialogContainer_, 'drive-go-to-drive',
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000598 Commands.driveGoToDriveCommand, this);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000599
Ben Murdochd3868032013-07-31 10:55:33 +0100600 CommandUtil.registerCommand(this.dialogContainer_, 'paste',
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000601 Commands.pasteFileCommand, doc, this.fileTransferController_);
602
Ben Murdochd3868032013-07-31 10:55:33 +0100603 CommandUtil.registerCommand(this.dialogContainer_, 'open-with',
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000604 Commands.openWithCommand, this);
605
Ben Murdochd3868032013-07-31 10:55:33 +0100606 CommandUtil.registerCommand(this.dialogContainer_, 'toggle-pinned',
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000607 Commands.togglePinnedCommand, this);
608
Ben Murdochd3868032013-07-31 10:55:33 +0100609 CommandUtil.registerCommand(this.dialogContainer_, 'zip-selection',
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000610 Commands.zipSelectionCommand, this, this.directoryModel_);
611
Ben Murdochd3868032013-07-31 10:55:33 +0100612 CommandUtil.registerCommand(this.dialogContainer_, 'share',
613 Commands.shareCommand, this);
Ben Murdocheb525c52013-07-10 11:40:50 +0100614
Ben Murdochd3868032013-07-31 10:55:33 +0100615 CommandUtil.registerCommand(this.dialogContainer_,
616 'create-folder-shortcut', Commands.createFolderShortcutCommand, this);
617
618 CommandUtil.registerCommand(this.dialogContainer_,
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100619 'remove-folder-shortcut', Commands.removeFolderShortcutCommand, this);
Ben Murdochd3868032013-07-31 10:55:33 +0100620
621 CommandUtil.registerCommand(this.dialogContainer_, 'search',
622 Commands.searchCommand, this,
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100623 this.dialogDom_.querySelector('#search-box'));
624
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100625 // Register commands with CTRL-1..9 shortcuts for switching between
626 // volumes.
627 for (var i = 1; i <= 9; i++) {
Ben Murdochd3868032013-07-31 10:55:33 +0100628 CommandUtil.registerCommand(this.dialogContainer_,
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100629 'volume-switch-' + i,
630 Commands.volumeSwitchCommand,
Ben Murdoch32409262013-08-07 11:04:47 +0100631 this.navigationList_,
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100632 i);
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100633 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000634
Ben Murdocheb525c52013-07-10 11:40:50 +0100635 CommandUtil.registerCommand(doc, 'zoom-in', Commands.zoomInCommand);
636 CommandUtil.registerCommand(doc, 'zoom-out', Commands.zoomOutCommand);
637 CommandUtil.registerCommand(doc, 'zoom-reset', Commands.zoomResetCommand);
638
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100639 CommandUtil.registerCommand(this.dialogContainer_, 'cut',
640 Commands.defaultCommand, doc);
641 CommandUtil.registerCommand(this.dialogContainer_, 'copy',
642 Commands.defaultCommand, doc);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000643
644 var inputs = this.dialogDom_.querySelectorAll(
645 'input[type=text], input[type=search], textarea');
646
647 for (i = 0; i < inputs.length; i++) {
648 cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_);
649 this.registerInputCommands_(inputs[i]);
650 }
651
652 cr.ui.contextMenuHandler.setContextMenu(this.renameInput_,
653 this.textContextMenu_);
654 this.registerInputCommands_(this.renameInput_);
655
656 doc.addEventListener('command', this.setNoHover_.bind(this, true));
657 };
658
659 /**
660 * Registers cut, copy, paste and delete commands on input element.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000661 *
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000662 * @param {Node} node Text input element to register on.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000663 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000664 */
665 FileManager.prototype.registerInputCommands_ = function(node) {
666 var defaultCommand = Commands.defaultCommand;
667 CommandUtil.forceDefaultHandler(node, 'cut');
668 CommandUtil.forceDefaultHandler(node, 'copy');
669 CommandUtil.forceDefaultHandler(node, 'paste');
670 CommandUtil.forceDefaultHandler(node, 'delete');
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100671 node.addEventListener('keydown', function(e) {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100672 if (util.getKeyModifiers(e) + e.keyCode == '191') {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100673 // If this key event is propagated, this is handled search command,
674 // which calls 'preventDefault' mehtod.
675 e.stopPropagation();
676 }
677 });
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000678 };
679
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100680 FileManager.prototype.initializeCore = function() {
681 this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral');
682 this.initializeQueue_.add(this.initStrings_.bind(this), [], 'initStrings');
683 this.initializeQueue_.add(
684 this.initPreferences_.bind(this), [], 'initPreferences');
685 this.initializeQueue_.add(
686 this.initFileSystem_.bind(this),
687 ['initGeneral', 'initPreferences'], 'initFileSystem');
688
689 this.initializeQueue_.run();
690 };
691
692 FileManager.prototype.initializeUI = function(dialogDom, callback) {
693 this.dialogDom_ = dialogDom;
694
695 this.initializeQueue_.add(
696 this.initEssentialUI_.bind(this),
697 ['initGeneral', 'initStrings'],
698 'initEssentialUI');
699 this.initializeQueue_.add(this.initAdditionalUI_.bind(this),
700 ['initEssentialUI'], 'initAdditionalUI');
701 this.initializeQueue_.add(
702 this.initFileSystemUI_.bind(this),
703 ['initFileSystem', 'initAdditionalUI'],
704 'initFileSystemUI');
705
706 // Run again just in case if all pending closures have completed and the
707 // queue has stopped and monitor the completion.
708 this.initializeQueue_.run(callback);
709 };
710
711 /**
712 * Initializes general purpose basic things, which are used by other
713 * initializing methods.
714 *
715 * @param {function()} callback Completion callback.
716 * @private
717 */
718 FileManager.prototype.initGeneral_ = function(callback) {
719 this.volumeManager_ = VolumeManager.getInstance();
720 if (window.appState) {
721 this.params_ = window.appState.params || {};
722 this.defaultPath = window.appState.defaultPath;
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100723 } else {
724 this.params_ = location.search ?
725 JSON.parse(decodeURIComponent(location.search.substr(1))) :
726 {};
727 this.defaultPath = this.params_.defaultPath;
728 }
729 callback();
730 };
731
732 /**
733 * One time initialization of strings (mostly i18n).
734 *
735 * @param {function()} callback Completion callback.
736 * @private
737 */
738 FileManager.prototype.initStrings_ = function(callback) {
Ben Murdocheb525c52013-07-10 11:40:50 +0100739 // Fetch the strings via the private api if running in the browser window.
740 // Otherwise, read cached strings from the local storage.
741 if (util.platform.runningInBrowser()) {
742 chrome.fileBrowserPrivate.getStrings(function(strings) {
743 loadTimeData.data = strings;
744 callback();
745 });
746 } else {
747 chrome.storage.local.get('strings', function(items) {
748 loadTimeData.data = items['strings'];
749 callback();
750 });
751 }
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100752 };
753
754 /**
755 * One time initialization of the Files.app's essential UI elements. These
756 * elements will be shown to the user. Only visible elements should be
757 * initialized here. Any heavy operation should be avoided. Files.app's
758 * window is shown at the end of this routine.
759 *
760 * @param {function()} callback Completion callback.
761 * @private
762 */
763 FileManager.prototype.initEssentialUI_ = function(callback) {
764 this.listType_ = null;
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100765
766 this.filesystemObserverId_ = null;
767 this.driveObserverId_ = null;
768
769 this.document_ = this.dialogDom_.ownerDocument;
770 this.dialogType = this.params_.type || DialogType.FULL_PAGE;
771 this.startupPrefName_ = 'file-manager-' + this.dialogType;
772
773 // Used to filter out focusing by mouse.
774 this.suppressFocus_ = false;
775
776 // Optional list of file types.
777 this.fileTypes_ = this.params_.typeList || [];
778 metrics.recordEnum('Create', this.dialogType,
779 [DialogType.SELECT_FOLDER,
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100780 DialogType.SELECT_UPLOAD_FOLDER,
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100781 DialogType.SELECT_SAVEAS_FILE,
782 DialogType.SELECT_OPEN_FILE,
783 DialogType.SELECT_OPEN_MULTI_FILE,
784 DialogType.FULL_PAGE]);
785
786 this.selectionHandler_ = null;
787 this.ctrlKeyPressed_ = false;
788
789 this.metadataCache_ = MetadataCache.createFull();
790
Ben Murdocheb525c52013-07-10 11:40:50 +0100791 this.hasFooterPanel_ =
792 this.dialogType == DialogType.SELECT_SAVEAS_FILE ||
793 this.dialogType == DialogType.SELECT_FOLDER;
794
795 // If the footer panel exists, the buttons are placed there. Otherwise,
796 // the buttons are on the preview panel.
797 var parentPanelOfButtons = this.dialogDom_.querySelector(
798 !this.hasFooterPanel_ ? '.preview-panel' : '.dialog-footer');
799 parentPanelOfButtons.classList.add('button-panel');
800 this.fileTypeSelector_ = parentPanelOfButtons.querySelector('.file-type');
801 this.okButton_ = parentPanelOfButtons.querySelector('.ok');
802 this.cancelButton_ = parentPanelOfButtons.querySelector('.cancel');
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100803
804 // Pre-populate the static localized strings.
805 i18nTemplate.process(this.document_, loadTimeData);
806
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100807 // Initialize the header.
808 this.dialogDom_.querySelector('#app-name').innerText =
809 chrome.runtime.getManifest().name;
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100810
811 this.initDialogType_();
812
813 // Show the window as soon as the UI pre-initialization is done.
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100814 if (this.dialogType == DialogType.FULL_PAGE &&
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100815 !util.platform.runningInBrowser()) {
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100816 chrome.app.window.current().show();
817 setTimeout(callback, 100); // Wait until the animation is finished.
818 } else {
819 callback();
820 }
821 };
822
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000823 /**
824 * One-time initialization of dialogs.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000825 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000826 */
827 FileManager.prototype.initDialogs_ = function() {
828 var d = cr.ui.dialogs;
829 d.BaseDialog.OK_LABEL = str('OK_LABEL');
830 d.BaseDialog.CANCEL_LABEL = str('CANCEL_LABEL');
Ben Murdochca12bfa2013-07-23 11:17:05 +0100831 this.error = new ErrorDialog(this.dialogDom_);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000832 this.alert = new d.AlertDialog(this.dialogDom_);
833 this.confirm = new d.ConfirmDialog(this.dialogDom_);
834 this.prompt = new d.PromptDialog(this.dialogDom_);
Ben Murdochbbcdd452013-07-25 10:06:34 +0100835 this.shareDialog_ = new ShareDialog(this.dialogDom_);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000836 this.defaultTaskPicker =
837 new cr.filebrowser.DefaultActionDialog(this.dialogDom_);
838 };
839
840 /**
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100841 * One-time initialization of various DOM nodes. Loads the additional DOM
842 * elements visible to the user. Initialize here elements, which are expensive
843 * or hidden in the beginning.
844 *
845 * @param {function()} callback Completion callback.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000846 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000847 */
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100848 FileManager.prototype.initAdditionalUI_ = function(callback) {
849 this.initDialogs_();
850
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000851 this.dialogDom_.addEventListener('drop', function(e) {
852 // Prevent opening an URL by dropping it onto the page.
853 e.preventDefault();
854 });
855
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000856 this.dialogDom_.addEventListener('click',
857 this.onExternalLinkClick_.bind(this));
858 // Cache nodes we'll be manipulating.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000859 var dom = this.dialogDom_;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000860
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000861 this.filenameInput_ = dom.querySelector('#filename-input-box input');
862 this.taskItems_ = dom.querySelector('#tasks');
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000863
864 this.table_ = dom.querySelector('.detail-table');
865 this.grid_ = dom.querySelector('.thumbnail-grid');
866 this.spinner_ = dom.querySelector('#spinner-with-text');
Ben Murdocheb525c52013-07-10 11:40:50 +0100867 this.showSpinner_(true);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000868
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000869 this.searchBreadcrumbs_ = new BreadcrumbsController(
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100870 dom.querySelector('#search-breadcrumbs'), this.metadataCache_);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000871 this.searchBreadcrumbs_.addEventListener(
872 'pathclick', this.onBreadcrumbClick_.bind(this));
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100873 this.searchBreadcrumbs_.setHideLast(false);
874
875 // Check the option to hide the selecting checkboxes.
Ben Murdocheb525c52013-07-10 11:40:50 +0100876 this.table_.showCheckboxes = this.showCheckboxes_;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000877
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000878 var fullPage = this.dialogType == DialogType.FULL_PAGE;
879 FileTable.decorate(this.table_, this.metadataCache_, fullPage);
880 FileGrid.decorate(this.grid_, this.metadataCache_);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000881
882 this.document_.addEventListener('keydown', this.onKeyDown_.bind(this));
883 this.document_.addEventListener('keyup', this.onKeyUp_.bind(this));
884
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000885 // This capturing event is only used to distinguish focusing using
886 // keyboard from focusing using mouse.
887 this.document_.addEventListener('mousedown', function() {
888 this.suppressFocus_ = true;
889 }.bind(this), true);
890
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000891 this.renameInput_ = this.document_.createElement('input');
892 this.renameInput_.className = 'rename';
893
894 this.renameInput_.addEventListener(
895 'keydown', this.onRenameInputKeyDown_.bind(this));
896 this.renameInput_.addEventListener(
897 'blur', this.onRenameInputBlur_.bind(this));
898
899 this.filenameInput_.addEventListener(
900 'keydown', this.onFilenameInputKeyDown_.bind(this));
901 this.filenameInput_.addEventListener(
902 'focus', this.onFilenameInputFocus_.bind(this));
903
904 this.listContainer_ = this.dialogDom_.querySelector('#list-container');
905 this.listContainer_.addEventListener(
906 'keydown', this.onListKeyDown_.bind(this));
907 this.listContainer_.addEventListener(
908 'keypress', this.onListKeyPress_.bind(this));
909 this.listContainer_.addEventListener(
910 'mousemove', this.onListMouseMove_.bind(this));
911
912 this.okButton_.addEventListener('click', this.onOk_.bind(this));
913 this.onCancelBound_ = this.onCancel_.bind(this);
914 this.cancelButton_.addEventListener('click', this.onCancelBound_);
915
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100916 this.decorateSplitter(
917 this.dialogDom_.querySelector('div#sidebar-splitter'));
918 this.decorateSplitter(
919 this.dialogDom_.querySelector('div#middlebar-splitter'));
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000920
921 this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container');
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100922
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000923 this.syncButton = this.dialogDom_.querySelector('#drive-sync-settings');
924 this.syncButton.addEventListener('activate', this.onDrivePrefClick_.bind(
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000925 this, 'cellularDisabled', false /* not inverted */));
926
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000927 this.hostedButton = this.dialogDom_.querySelector('#drive-hosted-settings');
928 this.hostedButton.addEventListener('activate', this.onDrivePrefClick_.bind(
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000929 this, 'hostedFilesDisabled', true /* inverted */));
930
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100931 this.detailViewButton_ =
932 this.dialogDom_.querySelector('#detail-view');
933 this.detailViewButton_.addEventListener('activate',
934 this.onDetailViewButtonClick_.bind(this));
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100935
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100936 this.thumbnailViewButton_ =
937 this.dialogDom_.querySelector('#thumbnail-view');
938 this.thumbnailViewButton_.addEventListener('activate',
939 this.onThumbnailViewButtonClick_.bind(this));
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100940
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000941 cr.ui.ComboButton.decorate(this.taskItems_);
942 this.taskItems_.addEventListener('select',
943 this.onTaskItemClicked_.bind(this));
944
945 this.dialogDom_.ownerDocument.defaultView.addEventListener(
946 'resize', this.onResize_.bind(this));
947
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000948 this.filePopup_ = null;
949
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +0100950 this.searchBoxWrapper_ =
951 this.dialogDom_.querySelector('.search-box-wrapper');
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000952 this.searchBox_ = this.dialogDom_.querySelector('#search-box');
953 this.searchBox_.addEventListener(
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000954 'input', this.onSearchBoxUpdate_.bind(this));
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100955 this.searchBox_.addEventListener(
956 'keydown', this.onSearchBoxKeyDown_.bind(this));
957 this.searchTextMeasure_ = new TextMeasure(this.searchBox_);
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100958 this.searchIcon_ = this.dialogDom_.querySelector('#search-icon');
959 this.searchIcon_.addEventListener(
960 'click',
961 function() { this.searchBox_.focus(); }.bind(this));
962 this.searchClearButton_ =
963 this.dialogDom_.querySelector('#search-clear-button');
964 this.searchClearButton_.addEventListener(
965 'click',
966 function() {
967 this.searchBox_.value = '';
968 this.onSearchBoxUpdate_();
969 }.bind(this));
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100970 this.lastSearchQuery_ = '';
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000971
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000972 var autocompleteList = new cr.ui.AutocompleteList();
973 autocompleteList.id = 'autocomplete-list';
974 autocompleteList.autoExpands = true;
975 autocompleteList.requestSuggestions =
976 this.requestAutocompleteSuggestions_.bind(this);
977 // function(item) {}.bind(this) does not work here, as it's a constructor.
978 var self = this;
979 autocompleteList.itemConstructor = function(item) {
980 return self.createAutocompleteListItem_(item);
981 };
982
983 // Do nothing when a suggestion is selected.
984 autocompleteList.handleSelectedSuggestion = function(selectedItem) {};
985 // Instead, open the suggested item when Enter key is pressed or
986 // mouse-clicked.
987 autocompleteList.handleEnterKeydown = function(event) {
988 this.openAutocompleteSuggestion_();
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100989 this.lastAutocompleteQuery_ = '';
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000990 this.autocompleteList_.suggestions = [];
991 }.bind(this);
992 autocompleteList.addEventListener('mousedown', function(event) {
993 this.openAutocompleteSuggestion_();
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100994 this.lastAutocompleteQuery_ = '';
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000995 this.autocompleteList_.suggestions = [];
996 }.bind(this));
997 autocompleteList.addEventListener('mouseover', function(event) {
998 // Change the selection by a mouse over instead of just changing the
999 // color of moused over element with :hover in CSS. Here's why:
1000 //
1001 // 1) The user selects an item A with up/down keys (item A is highlighted)
1002 // 2) Then the user moves the cursor to another item B
1003 //
1004 // If we just change the color of moused over element (item B), both
1005 // the item A and B are highlighted. This is bad. We should change the
1006 // selection so only the item B is highlighted.
1007 if (event.target.itemInfo)
1008 autocompleteList.selectedItem = event.target.itemInfo;
1009 }.bind(this));
1010
1011 var container = this.document_.querySelector('.dialog-header');
1012 container.appendChild(autocompleteList);
1013 this.autocompleteList_ = autocompleteList;
1014
1015 this.searchBox_.addEventListener('focus', function(event) {
1016 this.autocompleteList_.attachToInput(this.searchBox_);
1017 }.bind(this));
1018 this.searchBox_.addEventListener('blur', function(event) {
1019 this.autocompleteList_.detach();
1020 }.bind(this));
1021
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001022 this.defaultActionMenuItem_ =
1023 this.dialogDom_.querySelector('#default-action');
1024
1025 this.openWithCommand_ =
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001026 this.dialogDom_.querySelector('#open-with');
1027
1028 this.driveBuyMoreStorageCommand_ =
1029 this.dialogDom_.querySelector('#drive-buy-more-space');
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001030
1031 this.defaultActionMenuItem_.addEventListener('activate',
1032 this.dispatchSelectionAction_.bind(this));
1033
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001034 this.initFileTypeFilter_();
1035
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01001036 util.addIsFocusedMethod();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001037
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001038 // Populate the static localized strings.
1039 i18nTemplate.process(this.document_, loadTimeData);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001040
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +01001041 // Arrange the file list.
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01001042 this.table_.normalizeColumns();
1043 this.table_.redraw();
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +01001044
1045 callback();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001046 };
1047
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001048 /**
1049 * @private
1050 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001051 FileManager.prototype.onBreadcrumbClick_ = function(event) {
1052 this.directoryModel_.changeDirectory(event.path);
1053 };
1054
1055 /**
1056 * Constructs table and grid (heavy operation).
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001057 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001058 **/
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +01001059 FileManager.prototype.initFileList_ = function() {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001060 // Always sharing the data model between the detail/thumb views confuses
1061 // them. Instead we maintain this bogus data model, and hook it up to the
1062 // view that is not in use.
1063 this.emptyDataModel_ = new cr.ui.ArrayDataModel([]);
1064 this.emptySelectionModel_ = new cr.ui.ListSelectionModel();
1065
1066 var singleSelection =
1067 this.dialogType == DialogType.SELECT_OPEN_FILE ||
1068 this.dialogType == DialogType.SELECT_FOLDER ||
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01001069 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER ||
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001070 this.dialogType == DialogType.SELECT_SAVEAS_FILE;
1071
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001072 var showSpecialSearchRoots =
1073 this.dialogType == DialogType.SELECT_OPEN_FILE ||
1074 this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE ||
1075 this.dialogType == DialogType.FULL_PAGE;
1076
1077 this.fileFilter_ = new FileFilter(
1078 this.metadataCache_,
1079 false /* Don't show dot files by default. */);
1080
Ben Murdoch2385ea32013-08-06 11:01:04 +01001081 this.fileWatcher_ = new FileWatcher(this.metadataCache_);
Ben Murdoch558790d2013-07-30 15:19:42 +01001082 this.fileWatcher_.addEventListener(
1083 'watcher-metadata-changed',
1084 this.onWatcherMetadataChanged_.bind(this));
1085
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001086 this.directoryModel_ = new DirectoryModel(
1087 this.filesystem_.root,
1088 singleSelection,
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001089 this.fileFilter_,
Ben Murdoch558790d2013-07-30 15:19:42 +01001090 this.fileWatcher_,
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001091 this.metadataCache_,
1092 this.volumeManager_,
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001093 this.isDriveEnabled(),
1094 showSpecialSearchRoots);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001095
1096 this.directoryModel_.start();
1097
Ben Murdochca12bfa2013-07-23 11:17:05 +01001098 this.folderShortcutsModel_ = new FolderShortcutsDataModel();
Ben Murdoch7dbb3d52013-07-17 14:55:54 +01001099
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001100 this.selectionHandler_ = new FileSelectionHandler(this);
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01001101 this.selectionHandler_.addEventListener('show-preview-panel',
1102 this.onPreviewPanelVisibilityChanged_.bind(this, true));
1103 this.selectionHandler_.addEventListener('hide-preview-panel',
1104 this.onPreviewPanelVisibilityChanged_.bind(this, false));
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001105
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001106 var dataModel = this.directoryModel_.getFileList();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001107
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001108 this.table_.setupCompareFunctions(dataModel);
1109
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001110 dataModel.addEventListener('permuted',
1111 this.updateStartupPrefs_.bind(this));
1112
1113 this.directoryModel_.getFileListSelection().addEventListener('change',
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001114 this.selectionHandler_.onFileSelectionChanged.bind(
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001115 this.selectionHandler_));
1116
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001117 this.initList_(this.grid_);
1118 this.initList_(this.table_.list);
1119
1120 var fileListFocusBound = this.onFileListFocus_.bind(this);
1121 var fileListBlurBound = this.onFileListBlur_.bind(this);
1122
1123 this.table_.list.addEventListener('focus', fileListFocusBound);
1124 this.grid_.addEventListener('focus', fileListFocusBound);
1125
1126 this.table_.list.addEventListener('blur', fileListBlurBound);
1127 this.grid_.addEventListener('blur', fileListBlurBound);
1128
Torne (Richard Coles)5e3f23d2013-06-11 16:24:11 +01001129 var dragStartBound = this.onDragStart_.bind(this);
1130 this.table_.list.addEventListener('dragstart', dragStartBound);
1131 this.grid_.addEventListener('dragstart', dragStartBound);
1132
1133 var dragEndBound = this.onDragEnd_.bind(this);
1134 this.table_.list.addEventListener('dragend', dragEndBound);
1135 this.grid_.addEventListener('dragend', dragEndBound);
Ben Murdocheb525c52013-07-10 11:40:50 +01001136 // This event is published by DragSelector because drag end event is not
1137 // published at the end of drag selection.
1138 this.table_.list.addEventListener('dragselectionend', dragEndBound);
Torne (Richard Coles)5e3f23d2013-06-11 16:24:11 +01001139
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +01001140 // TODO(mtomasz, yoshiki): Create sidebar earlier, and here just attach
1141 // the directory model.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001142 this.initSidebar_();
1143
1144 this.table_.addEventListener('column-resize-end',
1145 this.updateStartupPrefs_.bind(this));
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001146
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01001147 // Restore preferences.
1148 this.directoryModel_.sortFileList(
1149 this.viewOptions_.sortField || 'modificationTime',
1150 this.viewOptions_.sortDirection || 'desc');
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01001151 if (this.viewOptions_.columns) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001152 var cm = this.table_.columnModel;
1153 for (var i = 0; i < cm.totalSize; i++) {
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +01001154 if (this.viewOptions_.columns[i] > 0)
1155 cm.setWidth(i, this.viewOptions_.columns[i]);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001156 }
1157 }
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01001158 this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001159
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001160 this.textSearchState_ = {text: '', date: new Date()};
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001161 this.closeOnUnmount_ = (this.params_.action == 'auto-open');
1162
1163 if (this.closeOnUnmount_) {
1164 this.volumeManager_.addEventListener('externally-unmounted',
1165 this.onExternallyUnmounted_.bind(this));
1166 }
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01001167
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001168 // Update metadata to change 'Today' and 'Yesterday' dates.
1169 var today = new Date();
1170 today.setHours(0);
1171 today.setMinutes(0);
1172 today.setSeconds(0);
1173 today.setMilliseconds(0);
1174 setTimeout(this.dailyUpdateModificationTime_.bind(this),
1175 today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000);
1176 };
1177
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001178 /**
1179 * @private
1180 */
1181 FileManager.prototype.initSidebar_ = function() {
1182 this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree');
1183 DirectoryTree.decorate(this.directoryTree_, this.directoryModel_);
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01001184 this.directoryTree_.addEventListener('content-updated', function() {
1185 this.updateMiddleBarVisibility_(true);
1186 }.bind(this));
Torne (Richard Coles)a93a17c2013-05-15 11:34:50 +01001187
Ben Murdoch32409262013-08-07 11:04:47 +01001188 this.navigationList_ = this.dialogDom_.querySelector('#volume-list');
1189 NavigationList.decorate(this.navigationList_,
1190 this.directoryModel_,
1191 this.folderShortcutsModel_);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001192 };
1193
1194 /**
1195 * @param {boolean=} opt_delayed If true, updating is delayed by 500ms.
1196 * @private
1197 */
1198 FileManager.prototype.updateMiddleBarVisibility_ = function(opt_delayed) {
1199 if (this.updateMiddleBarVisibilityTimer_) {
1200 clearTimeout(this.updateMiddleBarVisibilityTimer_);
1201 this.updateMiddleBarVisibilityTimer_ = null;
1202 }
1203
1204 if (opt_delayed) {
1205 this.updateMiddleBarVisibilityTimer_ =
1206 setTimeout(this.updateMiddleBarVisibility_.bind(this, false), 500);
1207 return;
1208 }
1209 var currentPath = this.directoryModel_.getCurrentDirPath();
1210 var visible =
1211 (this.directoryTree_.items.length > 0) &&
1212 (!DirectoryTreeUtil.shouldHideTree(currentPath));
1213 this.dialogDom_.
1214 querySelector('.dialog-middlebar-contents').hidden = !visible;
1215 this.dialogDom_.querySelector('#middlebar-splitter').hidden = !visible;
1216 this.onResize_();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001217 };
1218
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001219 /**
1220 * @private
1221 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001222 FileManager.prototype.updateStartupPrefs_ = function() {
1223 var sortStatus = this.directoryModel_.getFileList().sortStatus;
1224 var prefs = {
1225 sortField: sortStatus.field,
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001226 sortDirection: sortStatus.direction,
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01001227 columns: [],
Ben Murdocheb525c52013-07-10 11:40:50 +01001228 listType: this.listType_
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001229 };
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01001230 var cm = this.table_.columnModel;
1231 for (var i = 0; i < cm.totalSize; i++) {
1232 prefs.columns.push(cm.getWidth(i));
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001233 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001234 if (DialogType.isModal(this.dialogType))
1235 prefs.listType = this.listType;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001236 // Save the global default.
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001237 util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs));
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001238
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001239 // Save the window-specific preference.
1240 if (window.appState) {
1241 window.appState.viewOptions = prefs;
1242 util.saveAppState();
1243 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001244 };
1245
1246 FileManager.prototype.refocus = function() {
1247 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE)
1248 this.filenameInput_.focus();
1249 else
1250 this.currentList_.focus();
1251 };
1252
1253 /**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001254 * File list focus handler. Used to select the top most element on the list
1255 * if nothing was selected.
1256 *
1257 * @private
1258 */
1259 FileManager.prototype.onFileListFocus_ = function() {
1260 // Do not select default item if focused using mouse.
1261 if (this.suppressFocus_)
1262 return;
1263
1264 var selection = this.getSelection();
1265 if (!selection || selection.totalCount != 0)
1266 return;
1267
1268 this.directoryModel_.selectIndex(0);
1269 };
1270
1271 /**
1272 * File list blur handler.
1273 *
1274 * @private
1275 */
1276 FileManager.prototype.onFileListBlur_ = function() {
1277 this.suppressFocus_ = false;
1278 };
1279
1280 /**
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001281 * Index of selected item in the typeList of the dialog params.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001282 *
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001283 * @return {number} 1-based index of selected type or 0 if no type selected.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001284 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001285 */
1286 FileManager.prototype.getSelectedFilterIndex_ = function() {
1287 var index = Number(this.fileTypeSelector_.selectedIndex);
1288 if (index < 0) // Nothing selected.
1289 return 0;
1290 if (this.params_.includeAllFiles) // Already 1-based.
1291 return index;
1292 return index + 1; // Convert to 1-based;
1293 };
1294
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001295 FileManager.prototype.setListType = function(type) {
1296 if (type && type == this.listType_)
1297 return;
1298
1299 this.table_.list.startBatchUpdates();
1300 this.grid_.startBatchUpdates();
1301
1302 // TODO(dzvorygin): style.display and dataModel setting order shouldn't
1303 // cause any UI bugs. Currently, the only right way is first to set display
1304 // style and only then set dataModel.
1305
1306 if (type == FileManager.ListType.DETAIL) {
1307 this.table_.dataModel = this.directoryModel_.getFileList();
1308 this.table_.selectionModel = this.directoryModel_.getFileListSelection();
Ben Murdocheb525c52013-07-10 11:40:50 +01001309 this.table_.hidden = false;
1310 this.grid_.hidden = true;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001311 this.grid_.selectionModel = this.emptySelectionModel_;
1312 this.grid_.dataModel = this.emptyDataModel_;
Ben Murdocheb525c52013-07-10 11:40:50 +01001313 this.table_.hidden = false;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001314 /** @type {cr.ui.List} */
1315 this.currentList_ = this.table_.list;
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01001316 this.detailViewButton_.setAttribute('checked', '');
1317 this.thumbnailViewButton_.removeAttribute('checked');
1318 this.detailViewButton_.setAttribute('disabled', '');
1319 this.thumbnailViewButton_.removeAttribute('disabled');
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001320 } else if (type == FileManager.ListType.THUMBNAIL) {
1321 this.grid_.dataModel = this.directoryModel_.getFileList();
1322 this.grid_.selectionModel = this.directoryModel_.getFileListSelection();
Ben Murdocheb525c52013-07-10 11:40:50 +01001323 this.grid_.hidden = false;
1324 this.table_.hidden = true;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001325 this.table_.selectionModel = this.emptySelectionModel_;
1326 this.table_.dataModel = this.emptyDataModel_;
Ben Murdocheb525c52013-07-10 11:40:50 +01001327 this.grid_.hidden = false;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001328 /** @type {cr.ui.List} */
1329 this.currentList_ = this.grid_;
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01001330 this.thumbnailViewButton_.setAttribute('checked', '');
1331 this.detailViewButton_.removeAttribute('checked');
1332 this.thumbnailViewButton_.setAttribute('disabled', '');
1333 this.detailViewButton_.removeAttribute('disabled');
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001334 } else {
1335 throw new Error('Unknown list type: ' + type);
1336 }
1337
1338 this.listType_ = type;
1339 this.updateStartupPrefs_();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001340 this.onResize_();
1341
1342 this.table_.list.endBatchUpdates();
1343 this.grid_.endBatchUpdates();
1344 };
1345
1346 /**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001347 * Initialize the file list table or grid.
1348 *
1349 * @param {cr.ui.List} list The list.
1350 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001351 */
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001352 FileManager.prototype.initList_ = function(list) {
1353 // Overriding the default role 'list' to 'listbox' for better accessibility
1354 // on ChromeOS.
1355 list.setAttribute('role', 'listbox');
1356 list.addEventListener('click', this.onDetailClick_.bind(this));
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001357 list.id = 'file-list';
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001358 };
1359
1360 /**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001361 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001362 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001363 FileManager.prototype.onCopyProgress_ = function(event) {
Ben Murdoch558790d2013-07-30 15:19:42 +01001364 if (event.reason == 'ERROR' &&
1365 event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR &&
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001366 event.error.data.toDrive &&
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001367 event.error.data.code == FileError.QUOTA_EXCEEDED_ERR) {
1368 this.alert.showHtml(
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001369 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
1370 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001371 decodeURIComponent(
1372 event.error.data.sourceFileUrl.split('/').pop()),
1373 FileManager.GOOGLE_DRIVE_BUY_STORAGE));
1374 }
1375
1376 // TODO(benchan): Currently, there is no FileWatcher emulation for
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001377 // drive::FileSystem, so we need to manually trigger the directory rescan
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001378 // after paste operations complete. Remove this once we emulate file
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001379 // watching functionalities in drive::FileSystem.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001380 if (this.isOnDrive()) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001381 if (event.reason == 'SUCCESS' || event.reason == 'ERROR' ||
1382 event.reason == 'CANCELLED') {
1383 this.directoryModel_.rescanLater();
1384 }
1385 }
1386 };
1387
1388 /**
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01001389 * Handler of file manager operations. Called when an entry has been
1390 * changed.
1391 * This updates directory model to reflect operation result immediately (not
1392 * waiting for directory update event). Also, preloads thumbnails for the
1393 * images of new entries.
1394 * See also FileCopyManager.EventRouter.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001395 *
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01001396 * @param {cr.Event} event An event for the entry change.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001397 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001398 */
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01001399 FileManager.prototype.onCopyManagerEntryChanged_ = function(event) {
1400 var type = event.type;
1401 var entry = event.entry;
1402 this.directoryModel_.onEntryChanged(type, entry);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001403
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01001404 if (type == util.EntryChangedType.CREATE && FileType.isImage(entry)) {
1405 // Preload a thumbnail if the new copied entry an image.
1406 var metadata = entry.getMetadata(function(metadata) {
1407 var url = entry.toURL();
1408 var thumbnailLoader_ = new ThumbnailLoader(
1409 url,
1410 ThumbnailLoader.LoaderType.CANVAS,
1411 metadata,
1412 undefined, // Media type.
1413 FileType.isOnDrive(url) ?
1414 ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
1415 ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
1416 10); // Very low priority.
1417 thumbnailLoader_.loadDetachedImage(function(success) {});
1418 });
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001419 }
1420 };
1421
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001422 /**
1423 * Fills the file type list or hides it.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001424 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001425 */
1426 FileManager.prototype.initFileTypeFilter_ = function() {
1427 if (this.params_.includeAllFiles) {
1428 var option = this.document_.createElement('option');
1429 option.innerText = str('ALL_FILES_FILTER');
1430 this.fileTypeSelector_.appendChild(option);
1431 option.value = 0;
1432 }
1433
1434 for (var i = 0; i < this.fileTypes_.length; i++) {
1435 var fileType = this.fileTypes_[i];
1436 var option = this.document_.createElement('option');
1437 var description = fileType.description;
1438 if (!description) {
1439 // See if all the extensions in the group have the same description.
1440 for (var j = 0; j != fileType.extensions.length; j++) {
1441 var currentDescription =
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001442 FileType.getTypeString('.' + fileType.extensions[j]);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001443 if (!description) // Set the first time.
1444 description = currentDescription;
1445 else if (description != currentDescription) {
1446 // No single description, fall through to the extension list.
1447 description = null;
1448 break;
1449 }
1450 }
1451
1452 if (!description)
1453 // Convert ['jpg', 'png'] to '*.jpg, *.png'.
1454 description = fileType.extensions.map(function(s) {
1455 return '*.' + s;
1456 }).join(', ');
1457 }
1458 option.innerText = description;
1459
1460 option.value = i + 1;
1461
1462 if (fileType.selected)
1463 option.selected = true;
1464
1465 this.fileTypeSelector_.appendChild(option);
1466 }
1467
1468 var options = this.fileTypeSelector_.querySelectorAll('option');
1469 if (options.length < 2) {
1470 // There is in fact no choice, hide the selector.
1471 this.fileTypeSelector_.hidden = true;
1472 return;
1473 }
1474
1475 this.fileTypeSelector_.addEventListener('change',
1476 this.updateFileTypeFilter_.bind(this));
1477 };
1478
1479 /**
1480 * Filters file according to the selected file type.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001481 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001482 */
1483 FileManager.prototype.updateFileTypeFilter_ = function() {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001484 this.fileFilter_.removeFilter('fileType');
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001485 var selectedIndex = this.getSelectedFilterIndex_();
1486 if (selectedIndex > 0) { // Specific filter selected.
1487 var regexp = new RegExp('.*(' +
1488 this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i');
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001489 var filter = function(entry) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001490 return entry.isDirectory || regexp.test(entry.name);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001491 };
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001492 this.fileFilter_.addFilter('fileType', filter);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001493 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001494 };
1495
1496 /**
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001497 * Resize details and thumb views to fit the new window size.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001498 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001499 */
1500 FileManager.prototype.onResize_ = function() {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01001501 if (this.listType_ == FileManager.ListType.THUMBNAIL)
1502 this.grid_.relayout();
1503 else
1504 this.table_.relayout();
Ben Murdocheb525c52013-07-10 11:40:50 +01001505
1506 // May not be available during initialization.
1507 if (this.directoryTree_)
1508 this.directoryTree_.relayout();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001509
Ben Murdoch32409262013-08-07 11:04:47 +01001510 // TODO(mtomasz, yoshiki): Initialize navigation list earlier, before
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01001511 // file system is available.
Ben Murdoch32409262013-08-07 11:04:47 +01001512 if (this.navigationList_)
1513 this.navigationList_.redraw();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001514
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +01001515 // Hide the search box if there is not enough space.
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01001516 this.searchBoxWrapper_.classList.toggle(
1517 'too-short',
1518 this.searchBoxWrapper_.clientWidth < 100);
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +01001519
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001520 this.searchBreadcrumbs_.truncate();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001521 };
1522
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001523 /**
Ben Murdoch558790d2013-07-30 15:19:42 +01001524 * Handles local metadata changes in the currect directory.
1525 * @param {Event} event Change event.
1526 * @private
1527 */
1528 FileManager.prototype.onWatcherMetadataChanged_ = function(event) {
1529 this.updateMetadataInUI_(event.metadataType, event.urls, event.properties);
1530 };
1531
1532 /**
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01001533 * Resize details and thumb views to fit the new window size.
1534 * @private
1535 */
1536 FileManager.prototype.onPreviewPanelVisibilityChanged_ = function(visible) {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01001537 var panelHeight = visible ? this.getPreviewPanelHeight_() : 0;
Ben Murdocheb525c52013-07-10 11:40:50 +01001538 this.grid_.setBottomMarginForPanel(panelHeight);
1539 this.table_.setBottomMarginForPanel(panelHeight);
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01001540 this.directoryTree_.setBottomMarginForPanel(panelHeight);
1541 };
1542
1543 /**
Torne (Richard Coles)5e3f23d2013-06-11 16:24:11 +01001544 * Invoked when the drag is started on the list or the grid.
1545 * @private
1546 */
1547 FileManager.prototype.onDragStart_ = function() {
1548 this.selectionHandler_.setPreviewPanelMustBeHidden(true);
1549 };
1550
1551 /**
1552 * Invoked when the drag is ended on the list or the grid.
1553 * @private
1554 */
1555 FileManager.prototype.onDragEnd_ = function() {
1556 this.selectionHandler_.setPreviewPanelMustBeHidden(false);
1557 };
1558
1559 /**
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01001560 * Gets height of the preview panel, using cached value if available. This
1561 * returns the value even when the preview panel is hidden.
1562 *
1563 * @return {number} Height of the preview panel. If failure, returns 0.
1564 */
1565 FileManager.prototype.getPreviewPanelHeight_ = function() {
1566 if (!this.cachedPreviewPanelHeight_) {
1567 var previewPanel = this.dialogDom_.querySelector('.preview-panel');
1568 this.cachedPreviewPanelHeight_ = previewPanel.clientHeight;
1569 }
1570 return this.cachedPreviewPanelHeight_;
1571 };
1572
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001573 /**
1574 * Restores current directory and may be a selected item after page load (or
1575 * reload) or popping a state (after click on back/forward). If location.hash
1576 * is present it means that the user has navigated somewhere and that place
1577 * will be restored. defaultPath primarily is used with save/open dialogs.
1578 * Default path may also contain a file name. Freshly opened file manager
1579 * window has neither.
1580 *
1581 * @param {boolean} pageLoading True if the page is loading,
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001582 * false if popping state.
1583 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001584 */
1585 FileManager.prototype.setupCurrentDirectory_ = function(pageLoading) {
1586 var path = location.hash ? // Location hash has the highest priority.
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001587 decodeURIComponent(location.hash.substr(1)) :
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001588 this.defaultPath;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001589
1590 if (!pageLoading && path == this.directoryModel_.getCurrentDirPath())
1591 return;
1592
1593 if (!path) {
1594 path = this.directoryModel_.getDefaultDirectory();
1595 } else if (path.indexOf('/') == -1) {
1596 // Path is a file name.
1597 path = this.directoryModel_.getDefaultDirectory() + '/' + path;
1598 }
1599
1600 // In the FULL_PAGE mode if the hash path points to a file we might have
1601 // to invoke a task after selecting it.
1602 // If the file path is in params_ we only want to select the file.
1603 var invokeHandlers = pageLoading && (this.params_.action != 'select') &&
1604 this.dialogType == DialogType.FULL_PAGE;
1605
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001606 if (PathUtil.getRootType(path) === RootType.DRIVE) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001607 if (!this.isDriveEnabled()) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001608 var leafName = path.substr(path.indexOf('/') + 1);
1609 path = this.directoryModel_.getDefaultDirectory() + '/' + leafName;
1610 this.finishSetupCurrentDirectory_(path, invokeHandlers);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001611 return;
1612 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001613 if (this.volumeManager_.isMounted(RootDirectory.DRIVE)) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001614 this.finishSetupCurrentDirectory_(path, invokeHandlers);
1615 return;
1616 }
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001617
1618 var tracker = this.directoryModel_.createDirectoryChangeTracker();
1619 // Expected finish of setupPath to Drive.
1620 tracker.exceptInitialChange = true;
1621 tracker.start();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001622 // Waits until the Drive is mounted.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001623 this.volumeManager_.mountDrive(function() {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001624 tracker.stop();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001625 if (!tracker.hasChanged)
1626 this.finishSetupCurrentDirectory_(path, invokeHandlers);
1627 }.bind(this), function(error) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001628 tracker.stop();
1629 });
1630 } else {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001631 this.finishSetupCurrentDirectory_(path, invokeHandlers);
1632 }
1633 };
1634
1635 /**
1636 * @param {string} path Path to setup.
1637 * @param {boolean} invokeHandlers If thrue and |path| points to a file
1638 * then default handler is triggered.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001639 *
1640 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001641 */
1642 FileManager.prototype.finishSetupCurrentDirectory_ = function(
1643 path, invokeHandlers) {
1644 if (invokeHandlers) {
1645 var onResolve = function(baseName, leafName, exists) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001646 var urls = null;
1647 var action = null;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001648
1649 if (!exists || leafName == '') {
1650 // Non-existent file or a directory.
1651 if (this.params_.gallery) {
1652 // Reloading while the Gallery is open with empty or multiple
1653 // selection. Open the Gallery when the directory is scanned.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001654 urls = [];
1655 action = 'gallery';
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001656 }
1657 } else {
1658 // There are 3 ways we can get here:
1659 // 1. Invoked from file_manager_util::ViewFile. This can only
1660 // happen for 'gallery' and 'mount-archive' actions.
1661 // 2. Reloading a Gallery page. Must be an image or a video file.
1662 // 3. A user manually entered a URL pointing to a file.
1663 // We call the appropriate methods of FileTasks directly as we do
1664 // not need any of the preparations that |execute| method does.
1665 if (FileType.isImageOrVideo(path)) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001666 urls = [util.makeFilesystemUrl(path)];
1667 action = 'gallery';
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001668 }
1669 if (FileType.getMediaType(path) == 'archive') {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001670 urls = [util.makeFilesystemUrl(path)];
1671 action = 'archives';
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001672 }
1673 }
1674
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001675 if (urls) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001676 var listener = function() {
1677 this.directoryModel_.removeEventListener(
1678 'scan-completed', listener);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001679 var tasks = new FileTasks(this, this.params_);
1680 if (action == 'gallery') {
1681 tasks.openGallery(urls);
1682 } else if (action == 'archives') {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001683 tasks.mountArchives(urls);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001684 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001685 }.bind(this);
1686 this.directoryModel_.addEventListener('scan-completed', listener);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001687 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001688 }.bind(this);
1689
1690 this.directoryModel_.setupPath(path, onResolve);
1691 return;
1692 }
1693
1694 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
1695 this.directoryModel_.setupPath(path, function(basePath, leafName) {
1696 this.filenameInput_.value = leafName;
1697 this.selectDefaultPathInFilenameInput_();
1698 }.bind(this));
1699 return;
1700 }
1701
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001702 this.directoryModel_.setupPath(path);
1703 };
1704
1705 /**
1706 * Tweak the UI to become a particular kind of dialog, as determined by the
1707 * dialog type parameter passed to the constructor.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001708 *
1709 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001710 */
1711 FileManager.prototype.initDialogType_ = function() {
1712 var defaultTitle;
1713 var okLabel = str('OPEN_LABEL');
1714
1715 switch (this.dialogType) {
1716 case DialogType.SELECT_FOLDER:
1717 defaultTitle = str('SELECT_FOLDER_TITLE');
1718 break;
1719
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01001720 case DialogType.SELECT_UPLOAD_FOLDER:
1721 defaultTitle = str('SELECT_UPLOAD_FOLDER_TITLE');
1722 okLabel = str('UPLOAD_LABEL');
1723 break;
1724
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001725 case DialogType.SELECT_OPEN_FILE:
1726 defaultTitle = str('SELECT_OPEN_FILE_TITLE');
1727 break;
1728
1729 case DialogType.SELECT_OPEN_MULTI_FILE:
1730 defaultTitle = str('SELECT_OPEN_MULTI_FILE_TITLE');
1731 break;
1732
1733 case DialogType.SELECT_SAVEAS_FILE:
1734 defaultTitle = str('SELECT_SAVEAS_FILE_TITLE');
1735 okLabel = str('SAVE_LABEL');
1736 break;
1737
1738 case DialogType.FULL_PAGE:
1739 break;
1740
1741 default:
1742 throw new Error('Unknown dialog type: ' + this.dialogType);
1743 }
1744
1745 this.okButton_.textContent = okLabel;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001746 this.dialogDom_.setAttribute('type', this.dialogType);
1747 };
1748
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001749 /**
1750 * Unmounts device.
1751 * @param {string} path Path to a volume to unmount.
1752 */
1753 FileManager.prototype.unmountVolume = function(path) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001754 var onError = function(error) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001755 this.alert.showHtml('', str('UNMOUNT_FAILED'));
1756 };
1757 this.volumeManager_.unmount(path, function() {}, onError.bind(this));
1758 };
1759
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001760 /**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001761 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001762 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001763 FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() {
1764 var entries = this.directoryModel_.getFileList().slice();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001765 var directoryEntry = this.directoryModel_.getCurrentDirEntry();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001766 // We don't pass callback here. When new metadata arrives, we have an
1767 // observer registered to update the UI.
1768
1769 // TODO(dgozman): refresh content metadata only when modificationTime
1770 // changed.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001771 var isFakeEntry = typeof directoryEntry.toURL !== 'function';
1772 var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries);
1773 this.metadataCache_.clearRecursively(directoryEntry, '*');
1774 this.metadataCache_.get(getEntries, 'filesystem', null);
1775
1776 if (this.isOnDrive())
1777 this.metadataCache_.get(getEntries, 'drive', null);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001778
1779 var visibleItems = this.currentList_.items;
1780 var visibleEntries = [];
1781 for (var i = 0; i < visibleItems.length; i++) {
1782 var index = this.currentList_.getIndexOfListItem(visibleItems[i]);
1783 var entry = this.directoryModel_.getFileList().item(index);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001784 // The following check is a workaround for the bug in list: sometimes item
1785 // does not have listIndex, and therefore is not found in the list.
1786 if (entry) visibleEntries.push(entry);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001787 }
1788 this.metadataCache_.get(visibleEntries, 'thumbnail', null);
1789 };
1790
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001791 /**
1792 * @private
1793 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001794 FileManager.prototype.dailyUpdateModificationTime_ = function() {
1795 var fileList = this.directoryModel_.getFileList();
1796 var urls = [];
1797 for (var i = 0; i < fileList.length; i++) {
1798 urls.push(fileList.item(i).toURL());
1799 }
1800 this.metadataCache_.get(
1801 fileList.slice(), 'filesystem',
1802 this.updateMetadataInUI_.bind(this, 'filesystem', urls));
1803
1804 setTimeout(this.dailyUpdateModificationTime_.bind(this),
1805 MILLISECONDS_IN_DAY);
1806 };
1807
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001808 /**
1809 * @param {string} type Type of metadata changed.
1810 * @param {Array.<string>} urls Array of urls.
Ben Murdochca12bfa2013-07-23 11:17:05 +01001811 * @param {Object.<string, Object>} props Map from entry URLs to metadata
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001812 * props.
1813 * @private
1814 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001815 FileManager.prototype.updateMetadataInUI_ = function(
1816 type, urls, properties) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001817 var propertyByUrl = urls.reduce(function(map, url, index) {
1818 map[url] = properties[index];
1819 return map;
1820 }, {});
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001821
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001822 if (this.listType_ == FileManager.ListType.DETAIL)
1823 this.table_.updateListItemsMetadata(type, propertyByUrl);
1824 else
1825 this.grid_.updateListItemsMetadata(type, propertyByUrl);
1826 // TODO: update bottom panel thumbnails.
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001827 };
1828
1829 /**
1830 * Restore the item which is being renamed while refreshing the file list. Do
1831 * nothing if no item is being renamed or such an item disappeared.
1832 *
1833 * While refreshing file list it gets repopulated with new file entries.
1834 * There is not a big difference whether DOM items stay the same or not.
1835 * Except for the item that the user is renaming.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001836 *
1837 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001838 */
1839 FileManager.prototype.restoreItemBeingRenamed_ = function() {
1840 if (!this.isRenamingInProgress())
1841 return;
1842
1843 var dm = this.directoryModel_;
1844 var leadIndex = dm.getFileListSelection().leadIndex;
1845 if (leadIndex < 0)
1846 return;
1847
1848 var leadEntry = dm.getFileList().item(leadIndex);
1849 if (this.renameInput_.currentEntry.fullPath != leadEntry.fullPath)
1850 return;
1851
1852 var leadListItem = this.findListItemForNode_(this.renameInput_);
1853 if (this.currentList_ == this.table_.list) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001854 this.table_.updateFileMetadata(leadListItem, leadEntry);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001855 }
1856 this.currentList_.restoreLeadItem(leadListItem);
1857 };
1858
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001859 /**
1860 * @return {boolean} True if the current directory content is from Google
1861 * Drive.
1862 */
1863 FileManager.prototype.isOnDrive = function() {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001864 var rootType = this.directoryModel_.getCurrentRootType();
1865 return rootType === RootType.DRIVE ||
1866 rootType === RootType.DRIVE_SHARED_WITH_ME ||
1867 rootType === RootType.DRIVE_RECENT ||
1868 rootType === RootType.DRIVE_OFFLINE;
1869 };
1870
1871 /**
1872 * @return {boolean} True if the ctrl key is pressed now.
1873 */
1874 FileManager.prototype.isCtrlKeyPressed = function() {
1875 return this.ctrlKeyPressed_;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001876 };
1877
1878 /**
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001879 * Overrides default handling for clicks on hyperlinks.
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01001880 * In a packaged apps links with targer='_blank' open in a new tab by
1881 * default, other links do not open at all.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001882 *
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001883 * @param {Event} event Click event.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001884 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001885 */
1886 FileManager.prototype.onExternalLinkClick_ = function(event) {
1887 if (event.target.tagName != 'A' || !event.target.href)
1888 return;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001889
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01001890 if (this.dialogType != DialogType.FULL_PAGE)
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001891 this.onCancel_();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001892 };
1893
1894 /**
1895 * Task combobox handler.
1896 *
1897 * @param {Object} event Event containing task which was clicked.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001898 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001899 */
1900 FileManager.prototype.onTaskItemClicked_ = function(event) {
1901 var selection = this.getSelection();
1902 if (!selection.tasks) return;
1903
1904 if (event.item.task) {
1905 // Task field doesn't exist on change-default dropdown item.
1906 selection.tasks.execute(event.item.task.taskId);
1907 } else {
1908 var extensions = [];
1909
1910 for (var i = 0; i < selection.urls.length; i++) {
1911 var match = /\.(\w+)$/g.exec(selection.urls[i]);
1912 if (match) {
1913 var ext = match[1].toUpperCase();
1914 if (extensions.indexOf(ext) == -1) {
1915 extensions.push(ext);
1916 }
1917 }
1918 }
1919
1920 var format = '';
1921
1922 if (extensions.length == 1) {
1923 format = extensions[0];
1924 }
1925
1926 // Change default was clicked. We should open "change default" dialog.
1927 selection.tasks.showTaskPicker(this.defaultTaskPicker,
1928 loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'),
1929 strf('CHANGE_DEFAULT_CAPTION', format),
1930 this.onDefaultTaskDone_.bind(this));
1931 }
1932 };
1933
1934
1935 /**
1936 * Sets the given task as default, when this task is applicable.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001937 *
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001938 * @param {Object} task Task to set as default.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001939 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001940 */
1941 FileManager.prototype.onDefaultTaskDone_ = function(task) {
1942 // TODO(dgozman): move this method closer to tasks.
1943 var selection = this.getSelection();
1944 chrome.fileBrowserPrivate.setDefaultTask(task.taskId,
1945 selection.urls, selection.mimeTypes);
1946 selection.tasks = new FileTasks(this);
1947 selection.tasks.init(selection.urls, selection.mimeTypes);
1948 selection.tasks.display(this.taskItems_);
1949 this.refreshCurrentDirectoryMetadata_();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001950 this.selectionHandler_.onFileSelectionChanged();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001951 };
1952
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001953 /**
1954 * @private
1955 */
1956 FileManager.prototype.onPreferencesChanged_ = function() {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001957 var self = this;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001958 this.getPreferences_(function(prefs) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001959 self.initDateTimeFormatters_();
1960 self.refreshCurrentDirectoryMetadata_();
1961
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001962 self.directoryModel_.setDriveEnabled(self.isDriveEnabled());
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001963
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001964 if (prefs.cellularDisabled)
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001965 self.syncButton.setAttribute('checked', '');
1966 else
1967 self.syncButton.removeAttribute('checked');
1968
1969 if (self.hostedButton.hasAttribute('checked') !=
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001970 prefs.hostedFilesDisabled && self.isOnDrive()) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001971 self.directoryModel_.rescan();
1972 }
1973
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001974 if (!prefs.hostedFilesDisabled)
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001975 self.hostedButton.setAttribute('checked', '');
1976 else
1977 self.hostedButton.removeAttribute('checked');
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001978 },
1979 true /* refresh */);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001980 };
1981
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001982 FileManager.prototype.onDriveConnectionChanged_ = function() {
1983 var connection = this.volumeManager_.getDriveConnectionState();
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01001984 this.updateCommands();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001985 if (this.dialogContainer_)
1986 this.dialogContainer_.setAttribute('connection', connection.type);
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01001987 if (this.shareDialog_.isShowing()) {
1988 this.shareDialog_.hide();
1989 this.error.show(str('SHARE_ERROR'));
1990 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001991 };
1992
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001993 /**
1994 * Get the metered status of Drive connection.
1995 *
1996 * @return {boolean} Returns true if drive should limit the traffic because
1997 * the connection is metered and the 'disable-sync-on-metered' setting is
1998 * enabled. Otherwise, returns false.
1999 */
2000 FileManager.prototype.isDriveOnMeteredConnection = function() {
2001 var connection = this.volumeManager_.getDriveConnectionState();
2002 return connection.type == VolumeManager.DriveConnectionType.METERED;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002003 };
2004
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002005 /**
2006 * Get the online/offline status of drive.
2007 *
2008 * @return {boolean} Returns true if the connection is offline. Otherwise,
2009 * returns false.
2010 */
2011 FileManager.prototype.isDriveOffline = function() {
2012 var connection = this.volumeManager_.getDriveConnectionState();
2013 return connection.type == VolumeManager.DriveConnectionType.OFFLINE;
2014 };
2015
2016 FileManager.prototype.isDriveEnabled = function() {
Ben Murdoch32409262013-08-07 11:04:47 +01002017 // Auto resolving to local path does not work for folders (e.g., dialog for
2018 // loading unpacked extensions) and saving.
2019 // TODO(kinaba): make it work for the save dialog http://crbug.com/140425
2020 var noLocalPathResolution =
2021 this.params_.type == DialogType.SELECT_SAVEAS_FILE ||
2022 this.params_.type == DialogType.SELECT_FOLDER ||
2023 this.params_.type == DialogType.SELECT_UPLOAD_FOLDER;
2024 if (noLocalPathResolution && this.params_.shouldReturnLocalPath)
2025 return false;
2026 return this.preferences_.driveEnabled;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002027 };
2028
2029 FileManager.prototype.isOnReadonlyDirectory = function() {
2030 return this.directoryModel_.isReadOnly();
2031 };
2032
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002033 /**
2034 * @param {Event} Unmount event.
2035 * @private
2036 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002037 FileManager.prototype.onExternallyUnmounted_ = function(event) {
2038 if (event.mountPath == this.directoryModel_.getCurrentRootPath()) {
2039 if (this.closeOnUnmount_) {
2040 // If the file manager opened automatically when a usb drive inserted,
2041 // user have never changed current volume (that implies the current
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01002042 // directory is still on the device) then close this window.
2043 window.close();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002044 }
2045 }
2046 };
2047
2048 /**
2049 * Show a modal-like file viewer/editor on top of the File Manager UI.
2050 *
2051 * @param {HTMLElement} popup Popup element.
2052 * @param {function} closeCallback Function to call after the popup is closed.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002053 *
2054 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002055 */
2056 FileManager.prototype.openFilePopup_ = function(popup, closeCallback) {
2057 this.closeFilePopup_();
2058 this.filePopup_ = popup;
2059 this.filePopupCloseCallback_ = closeCallback;
2060 this.dialogDom_.appendChild(this.filePopup_);
2061 this.filePopup_.focus();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002062 this.document_.body.setAttribute('overlay-visible', '');
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01002063 this.document_.querySelector('#iframe-drag-area').hidden = false;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002064 };
2065
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002066 /**
2067 * @private
2068 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002069 FileManager.prototype.closeFilePopup_ = function() {
2070 if (this.filePopup_) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002071 this.document_.body.removeAttribute('overlay-visible');
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01002072 this.document_.querySelector('#iframe-drag-area').hidden = true;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002073 // The window resize would not be processed properly while the relevant
2074 // divs had 'display:none', force resize after the layout fired.
2075 setTimeout(this.onResize_.bind(this), 0);
2076 if (this.filePopup_.contentWindow &&
2077 this.filePopup_.contentWindow.unload) {
2078 this.filePopup_.contentWindow.unload();
2079 }
2080
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002081 if (this.filePopupCloseCallback_) {
2082 this.filePopupCloseCallback_();
2083 this.filePopupCloseCallback_ = null;
2084 }
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002085
2086 // These operations have to be in the end, otherwise v8 crashes on an
2087 // assert. See: crbug.com/224174.
2088 this.dialogDom_.removeChild(this.filePopup_);
2089 this.filePopup_ = null;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002090 }
2091 };
2092
2093 FileManager.prototype.getAllUrlsInCurrentDirectory = function() {
2094 var urls = [];
2095 var fileList = this.directoryModel_.getFileList();
2096 for (var i = 0; i != fileList.length; i++) {
2097 urls.push(fileList.item(i).toURL());
2098 }
2099 return urls;
2100 };
2101
2102 FileManager.prototype.isRenamingInProgress = function() {
2103 return !!this.renameInput_.currentEntry;
2104 };
2105
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002106 /**
2107 * @private
2108 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002109 FileManager.prototype.focusCurrentList_ = function() {
2110 if (this.listType_ == FileManager.ListType.DETAIL)
2111 this.table_.focus();
2112 else // this.listType_ == FileManager.ListType.THUMBNAIL)
2113 this.grid_.focus();
2114 };
2115
2116 /**
2117 * Return full path of the current directory or null.
Ben Murdochca12bfa2013-07-23 11:17:05 +01002118 * @return {?string} The full path of the current directory.
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002119 */
2120 FileManager.prototype.getCurrentDirectory = function() {
2121 return this.directoryModel_ &&
2122 this.directoryModel_.getCurrentDirPath();
2123 };
2124
2125 /**
2126 * Return URL of the current directory or null.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002127 * @return {string} URL representing the current directory.
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002128 */
2129 FileManager.prototype.getCurrentDirectoryURL = function() {
2130 return this.directoryModel_ &&
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002131 this.directoryModel_.getCurrentDirectoryURL();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002132 };
2133
Torne (Richard Coles)a93a17c2013-05-15 11:34:50 +01002134 /**
2135 * Return DirectoryEntry of the current directory or null.
2136 * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns
2137 * null if the directory model is not ready or the current directory is
2138 * not set.
2139 */
2140 FileManager.prototype.getCurrentDirectoryEntry = function() {
2141 return this.directoryModel_ &&
2142 this.directoryModel_.getCurrentDirEntry();
2143 };
2144
Ben Murdocheb525c52013-07-10 11:40:50 +01002145 /**
2146 * Deletes the selected file and directories recursively.
2147 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002148 FileManager.prototype.deleteSelection = function() {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002149 // TODO(mtomasz): Remove this temporary dialog. crbug.com/167364
2150 var entries = this.getSelection().entries;
2151 var message = entries.length == 1 ?
2152 strf('GALLERY_CONFIRM_DELETE_ONE', entries[0].name) :
2153 strf('GALLERY_CONFIRM_DELETE_SOME', entries.length);
2154 this.confirm.show(message, function() {
2155 this.copyManager_.deleteEntries(entries);
2156 }.bind(this));
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002157 };
2158
Ben Murdocheb525c52013-07-10 11:40:50 +01002159 /**
Ben Murdochca12bfa2013-07-23 11:17:05 +01002160 * Shows the share dialog for the selected file or directory.
Ben Murdocheb525c52013-07-10 11:40:50 +01002161 */
2162 FileManager.prototype.shareSelection = function() {
Ben Murdochca12bfa2013-07-23 11:17:05 +01002163 var entries = this.getSelection().entries;
2164 if (entries.length != 1) {
2165 console.warn('Unable to share multiple items at once.');
2166 return;
2167 }
2168 this.shareDialog_.show(entries[0], function() {
2169 this.error.show(str('SHARE_ERROR'));
2170 }.bind(this));
2171 };
2172
2173 /**
2174 * Folder shared feature is under development and hidden behind flag. This
2175 * method returns if the feature is explicitly enabled by the flag or not.
2176 * TODO(yoshiki): Remove this after launching folder feature feature.
2177 *
2178 * @return {boolena} True if the flag is enabled.
2179 */
2180 FileManager.prototype.isFolderShortcutsEnabled = function() {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01002181 // TODO(yoshiki): Remove this method in M31.
2182 return true;
Ben Murdocheb525c52013-07-10 11:40:50 +01002183 };
2184
2185 /**
Ben Murdochbbcdd452013-07-25 10:06:34 +01002186 * Creates a folder shortcut.
2187 * @param {string} path A shortcut which refers to |path| to be created.
Ben Murdoch7dbb3d52013-07-17 14:55:54 +01002188 */
Ben Murdochbbcdd452013-07-25 10:06:34 +01002189 FileManager.prototype.createFolderShortcut = function(path) {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +01002190 // Duplicate entry.
Ben Murdochbbcdd452013-07-25 10:06:34 +01002191 if (this.folderShortcutExists(path))
Ben Murdoch7dbb3d52013-07-17 14:55:54 +01002192 return;
2193
Ben Murdochbbcdd452013-07-25 10:06:34 +01002194 this.folderShortcutsModel_.add(path);
Ben Murdoch7dbb3d52013-07-17 14:55:54 +01002195 };
2196
2197 /**
Ben Murdochbbcdd452013-07-25 10:06:34 +01002198 * Checkes if the shortcut which refers to the given folder exists or not.
Ben Murdoch7dbb3d52013-07-17 14:55:54 +01002199 * @param {string} path Path of the folder to be checked.
2200 */
Ben Murdochbbcdd452013-07-25 10:06:34 +01002201 FileManager.prototype.folderShortcutExists = function(path) {
Ben Murdochca12bfa2013-07-23 11:17:05 +01002202 return this.folderShortcutsModel_.exists(path);
Ben Murdoch7dbb3d52013-07-17 14:55:54 +01002203 };
2204
2205 /**
Ben Murdochbbcdd452013-07-25 10:06:34 +01002206 * Removes the folder shortcut.
2207 * @param {string} path The shortcut which refers to |path| is to be removed.
Ben Murdoch7dbb3d52013-07-17 14:55:54 +01002208 */
Ben Murdochbbcdd452013-07-25 10:06:34 +01002209 FileManager.prototype.removeFolderShortcut = function(path) {
Ben Murdochca12bfa2013-07-23 11:17:05 +01002210 this.folderShortcutsModel_.remove(path);
Ben Murdoch7dbb3d52013-07-17 14:55:54 +01002211 };
2212
2213 /**
Ben Murdocheb525c52013-07-10 11:40:50 +01002214 * Blinks the selection. Used to give feedback when copying or cutting the
2215 * selection.
2216 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002217 FileManager.prototype.blinkSelection = function() {
2218 var selection = this.getSelection();
2219 if (!selection || selection.totalCount == 0)
2220 return;
2221
2222 for (var i = 0; i < selection.entries.length; i++) {
2223 var selectedIndex = selection.indexes[i];
2224 var listItem = this.currentList_.getListItemByIndex(selectedIndex);
2225 if (listItem)
2226 this.blinkListItem_(listItem);
2227 }
2228 };
2229
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002230 /**
2231 * @param {Element} listItem List item element.
2232 * @private
2233 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002234 FileManager.prototype.blinkListItem_ = function(listItem) {
2235 listItem.classList.add('blink');
2236 setTimeout(function() {
2237 listItem.classList.remove('blink');
2238 }, 100);
2239 };
2240
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002241 /**
2242 * @private
2243 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002244 FileManager.prototype.selectDefaultPathInFilenameInput_ = function() {
2245 var input = this.filenameInput_;
2246 input.focus();
2247 var selectionEnd = input.value.lastIndexOf('.');
2248 if (selectionEnd == -1) {
2249 input.select();
2250 } else {
2251 input.selectionStart = 0;
2252 input.selectionEnd = selectionEnd;
2253 }
2254 // Clear, so we never do this again.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002255 this.defaultPath = '';
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002256 };
2257
2258 /**
2259 * Handles mouse click or tap.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002260 *
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002261 * @param {Event} event The click event.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002262 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002263 */
2264 FileManager.prototype.onDetailClick_ = function(event) {
2265 if (this.isRenamingInProgress()) {
2266 // Don't pay attention to clicks during a rename.
2267 return;
2268 }
2269
2270 var listItem = this.findListItemForEvent_(event);
2271 var selection = this.getSelection();
2272 if (!listItem || !listItem.selected || selection.totalCount != 1) {
2273 return;
2274 }
2275
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01002276 // React on double click, but only if both clicks hit the same item.
2277 // TODO(mtomasz): Simplify it, and use a double click handler if possible.
2278 var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002279 this.lastClickedItem_ = listItem;
2280
2281 if (event.detail != clickNumber)
2282 return;
2283
2284 var entry = selection.entries[0];
2285 if (entry.isDirectory) {
2286 this.onDirectoryAction(entry);
2287 } else {
2288 this.dispatchSelectionAction_();
2289 }
2290 };
2291
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002292 /**
2293 * @private
2294 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002295 FileManager.prototype.dispatchSelectionAction_ = function() {
2296 if (this.dialogType == DialogType.FULL_PAGE) {
2297 var tasks = this.getSelection().tasks;
2298 if (tasks) tasks.executeDefault();
2299 return true;
2300 }
2301 if (!this.okButton_.disabled) {
2302 this.onOk_();
2303 return true;
2304 }
2305 return false;
2306 };
2307
2308 /**
2309 * Executes directory action (i.e. changes directory).
2310 *
2311 * @param {DirectoryEntry} entry Directory entry to which directory should be
2312 * changed.
2313 */
2314 FileManager.prototype.onDirectoryAction = function(entry) {
2315 var mountError = this.volumeManager_.getMountError(
2316 PathUtil.getRootPath(entry.fullPath));
2317 if (mountError == VolumeManager.Error.UNKNOWN_FILESYSTEM) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002318 return this.butterBar_.show(ButterBar.Mode.ERROR,
2319 str('UNKNOWN_FILESYSTEM_WARNING'));
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002320 } else if (mountError == VolumeManager.Error.UNSUPPORTED_FILESYSTEM) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002321 return this.butterBar_.show(ButterBar.Mode.ERROR,
2322 str('UNSUPPORTED_FILESYSTEM_WARNING'));
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002323 }
2324
2325 return this.directoryModel_.changeDirectory(entry.fullPath);
2326 };
2327
2328 /**
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01002329 * Update the window title.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002330 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002331 */
2332 FileManager.prototype.updateTitle_ = function() {
2333 if (this.dialogType != DialogType.FULL_PAGE)
2334 return;
2335
2336 var path = this.getCurrentDirectory();
2337 var rootPath = PathUtil.getRootPath(path);
2338 this.document_.title = PathUtil.getRootLabel(rootPath) +
2339 path.substring(rootPath.length);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002340 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002341
2342 /**
2343 * Updates search box value when directory gets changed.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002344 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002345 */
2346 FileManager.prototype.updateSearchBoxOnDirChange_ = function() {
Torne (Richard Coles)a93a17c2013-05-15 11:34:50 +01002347 if (!this.searchBox_.disabled) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002348 this.searchBox_.value = '';
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +01002349 this.updateSearchBoxStyles_();
Torne (Richard Coles)a93a17c2013-05-15 11:34:50 +01002350 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002351 };
2352
2353 /**
2354 * Update the gear menu.
2355 * @private
2356 */
2357 FileManager.prototype.updateGearMenu_ = function() {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002358 var hideItemsForDrive = !this.isOnDrive();
2359 this.syncButton.hidden = hideItemsForDrive;
2360 this.hostedButton.hidden = hideItemsForDrive;
2361 this.document_.getElementById('drive-separator').hidden =
2362 hideItemsForDrive;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002363
2364 // If volume has changed, then fetch remaining space data.
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002365 if (this.previousRootUrl_ != this.directoryModel_.getCurrentMountPointUrl())
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002366 this.refreshRemainingSpace_(true); // Show loading caption.
2367
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002368 this.previousRootUrl_ = this.directoryModel_.getCurrentMountPointUrl();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002369 };
2370
2371 /**
2372 * Refreshes space info of the current volume.
2373 * @param {boolean} showLoadingCaption Whether show loading caption or not.
2374 * @private
2375 */
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002376 FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002377 var volumeSpaceInfoLabel =
2378 this.dialogDom_.querySelector('#volume-space-info-label');
2379 var volumeSpaceInnerBar =
2380 this.dialogDom_.querySelector('#volume-space-info-bar');
2381 var volumeSpaceOuterBar =
2382 this.dialogDom_.querySelector('#volume-space-info-bar').parentNode;
2383
2384 volumeSpaceInnerBar.setAttribute('pending', '');
2385
2386 if (showLoadingCaption) {
2387 volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO');
2388 volumeSpaceInnerBar.style.width = '100%';
2389 }
2390
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002391 var currentMountPointUrl = this.directoryModel_.getCurrentMountPointUrl();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002392 chrome.fileBrowserPrivate.getSizeStats(
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002393 currentMountPointUrl, function(result) {
2394 if (this.directoryModel_.getCurrentMountPointUrl() !=
2395 currentMountPointUrl)
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002396 return;
2397 updateSpaceInfo(result,
2398 volumeSpaceInnerBar,
2399 volumeSpaceInfoLabel,
2400 volumeSpaceOuterBar);
2401 }.bind(this));
2402 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002403
2404 /**
2405 * Update the UI when the current directory changes.
2406 *
2407 * @param {cr.Event} event The directory-changed event.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002408 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002409 */
2410 FileManager.prototype.onDirectoryChanged_ = function(event) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002411 this.selectionHandler_.onFileSelectionChanged();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002412 this.updateSearchBoxOnDirChange_();
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01002413 util.updateAppState(this.getCurrentDirectory());
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002414
2415 if (this.closeOnUnmount_ && !event.initial &&
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002416 PathUtil.getRootPath(event.previousDirEntry.fullPath) !=
2417 PathUtil.getRootPath(event.newDirEntry.fullPath)) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002418 this.closeOnUnmount_ = false;
2419 }
2420
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01002421 this.updateCommands();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002422 this.updateUnformattedDriveStatus_();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002423 this.updateTitle_();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002424 this.updateGearMenu_();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002425 };
2426
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01002427 /**
2428 * Updates commands' states by emiting canExecute events. Should be used
2429 * only if there is need to reevaluate states without an user action, eg.
2430 * external events.
2431 */
2432 FileManager.prototype.updateCommands = function() {
2433 var commands = this.dialogDom_.querySelectorAll('command');
2434 for (var i = 0; i < commands.length; i++) {
2435 // Commands may not have been decorated yet.
2436 if (commands[i].canExecuteChange)
2437 commands[i].canExecuteChange();
2438 }
2439 };
2440
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002441 // TODO(haruki): Rename this method. "Drive" here does not refer
2442 // "Google Drive".
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002443 FileManager.prototype.updateUnformattedDriveStatus_ = function() {
2444 var volumeInfo = this.volumeManager_.getVolumeInfo_(
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002445 PathUtil.getRootPath(this.directoryModel_.getCurrentRootPath()));
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002446
2447 if (volumeInfo.error) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002448 this.dialogDom_.setAttribute('unformatted', '');
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002449
2450 var errorNode = this.dialogDom_.querySelector('#format-panel > .error');
2451 if (volumeInfo.error == VolumeManager.Error.UNSUPPORTED_FILESYSTEM) {
2452 errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING');
2453 } else {
2454 errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
2455 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002456
2457 // Update 'canExecute' for format command so the format button's disabled
2458 // property is properly set.
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01002459 this.updateCommands();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002460 } else {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002461 this.dialogDom_.removeAttribute('unformatted');
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002462 }
2463 };
2464
2465 FileManager.prototype.findListItemForEvent_ = function(event) {
2466 return this.findListItemForNode_(event.touchedElement || event.srcElement);
2467 };
2468
2469 FileManager.prototype.findListItemForNode_ = function(node) {
2470 var item = this.currentList_.getListItemAncestor(node);
2471 // TODO(serya): list should check that.
2472 return item && this.currentList_.isItem(item) ? item : null;
2473 };
2474
2475 /**
2476 * Unload handler for the page. May be called manually for the file picker
2477 * dialog, because it closes by calling extension API functions that do not
2478 * return.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002479 *
2480 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002481 */
2482 FileManager.prototype.onUnload_ = function() {
Ben Murdoch558790d2013-07-30 15:19:42 +01002483 if (this.directoryModel_)
2484 this.directoryModel_.dispose();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002485 if (this.filePopup_ &&
2486 this.filePopup_.contentWindow &&
Ben Murdochd3868032013-07-31 10:55:33 +01002487 this.filePopup_.contentWindow.unload)
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002488 this.filePopup_.contentWindow.unload(true /* exiting */);
Ben Murdochd3868032013-07-31 10:55:33 +01002489 if (this.butterBar_)
2490 this.butterBar_.dispose();
2491 if (this.copyManager_) {
2492 if (this.onCopyProgressBound_) {
2493 this.copyManager_.removeEventListener(
2494 'copy-progress', this.onCopyProgressBound_);
2495 }
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01002496 if (this.onCopyManagerEntryChangedBound_) {
Ben Murdochd3868032013-07-31 10:55:33 +01002497 this.copyManager_.removeEventListener(
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01002498 'entry-changed', this.onCopyManagerEntryChangedBound_);
Ben Murdochd3868032013-07-31 10:55:33 +01002499 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002500 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002501 };
2502
2503 FileManager.prototype.initiateRename = function() {
2504 var item = this.currentList_.ensureLeadItemExists();
2505 if (!item)
2506 return;
2507 var label = item.querySelector('.filename-label');
2508 var input = this.renameInput_;
2509
2510 input.value = label.textContent;
2511 label.parentNode.setAttribute('renaming', '');
2512 label.parentNode.appendChild(input);
2513 input.focus();
2514 var selectionEnd = input.value.lastIndexOf('.');
2515 if (selectionEnd == -1) {
2516 input.select();
2517 } else {
2518 input.selectionStart = 0;
2519 input.selectionEnd = selectionEnd;
2520 }
2521
2522 // This has to be set late in the process so we don't handle spurious
2523 // blur events.
2524 input.currentEntry = this.currentList_.dataModel.item(item.listIndex);
2525 };
2526
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002527 /**
2528 * @type {Event} Key event.
2529 * @private
2530 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002531 FileManager.prototype.onRenameInputKeyDown_ = function(event) {
2532 if (!this.isRenamingInProgress())
2533 return;
2534
2535 // Do not move selection or lead item in list during rename.
2536 if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') {
2537 event.stopPropagation();
2538 }
2539
2540 switch (util.getKeyModifiers(event) + event.keyCode) {
2541 case '27': // Escape
2542 this.cancelRename_();
2543 event.preventDefault();
2544 break;
2545
2546 case '13': // Enter
2547 this.commitRename_();
2548 event.preventDefault();
2549 break;
2550 }
2551 };
2552
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002553 /**
2554 * @type {Event} Blur event.
2555 * @private
2556 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002557 FileManager.prototype.onRenameInputBlur_ = function(event) {
2558 if (this.isRenamingInProgress() && !this.renameInput_.validation_)
2559 this.commitRename_();
2560 };
2561
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002562 /**
2563 * @private
2564 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002565 FileManager.prototype.commitRename_ = function() {
2566 var input = this.renameInput_;
2567 var entry = input.currentEntry;
2568 var newName = input.value;
2569
2570 if (newName == entry.name) {
2571 this.cancelRename_();
2572 return;
2573 }
2574
2575 var nameNode = this.findListItemForNode_(this.renameInput_).
2576 querySelector('.filename-label');
2577
2578 input.validation_ = true;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002579 var validationDone = function(valid) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002580 input.validation_ = false;
2581 // Alert dialog restores focus unless the item removed from DOM.
2582 if (this.document_.activeElement != input)
2583 this.cancelRename_();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002584 if (!valid)
2585 return;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002586
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002587 // Validation succeeded. Do renaming.
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002588
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002589 this.cancelRename_();
2590 // Optimistically apply new name immediately to avoid flickering in
2591 // case of success.
2592 nameNode.textContent = newName;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002593
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002594 this.directoryModel_.doesExist(entry, newName, function(exists, isFile) {
2595 if (!exists) {
2596 var onError = function(err) {
2597 this.alert.show(strf('ERROR_RENAMING', entry.name,
2598 util.getFileErrorString(err.code)));
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01002599 }.bind(this);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002600 this.directoryModel_.renameEntry(entry, newName, onError.bind(this));
2601 } else {
2602 nameNode.textContent = entry.name;
2603 var message = isFile ? 'FILE_ALREADY_EXISTS' :
2604 'DIRECTORY_ALREADY_EXISTS';
2605 this.alert.show(strf(message, newName));
2606 }
2607 }.bind(this));
2608 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002609
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002610 // TODO(haruki): this.getCurrentDirectoryURL() might not return the actual
2611 // parent if the directory content is a search result. Fix it to do proper
2612 // validation.
2613 this.validateFileName_(this.getCurrentDirectoryURL(),
2614 newName,
2615 validationDone.bind(this));
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002616 };
2617
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002618 /**
2619 * @private
2620 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002621 FileManager.prototype.cancelRename_ = function() {
2622 this.renameInput_.currentEntry = null;
2623
2624 var parent = this.renameInput_.parentNode;
2625 if (parent) {
2626 parent.removeAttribute('renaming');
2627 parent.removeChild(this.renameInput_);
2628 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002629 };
2630
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002631 /**
2632 * @param {Event} Key event.
2633 * @private
2634 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002635 FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
2636 var enabled = this.selectionHandler_.updateOkButton();
2637 if (enabled &&
2638 (util.getKeyModifiers(event) + event.keyCode) == '13' /* Enter */)
2639 this.onOk_();
2640 };
2641
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002642 /**
2643 * @param {Event} Focus event.
2644 * @private
2645 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002646 FileManager.prototype.onFilenameInputFocus_ = function(event) {
2647 var input = this.filenameInput_;
2648
2649 // On focus we want to select everything but the extension, but
2650 // Chrome will select-all after the focus event completes. We
2651 // schedule a timeout to alter the focus after that happens.
2652 setTimeout(function() {
2653 var selectionEnd = input.value.lastIndexOf('.');
2654 if (selectionEnd == -1) {
2655 input.select();
2656 } else {
2657 input.selectionStart = 0;
2658 input.selectionEnd = selectionEnd;
2659 }
2660 }, 0);
2661 };
2662
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002663 /**
2664 * @private
2665 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002666 FileManager.prototype.onScanStarted_ = function() {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002667 if (this.scanInProgress_ && !this.scanUpdatedAtLeastOnceOrCompleted_) {
2668 this.table_.list.endBatchUpdates();
2669 this.grid_.endBatchUpdates();
2670 }
2671
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01002672 this.updateCommands();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002673 this.table_.list.startBatchUpdates();
2674 this.grid_.startBatchUpdates();
2675 this.scanInProgress_ = true;
2676
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002677 this.scanUpdatedAtLeastOnceOrCompleted_ = false;
2678 if (this.scanCompletedTimer_) {
2679 clearTimeout(this.scanCompletedTimer_);
2680 this.scanCompletedTimer_ = null;
2681 }
Ben Murdocheb525c52013-07-10 11:40:50 +01002682
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002683 if (this.scanUpdatedTimer_) {
2684 clearTimeout(this.scanUpdatedTimer_);
2685 this.scanUpdatedTimer_ = null;
2686 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002687
Ben Murdocheb525c52013-07-10 11:40:50 +01002688 if (!this.spinner_.hidden) {
2689 this.cancelSpinnerTimeout_();
2690 this.showSpinner_(false);
2691 this.showSpinnerTimeout_ =
2692 setTimeout(this.showSpinner_.bind(this, true), 500);
2693 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002694 };
2695
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002696 /**
2697 * @private
2698 */
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002699 FileManager.prototype.onScanCompleted_ = function() {
2700 if (!this.scanInProgress_) {
2701 console.error('Scan-completed event recieved. But scan is not started.');
2702 return;
2703 }
2704
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01002705 this.updateCommands();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002706 this.hideSpinnerLater_();
2707 this.refreshCurrentDirectoryMetadata_();
2708
2709 // To avoid flickering postpone updating the ui by a small amount of time.
2710 // There is a high chance, that metadata will be received within 50 ms.
2711 this.scanCompletedTimer_ = setTimeout(function() {
2712 // Check if batch updates are already finished by onScanUpdated_().
2713 if (this.scanUpdatedAtLeastOnceOrCompleted_)
2714 return;
2715 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2716 this.scanInProgress_ = false;
2717 if (this.scanUpdatedTimer_) {
2718 clearTimeout(this.scanUpdatedTimer_);
2719 this.scanUpdatedTimer_ = null;
2720 }
2721 this.table_.list.endBatchUpdates();
2722 this.grid_.endBatchUpdates();
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01002723 this.updateMiddleBarVisibility_();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002724 this.scanCompletedTimer_ = null;
2725 }.bind(this), 50);
2726 };
2727
2728 /**
2729 * @private
2730 */
2731 FileManager.prototype.onScanUpdated_ = function() {
2732 if (!this.scanInProgress_) {
2733 console.error('Scan-updated event recieved. But scan is not started.');
2734 return;
2735 }
2736
2737 // We need to hide the spinner only once.
2738 if (this.scanUpdatedAtLeastOnceOrCompleted_ || this.scanUpdatedTimer_)
2739 return;
2740
2741 // Show contents incrementally by finishing batch updated, but only after
2742 // 200ms elapsed, to avoid flickering when it is not necessary.
2743 this.scanUpdatedTimer_ = setTimeout(function() {
2744 // We need to hide the spinner only once.
2745 if (this.scanUpdatedAtLeastOnceOrCompleted_)
2746 return;
2747 if (this.scanCompletedTimer_) {
2748 clearTimeout(this.scanCompletedTimer_);
2749 this.scanCompletedTimer_ = null;
2750 }
2751 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2752 this.scanInProgress_ = false;
2753 this.hideSpinnerLater_();
2754 this.table_.list.endBatchUpdates();
2755 this.grid_.endBatchUpdates();
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01002756 this.updateMiddleBarVisibility_();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002757 this.scanUpdatedTimer_ = null;
2758 }.bind(this), 200);
2759 };
2760
2761 /**
2762 * @private
2763 */
2764 FileManager.prototype.onScanCancelled_ = function() {
2765 if (!this.scanInProgress_) {
2766 console.error('Scan-cancelled event recieved. But scan is not started.');
2767 return;
2768 }
2769
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01002770 this.updateCommands();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002771 this.hideSpinnerLater_();
2772 if (this.scanCompletedTimer_) {
2773 clearTimeout(this.scanCompletedTimer_);
2774 this.scanCompletedTimer_ = null;
2775 }
2776 if (this.scanUpdatedTimer_) {
2777 clearTimeout(this.scanUpdatedTimer_);
2778 this.scanUpdatedTimer_ = null;
2779 }
2780 // Finish unfinished batch updates.
2781 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2782 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2783 this.scanInProgress_ = false;
2784 this.table_.list.endBatchUpdates();
2785 this.grid_.endBatchUpdates();
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +01002786 this.updateMiddleBarVisibility_();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002787 }
2788 };
2789
2790 /**
2791 * @private
2792 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002793 FileManager.prototype.cancelSpinnerTimeout_ = function() {
2794 if (this.showSpinnerTimeout_) {
2795 clearTimeout(this.showSpinnerTimeout_);
2796 this.showSpinnerTimeout_ = null;
2797 }
2798 };
2799
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002800 /**
2801 * @private
2802 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002803 FileManager.prototype.hideSpinnerLater_ = function() {
2804 this.cancelSpinnerTimeout_();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002805 this.showSpinner_(false);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002806 };
2807
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002808 /**
2809 * @param {boolean} on True to show, false to hide.
2810 * @private
2811 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002812 FileManager.prototype.showSpinner_ = function(on) {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002813 if (on && this.directoryModel_ && this.directoryModel_.isScanning())
2814 this.spinner_.hidden = false;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002815
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002816 if (!on && (!this.directoryModel_ ||
2817 !this.directoryModel_.isScanning() ||
2818 this.directoryModel_.getFileList().length != 0)) {
2819 this.spinner_.hidden = true;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002820 }
2821 };
2822
2823 FileManager.prototype.createNewFolder = function() {
2824 var defaultName = str('DEFAULT_NEW_FOLDER_NAME');
2825
2826 // Find a name that doesn't exist in the data model.
2827 var files = this.directoryModel_.getFileList();
2828 var hash = {};
2829 for (var i = 0; i < files.length; i++) {
2830 var name = files.item(i).name;
2831 // Filtering names prevents from conflicts with prototype's names
2832 // and '__proto__'.
2833 if (name.substring(0, defaultName.length) == defaultName)
2834 hash[name] = 1;
2835 }
2836
2837 var baseName = defaultName;
2838 var separator = '';
2839 var suffix = '';
2840 var index = '';
2841
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002842 var advance = function() {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002843 separator = ' (';
2844 suffix = ')';
2845 index++;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002846 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002847
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002848 var current = function() {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002849 return baseName + separator + index + suffix;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002850 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002851
2852 // Accessing hasOwnProperty is safe since hash properties filtered.
2853 while (hash.hasOwnProperty(current())) {
2854 advance();
2855 }
2856
2857 var self = this;
2858 var list = self.currentList_;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002859 var tryCreate = function() {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002860 self.directoryModel_.createDirectory(current(),
2861 onSuccess, onError);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002862 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002863
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002864 var onSuccess = function(entry) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002865 metrics.recordUserAction('CreateNewFolder');
2866 list.selectedItem = entry;
2867 self.initiateRename();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002868 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002869
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002870 var onError = function(error) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002871 self.alert.show(strf('ERROR_CREATING_FOLDER', current(),
2872 util.getFileErrorString(error.code)));
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002873 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002874
2875 tryCreate();
2876 };
2877
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002878 /**
2879 * @param {Event} event Click event.
2880 * @private
2881 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002882 FileManager.prototype.onDetailViewButtonClick_ = function(event) {
2883 this.setListType(FileManager.ListType.DETAIL);
2884 this.currentList_.focus();
2885 };
2886
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002887 /**
2888 * @param {Event} event Click event.
2889 * @private
2890 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002891 FileManager.prototype.onThumbnailViewButtonClick_ = function(event) {
2892 this.setListType(FileManager.ListType.THUMBNAIL);
2893 this.currentList_.focus();
2894 };
2895
2896 /**
2897 * KeyDown event handler for the document.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002898 * @param {Event} event Key event.
2899 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002900 */
2901 FileManager.prototype.onKeyDown_ = function(event) {
2902 if (event.srcElement === this.renameInput_) {
2903 // Ignore keydown handler in the rename input box.
2904 return;
2905 }
2906
2907 switch (util.getKeyModifiers(event) + event.keyCode) {
2908 case 'Ctrl-17': // Ctrl => Show hidden setting
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002909 this.setCtrlKeyPressed_(true);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002910 return;
2911
2912 case 'Ctrl-190': // Ctrl-. => Toggle filter files.
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002913 this.fileFilter_.setFilterHidden(
2914 !this.fileFilter_.isFilterHiddenOn());
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002915 event.preventDefault();
2916 return;
2917
2918 case '27': // Escape => Cancel dialog.
Ben Murdocha3f7b4e2013-07-24 10:36:34 +01002919 if (this.copyManager_ && this.copyManager_.isRunning()) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002920 // If there is a copy in progress, ESC will cancel it.
2921 event.preventDefault();
2922 this.copyManager_.requestCancel();
2923 return;
2924 }
2925
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002926 if (this.dialogType != DialogType.FULL_PAGE) {
2927 // If there is nothing else for ESC to do, then cancel the dialog.
2928 event.preventDefault();
2929 this.cancelButton_.click();
2930 }
2931 break;
2932 }
2933 };
2934
2935 /**
2936 * KeyUp event handler for the document.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002937 * @param {Event} event Key event.
2938 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002939 */
2940 FileManager.prototype.onKeyUp_ = function(event) {
2941 if (event.srcElement === this.renameInput_) {
2942 // Ignore keydown handler in the rename input box.
2943 return;
2944 }
2945
2946 switch (util.getKeyModifiers(event) + event.keyCode) {
2947 case '17': // Ctrl => Hide hidden setting
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002948 this.setCtrlKeyPressed_(false);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002949 return;
2950 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002951 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002952
2953 /**
2954 * KeyDown event handler for the div#list-container element.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002955 * @param {Event} event Key event.
2956 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002957 */
2958 FileManager.prototype.onListKeyDown_ = function(event) {
2959 if (event.srcElement.tagName == 'INPUT') {
2960 // Ignore keydown handler in the rename input box.
2961 return;
2962 }
2963
2964 switch (util.getKeyModifiers(event) + event.keyCode) {
2965 case '8': // Backspace => Up one directory.
2966 event.preventDefault();
2967 var path = this.getCurrentDirectory();
2968 if (path && !PathUtil.isRootPath(path)) {
2969 var path = path.replace(/\/[^\/]+$/, '');
2970 this.directoryModel_.changeDirectory(path);
2971 }
2972 break;
2973
2974 case '13': // Enter => Change directory or perform default action.
2975 // TODO(dgozman): move directory action to dispatchSelectionAction.
2976 var selection = this.getSelection();
2977 if (selection.totalCount == 1 &&
2978 selection.entries[0].isDirectory &&
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01002979 this.dialogType != DialogType.SELECT_FOLDER &&
2980 this.dialogType != DialogType.SELECT_UPLOAD_FOLDER) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002981 event.preventDefault();
2982 this.onDirectoryAction(selection.entries[0]);
2983 } else if (this.dispatchSelectionAction_()) {
2984 event.preventDefault();
2985 }
2986 break;
2987 }
2988
2989 switch (event.keyIdentifier) {
2990 case 'Home':
2991 case 'End':
2992 case 'Up':
2993 case 'Down':
2994 case 'Left':
2995 case 'Right':
2996 // When navigating with keyboard we hide the distracting mouse hover
2997 // highlighting until the user moves the mouse again.
2998 this.setNoHover_(true);
2999 break;
3000 }
3001 };
3002
3003 /**
3004 * Suppress/restore hover highlighting in the list container.
3005 * @param {boolean} on True to temporarity hide hover state.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003006 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003007 */
3008 FileManager.prototype.setNoHover_ = function(on) {
3009 if (on) {
3010 this.listContainer_.classList.add('nohover');
3011 } else {
3012 this.listContainer_.classList.remove('nohover');
3013 }
3014 };
3015
3016 /**
3017 * KeyPress event handler for the div#list-container element.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003018 * @param {Event} event Key event.
3019 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003020 */
3021 FileManager.prototype.onListKeyPress_ = function(event) {
3022 if (event.srcElement.tagName == 'INPUT') {
3023 // Ignore keypress handler in the rename input box.
3024 return;
3025 }
3026
3027 if (event.ctrlKey || event.metaKey || event.altKey)
3028 return;
3029
3030 var now = new Date();
3031 var char = String.fromCharCode(event.charCode).toLowerCase();
3032 var text = now - this.textSearchState_.date > 1000 ? '' :
3033 this.textSearchState_.text;
3034 this.textSearchState_ = {text: text + char, date: now};
3035
3036 this.doTextSearch_();
3037 };
3038
3039 /**
3040 * Mousemove event handler for the div#list-container element.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003041 * @param {Event} event Mouse event.
3042 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003043 */
3044 FileManager.prototype.onListMouseMove_ = function(event) {
3045 // The user grabbed the mouse, restore the hover highlighting.
3046 this.setNoHover_(false);
3047 };
3048
3049 /**
3050 * Performs a 'text search' - selects a first list entry with name
3051 * starting with entered text (case-insensitive).
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003052 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003053 */
3054 FileManager.prototype.doTextSearch_ = function() {
3055 var text = this.textSearchState_.text;
3056 if (!text)
3057 return;
3058
3059 var dm = this.directoryModel_.getFileList();
3060 for (var index = 0; index < dm.length; ++index) {
3061 var name = dm.item(index).name;
3062 if (name.substring(0, text.length).toLowerCase() == text) {
3063 this.currentList_.selectionModel.selectedIndexes = [index];
3064 return;
3065 }
3066 }
3067
3068 this.textSearchState_.text = '';
3069 };
3070
3071 /**
3072 * Handle a click of the cancel button. Closes the window.
3073 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3074 *
3075 * @param {Event} event The click event.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003076 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003077 */
3078 FileManager.prototype.onCancel_ = function(event) {
3079 chrome.fileBrowserPrivate.cancelDialog();
3080 this.onUnload_();
3081 window.close();
3082 };
3083
3084 /**
3085 * Resolves selected file urls returned from an Open dialog.
3086 *
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003087 * For drive files this involves some special treatment.
3088 * Starts getting drive files if needed.
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003089 *
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003090 * @param {Array.<string>} fileUrls Drive URLs.
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003091 * @param {function(Array.<string>)} callback To be called with fixed URLs.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003092 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003093 */
3094 FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003095 if (this.isOnDrive()) {
3096 chrome.fileBrowserPrivate.getDriveFiles(
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003097 fileUrls,
3098 function(localPaths) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003099 callback(fileUrls);
3100 });
3101 } else {
3102 callback(fileUrls);
3103 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003104 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003105
3106 /**
3107 * Closes this modal dialog with some files selected.
3108 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3109 * @param {Object} selection Contains urls, filterIndex and multiple fields.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003110 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003111 */
3112 FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003113 var self = this;
3114 function callback() {
3115 self.onUnload_();
3116 window.close();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003117 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003118 if (selection.multiple) {
3119 chrome.fileBrowserPrivate.selectFiles(
3120 selection.urls, this.params_.shouldReturnLocalPath, callback);
3121 } else {
3122 var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE);
3123 chrome.fileBrowserPrivate.selectFile(
3124 selection.urls[0], selection.filterIndex, forOpening,
3125 this.params_.shouldReturnLocalPath, callback);
3126 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003127 };
3128
3129 /**
3130 * Tries to close this modal dialog with some files selected.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003131 * Performs preprocessing if needed (e.g. for Drive).
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003132 * @param {Object} selection Contains urls, filterIndex and multiple fields.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003133 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003134 */
3135 FileManager.prototype.selectFilesAndClose_ = function(selection) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003136 if (!this.isOnDrive() ||
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003137 this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3138 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
3139 return;
3140 }
3141
3142 var shade = this.document_.createElement('div');
3143 shade.className = 'shade';
Ben Murdocheb525c52013-07-10 11:40:50 +01003144 var footer = this.dialogDom_.querySelector('.button-panel');
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003145 var progress = footer.querySelector('.progress-track');
3146 progress.style.width = '0%';
3147 var cancelled = false;
3148
3149 var progressMap = {};
3150 var filesStarted = 0;
3151 var filesTotal = selection.urls.length;
3152 for (var index = 0; index < selection.urls.length; index++) {
3153 progressMap[selection.urls[index]] = -1;
3154 }
3155 var lastPercent = 0;
3156 var bytesTotal = 0;
3157 var bytesDone = 0;
3158
3159 var onFileTransfersUpdated = function(statusList) {
3160 for (var index = 0; index < statusList.length; index++) {
3161 var status = statusList[index];
3162 var escaped = encodeURI(status.fileUrl);
3163 if (!(escaped in progressMap)) continue;
3164 if (status.total == -1) continue;
3165
3166 var old = progressMap[escaped];
3167 if (old == -1) {
3168 // -1 means we don't know file size yet.
3169 bytesTotal += status.total;
3170 filesStarted++;
3171 old = 0;
3172 }
3173 bytesDone += status.processed - old;
3174 progressMap[escaped] = status.processed;
3175 }
3176
3177 var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal;
3178 // For files we don't have information about, assume the progress is zero.
3179 percent = percent * filesStarted / filesTotal * 100;
3180 // Do not decrease the progress. This may happen, if first downloaded
3181 // file is small, and the second one is large.
3182 lastPercent = Math.max(lastPercent, percent);
3183 progress.style.width = lastPercent + '%';
3184 }.bind(this);
3185
3186 var setup = function() {
3187 this.document_.querySelector('.dialog-container').appendChild(shade);
3188 setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100);
3189 footer.setAttribute('progress', 'progress');
3190 this.cancelButton_.removeEventListener('click', this.onCancelBound_);
3191 this.cancelButton_.addEventListener('click', onCancel);
3192 chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener(
3193 onFileTransfersUpdated);
3194 }.bind(this);
3195
3196 var cleanup = function() {
3197 shade.parentNode.removeChild(shade);
3198 footer.removeAttribute('progress');
3199 this.cancelButton_.removeEventListener('click', onCancel);
3200 this.cancelButton_.addEventListener('click', this.onCancelBound_);
3201 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
3202 onFileTransfersUpdated);
3203 }.bind(this);
3204
3205 var onCancel = function() {
3206 cancelled = true;
3207 // According to API cancel may fail, but there is no proper UI to reflect
3208 // this. So, we just silently assume that everything is cancelled.
3209 chrome.fileBrowserPrivate.cancelFileTransfers(
3210 selection.urls, function(response) {});
3211 cleanup();
3212 }.bind(this);
3213
3214 var onResolved = function(resolvedUrls) {
3215 if (cancelled) return;
3216 cleanup();
3217 selection.urls = resolvedUrls;
3218 // Call next method on a timeout, as it's unsafe to
3219 // close a window from a callback.
3220 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
3221 }.bind(this);
3222
3223 var onProperties = function(properties) {
3224 for (var i = 0; i < properties.length; i++) {
3225 if (!properties[i] || properties[i].present) {
3226 // For files already in GCache, we don't get any transfer updates.
3227 filesTotal--;
3228 }
3229 }
3230 this.resolveSelectResults_(selection.urls, onResolved);
3231 }.bind(this);
3232
3233 setup();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003234 this.metadataCache_.get(selection.urls, 'drive', onProperties);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003235 };
3236
3237 /**
3238 * Handle a click of the ok button.
3239 *
3240 * The ok button has different UI labels depending on the type of dialog, but
3241 * in code it's always referred to as 'ok'.
3242 *
3243 * @param {Event} event The click event.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003244 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003245 */
3246 FileManager.prototype.onOk_ = function(event) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003247 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003248 // Save-as doesn't require a valid selection from the list, since
3249 // we're going to take the filename from the text input.
3250 var filename = this.filenameInput_.value;
3251 if (!filename)
3252 throw new Error('Missing filename!');
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003253
Ben Murdoch2385ea32013-08-06 11:01:04 +01003254 var directory = this.getCurrentDirectoryEntry();
3255 var currentDirUrl = directory.toURL();
3256 if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
3257 currentDirUrl += '/';
3258 this.validateFileName_(currentDirUrl, filename, function(isValid) {
3259 if (!isValid)
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003260 return;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003261
Ben Murdoch2385ea32013-08-06 11:01:04 +01003262 if (util.isFakeDirectoryEntry(directory)) {
3263 // Can't save a file into a fake directory.
3264 return;
3265 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003266
Ben Murdoch2385ea32013-08-06 11:01:04 +01003267 var selectFileAndClose = function() {
3268 this.selectFilesAndClose_({
3269 urls: [currentDirUrl + encodeURIComponent(filename)],
3270 multiple: false,
3271 filterIndex: this.getSelectedFilterIndex_(filename)
3272 });
3273 }.bind(this);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003274
Ben Murdoch2385ea32013-08-06 11:01:04 +01003275 directory.getFile(
3276 filename, {create: false},
3277 function(entry) {
3278 // An existing file is found. Show confirmation dialog to
3279 // overwrite it. If the user select "OK" on the dialog, save it.
3280 this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename),
3281 selectFileAndClose);
3282 }.bind(this),
3283 function(error) {
3284 if (error.code == FileError.NOT_FOUND_ERR) {
3285 // The file does not exist, so it should be ok to create a
3286 // new file.
3287 selectFileAndClose();
3288 return;
3289 }
3290 if (error.code == FileError.TYPE_MISMATCH_ERR) {
3291 // An directory is found.
3292 // Do not allow to overwrite directory.
3293 this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename));
3294 return;
3295 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003296
Ben Murdoch2385ea32013-08-06 11:01:04 +01003297 // Unexpected error.
3298 console.error('File save failed: ' + error.code);
3299 }.bind(this));
3300 }.bind(this));
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003301 return;
3302 }
3303
3304 var files = [];
3305 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
3306
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01003307 if ((this.dialogType == DialogType.SELECT_FOLDER ||
3308 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER) &&
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003309 selectedIndexes.length == 0) {
3310 var url = this.getCurrentDirectoryURL();
3311 var singleSelection = {
3312 urls: [url],
3313 multiple: false,
3314 filterIndex: this.getSelectedFilterIndex_()
3315 };
3316 this.selectFilesAndClose_(singleSelection);
3317 return;
3318 }
3319
3320 // All other dialog types require at least one selected list item.
3321 // The logic to control whether or not the ok button is enabled should
3322 // prevent us from ever getting here, but we sanity check to be sure.
3323 if (!selectedIndexes.length)
3324 throw new Error('Nothing selected!');
3325
3326 var dm = this.directoryModel_.getFileList();
3327 for (var i = 0; i < selectedIndexes.length; i++) {
3328 var entry = dm.item(selectedIndexes[i]);
3329 if (!entry) {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003330 console.error('Error locating selected file at index: ' + i);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003331 continue;
3332 }
3333
3334 files.push(entry.toURL());
3335 }
3336
3337 // Multi-file selection has no other restrictions.
3338 if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
3339 var multipleSelection = {
3340 urls: files,
3341 multiple: true
3342 };
3343 this.selectFilesAndClose_(multipleSelection);
3344 return;
3345 }
3346
3347 // Everything else must have exactly one.
3348 if (files.length > 1)
3349 throw new Error('Too many files selected!');
3350
3351 var selectedEntry = dm.item(selectedIndexes[0]);
3352
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01003353 if (this.dialogType == DialogType.SELECT_FOLDER ||
3354 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003355 if (!selectedEntry.isDirectory)
3356 throw new Error('Selected entry is not a folder!');
3357 } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) {
3358 if (!selectedEntry.isFile)
3359 throw new Error('Selected entry is not a file!');
3360 }
3361
3362 var singleSelection = {
3363 urls: [files[0]],
3364 multiple: false,
3365 filterIndex: this.getSelectedFilterIndex_()
3366 };
3367 this.selectFilesAndClose_(singleSelection);
3368 };
3369
3370 /**
3371 * Verifies the user entered name for file or folder to be created or
3372 * renamed to. Name restrictions must correspond to File API restrictions
3373 * (see DOMFilePath::isValidPath). Curernt WebKit implementation is
3374 * out of date (spec is
3375 * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to
3376 * be fixed. Shows message box if the name is invalid.
3377 *
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003378 * It also verifies if the name length is in the limit of the filesystem.
3379 *
3380 * @param {string} parentUrl The URL of the parent directory entry.
3381 * @param {string} name New file or folder name.
3382 * @param {function} onDone Function to invoke when user closes the
3383 * warning box or immediatelly if file name is correct. If the name was
3384 * valid it is passed true, and false otherwise.
3385 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003386 */
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003387 FileManager.prototype.validateFileName_ = function(parentUrl, name, onDone) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003388 var msg;
3389 var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name);
3390 if (testResult) {
3391 msg = strf('ERROR_INVALID_CHARACTER', testResult[0]);
3392 } else if (/^\s*$/i.test(name)) {
3393 msg = str('ERROR_WHITESPACE_NAME');
3394 } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) {
3395 msg = str('ERROR_RESERVED_NAME');
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003396 } else if (this.fileFilter_.isFilterHiddenOn() && name[0] == '.') {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003397 msg = str('ERROR_HIDDEN_NAME');
3398 }
3399
3400 if (msg) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003401 this.alert.show(msg, function() {
3402 onDone(false);
3403 });
3404 return;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003405 }
3406
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003407 var self = this;
3408 chrome.fileBrowserPrivate.validatePathNameLength(
3409 parentUrl, name, function(valid) {
3410 if (!valid) {
3411 self.alert.show(str('ERROR_LONG_NAME'),
3412 function() { onDone(false); });
3413 } else {
3414 onDone(true);
3415 }
3416 });
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003417 };
3418
3419 /**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003420 * Handler invoked on preference setting in drive context menu.
3421 *
3422 * @param {string} pref The preference to alter.
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003423 * @param {boolean} inverted Invert the value if true.
3424 * @param {Event} event The click event.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003425 * @private
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003426 */
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003427 FileManager.prototype.onDrivePrefClick_ = function(pref, inverted, event) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003428 var newValue = !event.target.hasAttribute('checked');
3429 if (newValue)
3430 event.target.setAttribute('checked', 'checked');
3431 else
3432 event.target.removeAttribute('checked');
3433
3434 var changeInfo = {};
3435 changeInfo[pref] = inverted ? !newValue : newValue;
3436 chrome.fileBrowserPrivate.setPreferences(changeInfo);
3437 };
3438
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003439 /**
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003440 * Invoked when the search box is changed.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003441 *
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +01003442 * @param {Event} event The changed event.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003443 * @private
3444 */
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003445 FileManager.prototype.onSearchBoxUpdate_ = function(event) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003446 var searchString = this.searchBox_.value;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003447
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +01003448 this.updateSearchBoxStyles_();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003449 if (this.isOnDrive()) {
3450 // When the search text is changed, finishes the search and showes back
3451 // the last directory by passing an empty string to
3452 // {@code DirectoryModel.search()}.
3453 if (this.directoryModel_.isSearching() &&
3454 this.lastSearchQuery_ != searchString) {
3455 this.doSearch('');
3456 }
3457
3458 // On drive, incremental search is not invoked since we have an auto-
3459 // complete suggestion instead.
3460 return;
3461 }
3462
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003463 this.search_(searchString);
3464 };
3465
3466 /**
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +01003467 * Handles special keys such as Escape on the search box.
3468 *
3469 * @param {Event} event The keydown event.
3470 * @private
3471 */
3472 FileManager.prototype.onSearchBoxKeyDown_ = function(event) {
3473 // Handle only Esc key now.
3474 if (event.keyCode != 27) return;
3475 if (this.searchBox_.value) return;
3476 var currentList = this.listType_ == FileManager.ListType.DETAIL ?
3477 this.table_.list : this.grid_;
3478 currentList.focus();
3479 if (currentList.dataModel.length != 0 &&
3480 currentList.selectionModel.selectedIndex == -1) {
3481 currentList.selectionModel.selectedIndex = 0;
3482 }
3483 };
3484
3485 /**
Torne (Richard Coles)a93a17c2013-05-15 11:34:50 +01003486 * Updates search box's CSS classes.
3487 * These classes are refered from CSS.
3488 *
3489 * @private
3490 */
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +01003491 FileManager.prototype.updateSearchBoxStyles_ = function() {
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +01003492 var TEXT_BOX_PADDING = 16; // in px.
3493 this.searchBoxWrapper_.classList.toggle('has-text',
3494 !!this.searchBox_.value);
3495 var width = this.searchTextMeasure_.getWidth(this.searchBox_.value) +
3496 TEXT_BOX_PADDING;
3497 this.searchBox_.style.width = width + 'px';
Torne (Richard Coles)a93a17c2013-05-15 11:34:50 +01003498 };
3499
3500 /**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003501 * Search files and update the list with the search result.
3502 *
3503 * @param {string} searchString String to be searched with.
3504 * @private
3505 */
3506 FileManager.prototype.search_ = function(searchString) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003507 var noResultsDiv = this.document_.getElementById('no-search-results');
3508
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003509 var reportEmptySearchResults = function() {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003510 if (this.directoryModel_.getFileList().length === 0) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003511 // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags,
3512 // hence we escapes |searchString| here.
3513 var html = strf('SEARCH_NO_MATCHING_FILES_HTML',
3514 util.htmlEscape(searchString));
3515 noResultsDiv.innerHTML = html;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003516 noResultsDiv.setAttribute('show', 'true');
3517 } else {
3518 noResultsDiv.removeAttribute('show');
3519 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003520 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003521
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003522 var hideNoResultsDiv = function() {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003523 noResultsDiv.removeAttribute('show');
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003524 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003525
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003526 this.doSearch(searchString,
3527 reportEmptySearchResults.bind(this),
3528 hideNoResultsDiv.bind(this));
3529 };
3530
3531 /**
3532 * Performs search and displays results.
3533 *
3534 * @param {string} query Query that will be searched for.
3535 * @param {function()=} opt_onSearchRescan Function that will be called when
3536 * the search directory is rescanned (i.e. search results are displayed).
3537 * @param {function()=} opt_onClearSearch Function to be called when search
3538 * state gets cleared.
3539 */
3540 FileManager.prototype.doSearch = function(
3541 searchString, opt_onSearchRescan, opt_onClearSearch) {
3542 var onSearchRescan = opt_onSearchRescan || function() {};
3543 var onClearSearch = opt_onClearSearch || function() {};
3544
3545 this.lastSearchQuery_ = searchString;
3546 this.directoryModel_.search(searchString, onSearchRescan, onClearSearch);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003547 };
3548
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003549 /**
3550 * Requests autocomplete suggestions for files on Drive.
3551 * Once the suggestions are returned, the autocomplete popup will show up.
3552 *
3553 * @param {string} query The text to autocomplete from.
3554 * @private
3555 */
3556 FileManager.prototype.requestAutocompleteSuggestions_ = function(query) {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003557 query = query.trimLeft();
3558
3559 // Only Drive supports auto-compelete
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003560 if (!this.isOnDrive())
3561 return;
3562
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003563 // Remember the most recent query. If there is an other request in progress,
3564 // then it's result will be discarded and it will call a new request for
3565 // this query.
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01003566 this.lastAutocompleteQuery_ = query;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003567 if (this.autocompleteSuggestionsBusy_)
3568 return;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003569
3570 // The autocomplete list should be resized and repositioned here as the
3571 // search box is resized when it's focused.
3572 this.autocompleteList_.syncWidthAndPositionToInput();
3573
3574 if (!query) {
3575 this.autocompleteList_.suggestions = [];
3576 return;
3577 }
3578
3579 var headerItem = {isHeaderItem: true, searchQuery: query};
3580 if (!this.autocompleteList_.dataModel ||
3581 this.autocompleteList_.dataModel.length == 0)
3582 this.autocompleteList_.suggestions = [headerItem];
3583 else
3584 // Updates only the head item to prevent a flickering on typing.
3585 this.autocompleteList_.dataModel.splice(0, 1, headerItem);
3586
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003587 this.autocompleteSuggestionsBusy_ = true;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003588
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003589 var searchParams = {
3590 'query': query,
3591 'types': 'ALL',
3592 'maxResults': 4
3593 };
3594 chrome.fileBrowserPrivate.searchDriveMetadata(
3595 searchParams,
3596 function(suggestions) {
3597 this.autocompleteSuggestionsBusy_ = false;
3598
3599 // Discard results for previous requests and fire a new search
3600 // for the most recent query.
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01003601 if (query != this.lastAutocompleteQuery_) {
3602 this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003603 return;
3604 }
3605
3606 // Keeps the items in the suggestion list.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003607 this.autocompleteList_.suggestions = [headerItem].concat(suggestions);
3608 }.bind(this));
3609 };
3610
3611 /**
3612 * Creates a ListItem element for autocomple.
3613 *
3614 * @param {Object} item An object representing a suggestion.
3615 * @return {HTMLElement} Element containing the autocomplete suggestions.
3616 * @private
3617 */
3618 FileManager.prototype.createAutocompleteListItem_ = function(item) {
3619 var li = new cr.ui.ListItem();
3620 li.itemInfo = item;
3621
3622 var icon = this.document_.createElement('div');
3623 icon.className = 'detail-icon';
3624
3625 var text = this.document_.createElement('div');
3626 text.className = 'detail-text';
3627
3628 if (item.isHeaderItem) {
3629 icon.setAttribute('search-icon');
3630 text.innerHTML =
3631 strf('SEARCH_DRIVE_HTML', util.htmlEscape(item.searchQuery));
3632 } else {
3633 var iconType = FileType.getIcon(item.entry);
3634 icon.setAttribute('file-type-icon', iconType);
3635 // highlightedBaseName is a piece of HTML with meta characters properly
3636 // escaped. See the comment at fileBrowserPrivate.searchDriveMetadata().
3637 text.innerHTML = item.highlightedBaseName;
3638 }
3639 li.appendChild(icon);
3640 li.appendChild(text);
3641 return li;
3642 };
3643
3644 /**
3645 * Opens the currently selected suggestion item.
3646 * @private
3647 */
3648 FileManager.prototype.openAutocompleteSuggestion_ = function() {
3649 var selectedItem = this.autocompleteList_.selectedItem;
3650
3651 // If the entry is the search item or no entry is selected, just change to
3652 // the search result.
3653 if (!selectedItem || selectedItem.isHeaderItem) {
3654 var query = selectedItem ?
3655 selectedItem.searchQuery : this.searchBox_.value;
3656 this.search_(query);
3657 return;
3658 }
3659
3660 var entry = selectedItem.entry;
3661 // If the entry is a directory, just change the directory.
3662 if (entry.isDirectory) {
3663 this.onDirectoryAction(entry);
3664 return;
3665 }
3666
3667 var urls = [entry.toURL()];
3668 var self = this;
3669
3670 // To open a file, first get the mime type.
3671 this.metadataCache_.get(urls, 'drive', function(props) {
3672 var mimeType = props[0].contentMimeType || '';
3673 var mimeTypes = [mimeType];
3674 var openIt = function() {
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +01003675 if (self.dialogType == DialogType.FULL_PAGE) {
3676 var tasks = new FileTasks(self);
3677 tasks.init(urls, mimeTypes);
3678 tasks.executeDefault();
3679 } else {
3680 self.onOk_();
3681 }
3682 };
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003683
3684 // Change the current directory to the directory that contains the
3685 // selected file. Note that this is necessary for an image or a video,
3686 // which should be opened in the gallery mode, as the gallery mode
3687 // requires the entry to be in the current directory model. For
3688 // consistency, the current directory is always changed regardless of
3689 // the file type.
3690 entry.getParent(function(parent) {
3691 var onDirectoryChanged = function(event) {
3692 self.directoryModel_.removeEventListener('scan-completed',
3693 onDirectoryChanged);
3694 self.directoryModel_.selectEntry(entry.name);
3695 openIt();
3696 }
3697 // changeDirectory() returns immediately. We should wait until the
3698 // directory scan is complete.
3699 self.directoryModel_.addEventListener('scan-completed',
3700 onDirectoryChanged);
3701 self.directoryModel_.changeDirectory(
3702 parent.fullPath,
3703 function() {
3704 // Remove the listner if the change directory failed.
3705 self.directoryModel_.removeEventListener('scan-completed',
3706 onDirectoryChanged);
3707 });
3708 });
3709 });
3710 };
3711
3712 /**
3713 * Opens the default app change dialog.
3714 */
3715 FileManager.prototype.showChangeDefaultAppPicker = function() {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003716 var onActionsReady = function(actions, rememberedActionId) {
3717 var items = [];
3718 var defaultIndex = -1;
3719 for (var i = 0; i < actions.length; i++) {
3720 if (actions[i].hidden)
3721 continue;
3722 var title = actions[i].title;
3723 if (actions[i].id == rememberedActionId) {
3724 title += ' ' + loadTimeData.getString('DEFAULT_ACTION_LABEL');
3725 defaultIndex = i;
3726 }
3727 var item = {
3728 id: actions[i].id,
3729 label: title,
3730 class: actions[i].class,
3731 iconUrl: actions[i].icon100
3732 };
3733 items.push(item);
3734 }
3735 this.defaultTaskPicker.show(
3736 str('CHANGE_DEFAULT_APP_BUTTON_LABEL'),
3737 '',
3738 items,
3739 defaultIndex,
3740 function(action) {
3741 ActionChoiceUtil.setRememberedActionId(action.id);
3742 });
3743 }.bind(this);
3744
3745 ActionChoiceUtil.getDefinedActions(loadTimeData, function(actions) {
3746 ActionChoiceUtil.getRememberedActionId(function(actionId) {
3747 onActionsReady(actions, actionId);
3748 });
3749 });
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003750 };
3751
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003752 FileManager.prototype.decorateSplitter = function(splitterElement) {
3753 var self = this;
3754
3755 var Splitter = cr.ui.Splitter;
3756
3757 var customSplitter = cr.ui.define('div');
3758
3759 customSplitter.prototype = {
3760 __proto__: Splitter.prototype,
3761
3762 handleSplitterDragStart: function(e) {
3763 Splitter.prototype.handleSplitterDragStart.apply(this, arguments);
3764 this.ownerDocument.documentElement.classList.add('col-resize');
3765 },
3766
3767 handleSplitterDragMove: function(deltaX) {
3768 Splitter.prototype.handleSplitterDragMove.apply(this, arguments);
3769 self.onResize_();
3770 },
3771
3772 handleSplitterDragEnd: function(e) {
3773 Splitter.prototype.handleSplitterDragEnd.apply(this, arguments);
3774 this.ownerDocument.documentElement.classList.remove('col-resize');
3775 }
3776 };
3777
3778 customSplitter.decorate(splitterElement);
3779 };
3780
3781 /**
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003782 * Updates default action menu item to match passed taskItem (icon,
3783 * label and action).
3784 *
3785 * @param {Object} defaultItem - taskItem to match.
3786 * @param {boolean} isMultiple - if multiple tasks available.
3787 */
3788 FileManager.prototype.updateContextMenuActionItems = function(defaultItem,
3789 isMultiple) {
3790 if (defaultItem) {
3791 if (defaultItem.iconType) {
3792 this.defaultActionMenuItem_.style.backgroundImage = '';
3793 this.defaultActionMenuItem_.setAttribute('file-type-icon',
3794 defaultItem.iconType);
3795 } else if (defaultItem.iconUrl) {
3796 this.defaultActionMenuItem_.style.backgroundImage =
3797 'url(' + defaultItem.iconUrl + ')';
3798 } else {
3799 this.defaultActionMenuItem_.style.backgroundImage = '';
3800 }
3801
3802 this.defaultActionMenuItem_.label = defaultItem.title;
3803 this.defaultActionMenuItem_.taskId = defaultItem.taskId;
3804 }
3805
3806 var defaultActionSeparator =
3807 this.dialogDom_.querySelector('#default-action-separator');
3808
3809 this.openWithCommand_.canExecuteChange();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003810 this.openWithCommand_.setHidden(!(defaultItem && isMultiple));
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003811 this.defaultActionMenuItem_.hidden = !defaultItem;
3812 defaultActionSeparator.hidden = !defaultItem;
3813 };
3814
3815
3816 /**
3817 * Window beforeunload handler.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003818 * @return {string} Message to show. Ignored when running as a packaged app.
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003819 * @private
3820 */
3821 FileManager.prototype.onBeforeUnload_ = function() {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003822 if (this.filePopup_ &&
3823 this.filePopup_.contentWindow &&
3824 this.filePopup_.contentWindow.beforeunload) {
3825 // The gallery might want to prevent the unload if it is busy.
3826 return this.filePopup_.contentWindow.beforeunload();
3827 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003828 return null;
3829 };
3830
3831 /**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003832 * @return {FileSelection} Selection object.
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003833 */
3834 FileManager.prototype.getSelection = function() {
3835 return this.selectionHandler_.selection;
3836 };
3837
3838 /**
3839 * @return {ArrayDataModel} File list.
3840 */
3841 FileManager.prototype.getFileList = function() {
3842 return this.directoryModel_.getFileList();
3843 };
3844
3845 /**
3846 * @return {cr.ui.List} Current list object.
3847 */
3848 FileManager.prototype.getCurrentList = function() {
3849 return this.currentList_;
3850 };
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00003851
3852 /**
3853 * Retrieve the preferences of the files.app. This method caches the result
3854 * and returns it unless opt_update is true.
3855 * @param {function(Object.<string, *>)} callback Callback to get the
3856 * preference.
3857 * @param {boolean=} opt_update If is's true, don't use the cache and
3858 * retrieve latest preference. Default is false.
3859 * @private
3860 */
3861 FileManager.prototype.getPreferences_ = function(callback, opt_update) {
3862 if (!opt_update && this.preferences_ !== undefined) {
3863 callback(this.preferences_);
3864 return;
3865 }
3866
3867 chrome.fileBrowserPrivate.getPreferences(function(prefs) {
3868 this.preferences_ = prefs;
3869 callback(prefs);
3870 }.bind(this));
3871 };
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003872
3873 /**
3874 * Set the flag expressing whether the ctrl key is pressed or not.
3875 * @param {boolean} flag New value of the flag
3876 * @private
3877 */
3878 FileManager.prototype.setCtrlKeyPressed_ = function(flag) {
3879 this.ctrlKeyPressed_ = flag;
Ben Murdochca12bfa2013-07-23 11:17:05 +01003880 // Before the DOM is constructed, the key event can be handled.
3881 var cacheClearCommand =
3882 this.document_.querySelector('#drive-clear-local-cache');
3883 if (cacheClearCommand)
3884 cacheClearCommand.canExecuteChange();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01003885 };
Torne (Richard Coles)58218062012-11-14 11:43:16 +00003886})();