2 * Copyright (C) 2011 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above
12 * copyright notice, this list of conditions and the following disclaimer
13 * in the documentation and/or other materials provided with the
16 * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. AND ITS CONTRIBUTORS
17 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE INC.
20 * OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 WebInspector.TabbedEditorContainerDelegate = function() { }
34 WebInspector.TabbedEditorContainerDelegate.prototype = {
36 * @param {!WebInspector.UISourceCode} uiSourceCode
37 * @return {!WebInspector.SourceFrame}
39 viewForFile: function(uiSourceCode) { },
44 * @extends {WebInspector.Object}
45 * @param {!WebInspector.TabbedEditorContainerDelegate} delegate
46 * @param {!WebInspector.Setting} setting
47 * @param {string} placeholderText
49 WebInspector.TabbedEditorContainer = function(delegate, setting, placeholderText)
51 WebInspector.Object.call(this);
52 this._delegate = delegate;
54 this._tabbedPane = new WebInspector.TabbedPane();
55 this._tabbedPane.setPlaceholderText(placeholderText);
56 this._tabbedPane.setTabDelegate(new WebInspector.EditorContainerTabDelegate(this));
58 this._tabbedPane.setCloseableTabs(true);
59 this._tabbedPane.setAllowTabReorder(true, true);
60 this._tabbedPane.insertBeforeTabStrip(createElementWithClass("div", "sources-editor-tabstrip-left"));
61 this._tabbedPane.appendAfterTabStrip(createElementWithClass("div", "sources-editor-tabstrip-right"));
63 this._tabbedPane.addEventListener(WebInspector.TabbedPane.EventTypes.TabClosed, this._tabClosed, this);
64 this._tabbedPane.addEventListener(WebInspector.TabbedPane.EventTypes.TabSelected, this._tabSelected, this);
66 this._tabIds = new Map();
69 this._previouslyViewedFilesSetting = setting;
70 this._history = WebInspector.TabbedEditorContainer.History.fromObject(this._previouslyViewedFilesSetting.get());
73 WebInspector.TabbedEditorContainer.Events = {
74 EditorSelected: "EditorSelected",
75 EditorClosed: "EditorClosed"
78 WebInspector.TabbedEditorContainer._tabId = 0;
80 WebInspector.TabbedEditorContainer.maximalPreviouslyViewedFilesCount = 30;
82 WebInspector.TabbedEditorContainer.prototype = {
84 * @return {!WebInspector.Widget}
88 return this._tabbedPane;
92 * @type {!WebInspector.SourceFrame}
96 return this._tabbedPane.visibleView;
100 * @return {!Array.<!WebInspector.SourceFrame>}
102 fileViews: function()
104 return /** @type {!Array.<!WebInspector.SourceFrame>} */ (this._tabbedPane.tabViews());
108 * @param {!Element} parentElement
110 show: function(parentElement)
112 this._tabbedPane.show(parentElement);
116 * @param {!WebInspector.UISourceCode} uiSourceCode
118 showFile: function(uiSourceCode)
120 this._innerShowFile(uiSourceCode, true);
124 * @param {!WebInspector.UISourceCode} uiSourceCode
126 closeFile: function(uiSourceCode)
128 var tabId = this._tabIds.get(uiSourceCode);
131 this._closeTabs([tabId]);
135 * @return {!Array.<!WebInspector.UISourceCode>}
137 historyUISourceCodes: function()
139 // FIXME: there should be a way to fetch UISourceCode for its uri.
140 var uriToUISourceCode = {};
141 for (var id in this._files) {
142 var uiSourceCode = this._files[id];
143 uriToUISourceCode[uiSourceCode.uri()] = uiSourceCode;
147 var uris = this._history._urls();
148 for (var i = 0; i < uris.length; ++i) {
149 var uiSourceCode = uriToUISourceCode[uris[i]];
151 result.push(uiSourceCode);
156 _addViewListeners: function()
158 if (!this._currentView)
160 this._currentView.addEventListener(WebInspector.SourceFrame.Events.ScrollChanged, this._scrollChanged, this);
161 this._currentView.addEventListener(WebInspector.SourceFrame.Events.SelectionChanged, this._selectionChanged, this);
164 _removeViewListeners: function()
166 if (!this._currentView)
168 this._currentView.removeEventListener(WebInspector.SourceFrame.Events.ScrollChanged, this._scrollChanged, this);
169 this._currentView.removeEventListener(WebInspector.SourceFrame.Events.SelectionChanged, this._selectionChanged, this);
173 * @param {!WebInspector.Event} event
175 _scrollChanged: function(event)
177 var lineNumber = /** @type {number} */ (event.data);
178 this._history.updateScrollLineNumber(this._currentFile.uri(), lineNumber);
179 this._history.save(this._previouslyViewedFilesSetting);
183 * @param {!WebInspector.Event} event
185 _selectionChanged: function(event)
187 var range = /** @type {!WebInspector.TextRange} */ (event.data);
188 this._history.updateSelectionRange(this._currentFile.uri(), range);
189 this._history.save(this._previouslyViewedFilesSetting);
193 * @param {!WebInspector.UISourceCode} uiSourceCode
194 * @param {boolean=} userGesture
196 _innerShowFile: function(uiSourceCode, userGesture)
198 if (this._currentFile === uiSourceCode)
201 this._removeViewListeners();
202 this._currentFile = uiSourceCode;
204 var tabId = this._tabIds.get(uiSourceCode) || this._appendFileTab(uiSourceCode, userGesture);
206 this._tabbedPane.selectTab(tabId, userGesture);
208 this._editorSelectedByUserAction();
210 this._currentView = this.visibleView;
211 this._addViewListeners();
213 var eventData = { currentFile: this._currentFile, userGesture: userGesture };
214 this.dispatchEventToListeners(WebInspector.TabbedEditorContainer.Events.EditorSelected, eventData);
218 * @param {!WebInspector.UISourceCode} uiSourceCode
221 _titleForFile: function(uiSourceCode)
223 var maxDisplayNameLength = 30;
224 var title = uiSourceCode.displayName(true).trimMiddle(maxDisplayNameLength);
225 if (uiSourceCode.isDirty() || uiSourceCode.hasUnsavedCommittedChanges())
232 * @param {string} nextTabId
234 _maybeCloseTab: function(id, nextTabId)
236 var uiSourceCode = this._files[id];
237 var shouldPrompt = uiSourceCode.isDirty() && uiSourceCode.project().canSetFileContent();
238 // FIXME: this should be replaced with common Save/Discard/Cancel dialog.
239 if (!shouldPrompt || confirm(WebInspector.UIString("Are you sure you want to close unsaved file: %s?", uiSourceCode.name()))) {
240 uiSourceCode.resetWorkingCopy();
242 this._tabbedPane.selectTab(nextTabId, true);
243 this._tabbedPane.closeTab(id, true);
250 * @param {!Array.<string>} ids
252 _closeTabs: function(ids)
256 for (var i = 0; i < ids.length; ++i) {
258 var uiSourceCode = this._files[id];
259 if (uiSourceCode.isDirty())
264 if (dirtyTabs.length)
265 this._tabbedPane.selectTab(dirtyTabs[0], true);
266 this._tabbedPane.closeTabs(cleanTabs, true);
267 for (var i = 0; i < dirtyTabs.length; ++i) {
268 var nextTabId = i + 1 < dirtyTabs.length ? dirtyTabs[i + 1] : null;
269 if (!this._maybeCloseTab(dirtyTabs[i], nextTabId))
275 * @param {string} tabId
276 * @param {!WebInspector.ContextMenu} contextMenu
278 _onContextMenu: function(tabId, contextMenu)
280 var uiSourceCode = this._files[tabId];
282 contextMenu.appendApplicableItems(uiSourceCode);
286 * @param {!WebInspector.UISourceCode} uiSourceCode
288 addUISourceCode: function(uiSourceCode)
290 var uri = uiSourceCode.uri();
291 if (this._userSelectedFiles)
294 var index = this._history.index(uri);
298 if (!this._tabIds.has(uiSourceCode))
299 this._appendFileTab(uiSourceCode, false);
301 // Select tab if this file was the last to be shown.
303 this._innerShowFile(uiSourceCode, false);
307 if (!this._currentFile)
309 var currentProjectType = this._currentFile.project().type();
310 var addedProjectType = uiSourceCode.project().type();
311 var snippetsProjectType = WebInspector.projectTypes.Snippets;
312 if (this._history.index(this._currentFile.uri()) && currentProjectType === snippetsProjectType && addedProjectType !== snippetsProjectType)
313 this._innerShowFile(uiSourceCode, false);
317 * @param {!WebInspector.UISourceCode} uiSourceCode
319 removeUISourceCode: function(uiSourceCode)
321 this.removeUISourceCodes([uiSourceCode]);
325 * @param {!Array.<!WebInspector.UISourceCode>} uiSourceCodes
327 removeUISourceCodes: function(uiSourceCodes)
330 for (var i = 0; i < uiSourceCodes.length; ++i) {
331 var uiSourceCode = uiSourceCodes[i];
332 var tabId = this._tabIds.get(uiSourceCode);
336 this._tabbedPane.closeTabs(tabIds);
340 * @param {!WebInspector.UISourceCode} uiSourceCode
342 _editorClosedByUserAction: function(uiSourceCode)
344 this._userSelectedFiles = true;
345 this._history.remove(uiSourceCode.uri());
346 this._updateHistory();
349 _editorSelectedByUserAction: function()
351 this._userSelectedFiles = true;
352 this._updateHistory();
355 _updateHistory: function()
357 var tabIds = this._tabbedPane.lastOpenedTabIds(WebInspector.TabbedEditorContainer.maximalPreviouslyViewedFilesCount);
360 * @param {string} tabId
361 * @this {WebInspector.TabbedEditorContainer}
363 function tabIdToURI(tabId)
365 return this._files[tabId].uri();
368 this._history.update(tabIds.map(tabIdToURI.bind(this)));
369 this._history.save(this._previouslyViewedFilesSetting);
373 * @param {!WebInspector.UISourceCode} uiSourceCode
376 _tooltipForFile: function(uiSourceCode)
378 return uiSourceCode.originURL();
382 * @param {!WebInspector.UISourceCode} uiSourceCode
383 * @param {boolean=} userGesture
386 _appendFileTab: function(uiSourceCode, userGesture)
388 var view = this._delegate.viewForFile(uiSourceCode);
389 var title = this._titleForFile(uiSourceCode);
390 var tooltip = this._tooltipForFile(uiSourceCode);
392 var tabId = this._generateTabId();
393 this._tabIds.set(uiSourceCode, tabId);
394 this._files[tabId] = uiSourceCode;
396 var savedSelectionRange = this._history.selectionRange(uiSourceCode.uri());
397 if (savedSelectionRange)
398 view.setSelection(savedSelectionRange);
399 var savedScrollLineNumber = this._history.scrollLineNumber(uiSourceCode.uri());
400 if (savedScrollLineNumber)
401 view.scrollToLine(savedScrollLineNumber);
403 this._tabbedPane.appendTab(tabId, title, view, tooltip, userGesture);
405 this._updateFileTitle(uiSourceCode);
406 this._addUISourceCodeListeners(uiSourceCode);
411 * @param {!WebInspector.Event} event
413 _tabClosed: function(event)
415 var tabId = /** @type {string} */ (event.data.tabId);
416 var userGesture = /** @type {boolean} */ (event.data.isUserGesture);
418 var uiSourceCode = this._files[tabId];
419 if (this._currentFile === uiSourceCode) {
420 this._removeViewListeners();
421 delete this._currentView;
422 delete this._currentFile;
424 this._tabIds.remove(uiSourceCode);
425 delete this._files[tabId];
427 this._removeUISourceCodeListeners(uiSourceCode);
429 this.dispatchEventToListeners(WebInspector.TabbedEditorContainer.Events.EditorClosed, uiSourceCode);
432 this._editorClosedByUserAction(uiSourceCode);
436 * @param {!WebInspector.Event} event
438 _tabSelected: function(event)
440 var tabId = /** @type {string} */ (event.data.tabId);
441 var userGesture = /** @type {boolean} */ (event.data.isUserGesture);
443 var uiSourceCode = this._files[tabId];
444 this._innerShowFile(uiSourceCode, userGesture);
448 * @param {!WebInspector.UISourceCode} uiSourceCode
450 _addUISourceCodeListeners: function(uiSourceCode)
452 uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.TitleChanged, this._uiSourceCodeTitleChanged, this);
453 uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._uiSourceCodeWorkingCopyChanged, this);
454 uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._uiSourceCodeWorkingCopyCommitted, this);
455 uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.SavedStateUpdated, this._uiSourceCodeSavedStateUpdated, this);
459 * @param {!WebInspector.UISourceCode} uiSourceCode
461 _removeUISourceCodeListeners: function(uiSourceCode)
463 uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.TitleChanged, this._uiSourceCodeTitleChanged, this);
464 uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._uiSourceCodeWorkingCopyChanged, this);
465 uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._uiSourceCodeWorkingCopyCommitted, this);
466 uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.SavedStateUpdated, this._uiSourceCodeSavedStateUpdated, this);
470 * @param {!WebInspector.UISourceCode} uiSourceCode
472 _updateFileTitle: function(uiSourceCode)
474 var tabId = this._tabIds.get(uiSourceCode);
476 var title = this._titleForFile(uiSourceCode);
477 this._tabbedPane.changeTabTitle(tabId, title);
478 if (uiSourceCode.hasUnsavedCommittedChanges())
479 this._tabbedPane.setTabIcon(tabId, "warning-icon", WebInspector.UIString("Changes to this file were not saved to file system."));
481 this._tabbedPane.setTabIcon(tabId, "");
485 _uiSourceCodeTitleChanged: function(event)
487 var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.target);
488 this._updateFileTitle(uiSourceCode);
489 this._updateHistory();
492 _uiSourceCodeWorkingCopyChanged: function(event)
494 var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.target);
495 this._updateFileTitle(uiSourceCode);
498 _uiSourceCodeWorkingCopyCommitted: function(event)
500 var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.target);
501 this._updateFileTitle(uiSourceCode);
504 _uiSourceCodeSavedStateUpdated: function(event)
506 var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.target);
507 this._updateFileTitle(uiSourceCode);
512 delete this._userSelectedFiles;
518 _generateTabId: function()
520 return "tab_" + (WebInspector.TabbedEditorContainer._tabId++);
524 * @return {!WebInspector.UISourceCode} uiSourceCode
526 currentFile: function()
528 return this._currentFile;
531 __proto__: WebInspector.Object.prototype
536 * @param {string} url
537 * @param {!WebInspector.TextRange=} selectionRange
538 * @param {number=} scrollLineNumber
540 WebInspector.TabbedEditorContainer.HistoryItem = function(url, selectionRange, scrollLineNumber)
542 /** @const */ this.url = url;
543 /** @const */ this._isSerializable = url.length < WebInspector.TabbedEditorContainer.HistoryItem.serializableUrlLengthLimit;
544 this.selectionRange = selectionRange;
545 this.scrollLineNumber = scrollLineNumber;
548 WebInspector.TabbedEditorContainer.HistoryItem.serializableUrlLengthLimit = 4096;
551 * @param {!Object} serializedHistoryItem
552 * @return {!WebInspector.TabbedEditorContainer.HistoryItem}
554 WebInspector.TabbedEditorContainer.HistoryItem.fromObject = function (serializedHistoryItem)
556 var selectionRange = serializedHistoryItem.selectionRange ? WebInspector.TextRange.fromObject(serializedHistoryItem.selectionRange) : undefined;
557 return new WebInspector.TabbedEditorContainer.HistoryItem(serializedHistoryItem.url, selectionRange, serializedHistoryItem.scrollLineNumber);
560 WebInspector.TabbedEditorContainer.HistoryItem.prototype = {
564 serializeToObject: function()
566 if (!this._isSerializable)
568 var serializedHistoryItem = {};
569 serializedHistoryItem.url = this.url;
570 serializedHistoryItem.selectionRange = this.selectionRange;
571 serializedHistoryItem.scrollLineNumber = this.scrollLineNumber;
572 return serializedHistoryItem;
578 * @param {!Array.<!WebInspector.TabbedEditorContainer.HistoryItem>} items
580 WebInspector.TabbedEditorContainer.History = function(items)
583 this._rebuildItemIndex();
587 * @param {!Array.<!Object>} serializedHistory
588 * @return {!WebInspector.TabbedEditorContainer.History}
590 WebInspector.TabbedEditorContainer.History.fromObject = function(serializedHistory)
593 for (var i = 0; i < serializedHistory.length; ++i)
594 items.push(WebInspector.TabbedEditorContainer.HistoryItem.fromObject(serializedHistory[i]));
595 return new WebInspector.TabbedEditorContainer.History(items);
598 WebInspector.TabbedEditorContainer.History.prototype = {
600 * @param {string} url
605 var index = this._itemsIndex[url];
606 if (typeof index === "number")
611 _rebuildItemIndex: function()
613 this._itemsIndex = {};
614 for (var i = 0; i < this._items.length; ++i) {
615 console.assert(!this._itemsIndex.hasOwnProperty(this._items[i].url));
616 this._itemsIndex[this._items[i].url] = i;
621 * @param {string} url
622 * @return {!WebInspector.TextRange|undefined}
624 selectionRange: function(url)
626 var index = this.index(url);
627 return index !== -1 ? this._items[index].selectionRange : undefined;
631 * @param {string} url
632 * @param {!WebInspector.TextRange=} selectionRange
634 updateSelectionRange: function(url, selectionRange)
638 var index = this.index(url);
641 this._items[index].selectionRange = selectionRange;
645 * @param {string} url
646 * @return {number|undefined}
648 scrollLineNumber: function(url)
650 var index = this.index(url);
651 return index !== -1 ? this._items[index].scrollLineNumber : undefined;
655 * @param {string} url
656 * @param {number} scrollLineNumber
658 updateScrollLineNumber: function(url, scrollLineNumber)
660 var index = this.index(url);
663 this._items[index].scrollLineNumber = scrollLineNumber;
667 * @param {!Array.<string>} urls
669 update: function(urls)
671 for (var i = urls.length - 1; i >= 0; --i) {
672 var index = this.index(urls[i]);
675 item = this._items[index];
676 this._items.splice(index, 1);
678 item = new WebInspector.TabbedEditorContainer.HistoryItem(urls[i]);
679 this._items.unshift(item);
680 this._rebuildItemIndex();
685 * @param {string} url
687 remove: function(url)
689 var index = this.index(url);
691 this._items.splice(index, 1);
692 this._rebuildItemIndex();
697 * @param {!WebInspector.Setting} setting
699 save: function(setting)
701 setting.set(this._serializeToObject());
705 * @return {!Array.<!Object>}
707 _serializeToObject: function()
709 var serializedHistory = [];
710 for (var i = 0; i < this._items.length; ++i) {
711 var serializedItem = this._items[i].serializeToObject();
713 serializedHistory.push(serializedItem);
714 if (serializedHistory.length === WebInspector.TabbedEditorContainer.maximalPreviouslyViewedFilesCount)
717 return serializedHistory;
722 * @return {!Array.<string>}
727 for (var i = 0; i < this._items.length; ++i)
728 result.push(this._items[i].url);
735 * @implements {WebInspector.TabbedPaneTabDelegate}
736 * @param {!WebInspector.TabbedEditorContainer} editorContainer
738 WebInspector.EditorContainerTabDelegate = function(editorContainer)
740 this._editorContainer = editorContainer;
743 WebInspector.EditorContainerTabDelegate.prototype = {
746 * @param {!WebInspector.TabbedPane} tabbedPane
747 * @param {!Array.<string>} ids
749 closeTabs: function(tabbedPane, ids)
751 this._editorContainer._closeTabs(ids);
756 * @param {string} tabId
757 * @param {!WebInspector.ContextMenu} contextMenu
759 onContextMenu: function(tabId, contextMenu)
761 this._editorContainer._onContextMenu(tabId, contextMenu);