/* * initializes global sort method from preference settings or the default */ function initSortMethod() { var sortMethod = PreferencesManager.getViewState(_WORKING_SET_SORT_PREF); if (!sortMethod) { sortMethod = _convertSortPref(PreferencesManager.getViewState(_LEGACY_SORT_PREF)); } if (!sortMethod) { sortMethod = Commands.CMD_WORKINGSET_SORT_BY_ADDED; } return sortMethod; }
/** * Called after a file or folder has been deleted. This function is responsible * for updating underlying model data and notifying all views of the change. * * @param {string} fullPath The path of the file/folder that has been deleted */ function notifyPathDeleted(fullPath) { // FileSyncManager.syncOpenDocuments() does all the work prompting // the user to save any unsaved changes and then calls us back // via notifyFileDeleted FileSyncManager.syncOpenDocuments(Strings.FILE_DELETED_TITLE); var projectRoot = ProjectManager.getProjectRoot(), context = { location : { scope: "user", layer: "project", layerID: projectRoot.fullPath } }; var encoding = PreferencesManager.getViewState("encoding", context); delete encoding[fullPath]; PreferencesManager.setViewState("encoding", encoding, context); if (!getOpenDocumentForPath(fullPath) && !MainViewManager.findInAllWorkingSets(fullPath).length) { // For images not open in the workingset, // FileSyncManager.syncOpenDocuments() will // not tell us to close those views exports.trigger("pathDeleted", fullPath); } }
/** * @private * Auto-install extensions bundled with installer * @return {$.Promise} Promise that resolves when finished */ function _autoInstallExtensions() { var dirPath = FileUtils.getDirectoryPath(FileUtils.getNativeBracketsDirectoryPath()) + FOLDER_AUTOINSTALL + "/", autoExtensions = PreferencesManager.getViewState(FOLDER_AUTOINSTALL) || {}, deferred = new $.Deferred(); _getAutoInstallFiles(dirPath, autoExtensions).done(function (result) { var installPromise = Async.doSequentially(result.installZips, function (zip) { autoExtensions[zip.info.metadata.name] = zip.info.metadata.version; return Package.installFromPath(zip.file.fullPath); }); var updatePromise = installPromise.always(function () { return Async.doSequentially(result.updateZips, function (zip) { autoExtensions[zip.info.metadata.name] = zip.info.metadata.version; return Package.installUpdate(zip.file.fullPath); }); }); // Always resolve the outer promise updatePromise.always(function () { // Keep track of auto-installed extensions so we only install an extension once PreferencesManager.setViewState(FOLDER_AUTOINSTALL, autoExtensions); deferred.resolve(); }); }); return deferred.promise(); }
/** * Remove the target item from the filter dropdown list and update dropdown button * and dropdown list UI. * @param {!Event} e Mouse events */ function _handleDeleteFilter(e) { // Remove the filter set from the preferences and // clear the active filter set index from view state. var filterSets = PreferencesManager.get("fileFilters") || [], activeFilterIndex = PreferencesManager.getViewState("activeFileFilter"), filterIndex = $(e.target).parent().data("index") - FIRST_FILTER_INDEX; // Don't let the click bubble upward. e.stopPropagation(); filterSets.splice(filterIndex, 1); PreferencesManager.set("fileFilters", filterSets); if (activeFilterIndex === filterIndex) { // Removing the active filter, so clear the active filter // both in the view state. setActiveFilter(null); } else if (activeFilterIndex > filterIndex) { // Adjust the active filter index after the removal of a filter set before it. --activeFilterIndex; setActiveFilter(filterSets[activeFilterIndex], activeFilterIndex); } _updatePicker(); _doPopulate(); _picker.refresh(); }
MultiRangeInlineEditor.prototype._toggleSection = function (fullPath, duringInit) { var $headerItem = this._$headers[fullPath]; var $disclosureIcon = $headerItem.find(".disclosure-triangle"); var isCollapsing = $disclosureIcon.hasClass("expanded"); $disclosureIcon.toggleClass("expanded"); $headerItem.nextUntil(".section-header").toggle(!isCollapsing); // explicit visibility arg, since during load() jQ doesn't think nodes are visible // Update instance-specific state... this._collapsedFiles[fullPath] = isCollapsing; // ...AND persist as per-project view state if (!duringInit) { var setting = PreferencesManager.getViewState("inlineEditor.collapsedFiles", _getPrefsContext()) || {}; if (isCollapsing) { setting[fullPath] = true; } else { delete setting[fullPath]; } PreferencesManager.setViewState("inlineEditor.collapsedFiles", setting, _getPrefsContext()); } // Show/hide selection indicator if selection was in collapsed section this._updateSelectedMarker(false); // Changing height of rule list may change ht of overall editor this._ruleListHeightChanged(); // If user expands collapsed section and nothing selected yet, select first result in this section if (this._selectedRangeIndex === -1 && !isCollapsing && !duringInit) { var index = _.findIndex(this._ranges, function (resultItem) { return resultItem.textRange.document.file.fullPath === fullPath; }); this.setSelectedIndex(index); } };
/** * Set up mouse click event listeners for 'Delete' and 'Edit' buttons * when the dropdown is open. Also set check mark on the active filter. * @param {!Event>} event listRendered event triggered when the dropdown is open * @param {!jQueryObject} $dropdown the jQuery DOM node of the dropdown list */ function _handleListRendered(event, $dropdown) { var activeFilterIndex = PreferencesManager.getViewState("activeFileFilter"), checkedItemIndex = (activeFilterIndex > -1) ? (activeFilterIndex + FIRST_FILTER_INDEX) : -1; _picker.setChecked(checkedItemIndex, true); $dropdown.find(".filter-trash-icon") .on("click", _handleDeleteFilter); $dropdown.find(".filter-edit-icon") .on("click", _handleEditFilter); }
ProjectManager.openProject(initialProjectPath).always(function () { _initTest(); // If this is the first launch, and we have an index.html file in the project folder (which should be // the samples folder on first launch), open it automatically. (We explicitly check for the // samples folder in case this is the first time we're launching Brackets after upgrading from // an old version that might not have set the "afterFirstLaunch" pref.) var deferred = new $.Deferred(); if (!params.get("skipSampleProjectLoad") && !PreferencesManager.getViewState("afterFirstLaunch")) { PreferencesManager.setViewState("afterFirstLaunch", "true"); if (ProjectManager.isWelcomeProjectPath(initialProjectPath)) { FileSystem.resolve(initialProjectPath + "index.html", function (err, file) { if (!err) { var promise = CommandManager.execute(Commands.FILE_ADD_TO_WORKING_SET, { fullPath: file.fullPath }); promise.then(deferred.resolve, deferred.reject); } else { deferred.reject(); } }); } else { deferred.resolve(); } } else { deferred.resolve(); } deferred.always(function () { // Signal that Brackets is loaded AppInit._dispatchReady(AppInit.APP_READY); PerfUtils.addMeasurement("Application Startup"); if (PreferencesManager._isUserScopeCorrupt()) { Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, Strings.ERROR_PREFS_CORRUPT_TITLE, Strings.ERROR_PREFS_CORRUPT ) .done(function () { CommandManager.execute(Commands.FILE_OPEN_PREFERENCES); }); } }); // See if any startup files were passed to the application if (brackets.app.getPendingFilesToOpen) { brackets.app.getPendingFilesToOpen(function (err, files) { DragAndDrop.openDroppedFiles(files); }); } });
/** * A search filter is an array of one or more glob strings. The filter must be 'compiled' via compile() * before passing to filterPath()/filterFileList(). * @return {?{name: string, patterns: Array.<string>}} */ function getActiveFilter() { var filterSets = PreferencesManager.get("fileFilters") || [], activeFilterIndex = PreferencesManager.getViewState("activeFileFilter"), oldFilter = PreferencesManager.getViewState("search.exclusions") || [], activeFilter = null; if (activeFilterIndex === undefined && oldFilter.length) { activeFilter = { name: "", patterns: oldFilter }; activeFilterIndex = _getFilterIndex(filterSets, activeFilter); // Migrate the old filter into the new filter storage if (activeFilterIndex === -1) { activeFilterIndex = filterSets.length; filterSets.push(activeFilter); PreferencesManager.set("fileFilters", filterSets); } PreferencesManager.setViewState("activeFileFilter", activeFilterIndex); } else if (activeFilterIndex > -1 && activeFilterIndex < filterSets.length) { activeFilter = filterSets[activeFilterIndex]; } return activeFilter; }
/** * Send Analytics Data * @param {string} eventCategory The kind of Event Category that * needs to be logged- should be a js var compatible string * @param {string} eventSubCategory The kind of Event Sub Category that * needs to be logged- should be a js var compatible string * @param {string} eventType The kind of Event Type that needs to be logged- should be a js var compatible string * @param {string} eventSubType The kind of Event Sub Type that * needs to be logged- should be a js var compatible string */ function sendAnalyticsData(eventName, eventCategory, eventSubCategory, eventType, eventSubType) { var isEventDataAlreadySent = PreferencesManager.getViewState(eventName), isHDTracking = PreferencesManager.getExtensionPrefs("healthData").get("healthDataTracking"), eventParams = {}; if (isHDTracking && !isEventDataAlreadySent && eventName && eventCategory) { eventParams = { eventName: eventName, eventCategory: eventCategory, eventSubCategory: eventSubCategory || "", eventType: eventType || "", eventSubType: eventSubType || "" }; notifyHealthManagerToSendData(eventParams); } }
AppInit.appReady(function () { var sortMethod = initSortMethod(), curSort = get(sortMethod), autoSort = PreferencesManager.getViewState("automaticSort"); if (curSort) { _setCurrentSort(curSort); } if (autoSort) { setAutomatic(autoSort); } if (curSort && autoSort) { curSort.sort(); } });
promise.done(function (text, readTimestamp) { encodingSelect.$button.text(document.file._encoding); // Store the preferred encoding in the state var projectRoot = ProjectManager.getProjectRoot(), context = { location : { scope: "user", layer: "project", layerID: projectRoot.fullPath } }; var encoding = PreferencesManager.getViewState("encoding", context); encoding[document.file.fullPath] = document.file._encoding; PreferencesManager.setViewState("encoding", encoding, context); });
/** * Every 24 hours downloads registry information to check for update, but only if the registry download * wasn't triggered by another action (like opening extension manager) * If there isn't 24 hours elapsed from the last download, use cached information from last download * to determine state of the update notification. */ function checkForExtensionsUpdate() { var lastExtensionRegistryCheckTime = PreferencesManager.getViewState("lastExtensionRegistryCheckTime"), timeOfNextCheck = lastExtensionRegistryCheckTime + ONE_DAY, currentTime = (new Date()).getTime(); // update icon according to previously saved information var availableUpdates = PreferencesManager.getViewState("extensionUpdateInfo"); availableUpdates = ExtensionManager.cleanAvailableUpdates(availableUpdates); $("#toolbar-extension-manager").toggleClass("updatesAvailable", availableUpdates.length > 0); if (availableUpdates.length === 0) { // icon is gray, no updates available if (currentTime > timeOfNextCheck) { // downloadRegistry, will be resolved in _onRegistryDownloaded ExtensionManager.downloadRegistry().done(function () { // schedule another check in 24 hours + 2 minutes setTimeout(checkForExtensionsUpdate, ONE_DAY + TWO_MINUTES); }); } else { // schedule the download of the registry in appropriate time setTimeout(checkForExtensionsUpdate, (timeOfNextCheck - currentTime) + TWO_MINUTES); } } }
FindBar.prototype._addElementToSearchHistory = function (searchVal) { if (searchVal) { var searchHistory = PreferencesManager.getViewState("searchHistory"); var maxCount = PreferencesManager.get("maxSearchHistory"); var searchQueryIndex = searchHistory.indexOf(searchVal); if (searchQueryIndex !== -1) { searchHistory.splice(searchQueryIndex, 1); } else { if (searchHistory.length === maxCount) { searchHistory.pop(); } } searchHistory.unshift(searchVal); PreferencesManager.setViewState("searchHistory", searchHistory); } };
/** * @private * Initializes the working set. */ function _projectOpen(e) { // file root is appended for each project var projectRoot = ProjectManager.getProjectRoot(), files = [], context = { location : { scope: "user", layer: "project" } }; files = PreferencesManager.getViewState("project.files", context); console.assert(Object.keys(_openDocuments).length === 0); // no files leftover from prev proj if (!files) { return; } var filesToOpen = [], viewStates = {}, activeFile; // Add all files to the working set without verifying that // they still exist on disk (for faster project switching) files.forEach(function (value, index) { filesToOpen.push(FileSystem.getFileForPath(value.file)); if (value.active) { activeFile = value.file; } if (value.viewState) { viewStates[value.file] = value.viewState; } }); addListToWorkingSet(filesToOpen); // Allow for restoring saved editor UI state EditorManager._resetViewStates(viewStates); // Initialize the active editor if (!activeFile && _workingSet.length > 0) { activeFile = _workingSet[0].fullPath; } if (activeFile) { var promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: activeFile }); // Add this promise to the event's promises to signal that this handler isn't done yet e.promises.push(promise); } }
/** * Toggles LiveDevelopment and synchronizes the state of UI elements that reports LiveDevelopment status * * Stop Live Dev when in an active state (ACTIVE, OUT_OF_SYNC, SYNC_ERROR). * Start Live Dev when in an inactive state (ERROR, INACTIVE). * Do nothing when in a connecting state (CONNECTING, LOADING_AGENTS). */ function _handleGoLiveCommand() { if (LiveDevelopment.status >= LiveDevelopment.STATUS_ACTIVE) { LiveDevelopment.close(); } else if (LiveDevelopment.status <= LiveDevelopment.STATUS_INACTIVE) { if (!params.get("skipLiveDevelopmentInfo") && !PreferencesManager.getViewState("livedev.afterFirstLaunch")) { PreferencesManager.setViewState("livedev.afterFirstLaunch", "true"); Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_INFO, Strings.LIVE_DEVELOPMENT_INFO_TITLE, Strings.LIVE_DEVELOPMENT_INFO_MESSAGE ).done(function (id) { LiveDevelopment.open(); }); } else { LiveDevelopment.open(); } } }
/** * Restores the font size using the saved style and migrates the old fontSizeAdjustment * view state to the new fontSize, when required */ function restoreFontSize() { var fsStyle = prefs.get("fontSize"), fsAdjustment = PreferencesManager.getViewState("fontSizeAdjustment"); if (fsAdjustment) { // Always remove the old view state even if we also have the new view state. PreferencesManager.setViewState("fontSizeAdjustment"); if (!fsStyle) { // Migrate the old view state to the new one. fsStyle = (DEFAULT_FONT_SIZE + fsAdjustment) + "px"; prefs.set("fontSize", fsStyle); } } if (fsStyle) { _removeDynamicFontSize(); _addDynamicFontSize(fsStyle); } }
ProjectManager.on("projectOpen", function () { var projectRoot = ProjectManager.getProjectRoot(), context = { location : { scope: "user", layer: "project", layerID: projectRoot.fullPath } }; var encoding = PreferencesManager.getViewState("encoding", context); if (!encoding) { encoding = {}; PreferencesManager.setViewState("encoding", encoding, context); } Async.doSequentially(Object.keys(encoding), function (filePath, index) { return _checkFileExistance(filePath, index, encoding); }, false) .always(function () { PreferencesManager.setViewState("encoding", encoding, context); }); });
/** * Check for updates. If "force" is true, update notification dialogs are always displayed * (if an update is available). If "force" is false, the update notification is only * displayed for newly available updates. * * If an update is available, show the "update available" notification icon in the title bar. * * @param {boolean} force If true, always show the notification dialog. * @param {Object} _testValues This should only be used for testing purposes. See comments for details. * @return {$.Promise} jQuery Promise object that is resolved or rejected after the update check is complete. */ function checkForUpdate(force, _testValues) { // This is the last version we notified the user about. If checkForUpdate() // is called with "false", only show the update notification dialog if there // is an update newer than this one. This value is saved in preferences. var lastNotifiedBuildNumber = PreferencesManager.getViewState("lastNotifiedBuildNumber"); // The second param, if non-null, is an Object containing value overrides. Values // in the object temporarily override the local values. This should *only* be used for testing. // If any overrides are set, permanent changes are not made (including showing // the update notification icon and saving prefs). var oldValues; var usingOverrides = false; // true if any of the values are overridden. var result = new $.Deferred(); var versionInfoUrl; if (_testValues) { oldValues = {}; if (_testValues.hasOwnProperty("_buildNumber")) { oldValues._buildNumber = _buildNumber; _buildNumber = _testValues._buildNumber; usingOverrides = true; } if (_testValues.hasOwnProperty("lastNotifiedBuildNumber")) { oldValues.lastNotifiedBuildNumber = lastNotifiedBuildNumber; lastNotifiedBuildNumber = _testValues.lastNotifiedBuildNumber; usingOverrides = true; } if (_testValues.hasOwnProperty("_versionInfoURL")) { versionInfoUrl = _testValues._versionInfoURL; usingOverrides = true; } } _getUpdateInformation(force || usingOverrides, usingOverrides, versionInfoUrl) .done(function (versionInfo) { // Get all available updates var allUpdates = _stripOldVersionInfo(versionInfo, _buildNumber); // When running directly from GitHub source (as opposed to // an installed build), _buildNumber is 0. In this case, if the // test is not forced, don't show the update notification icon or // dialog. if (_buildNumber === 0 && !force) { result.resolve(); return; } if (allUpdates) { // Always show the "update available" icon if any updates are available var $updateNotification = $("#update-notification"); $updateNotification.css("display", "block"); if (!_addedClickHandler) { _addedClickHandler = true; $updateNotification.on("click", function () { checkForUpdate(true); }); } // Only show the update dialog if force = true, or if the user hasn't been // alerted of this update if (force || allUpdates[0].buildNumber > lastNotifiedBuildNumber) { _showUpdateNotificationDialog(allUpdates); // Update prefs with the last notified build number lastNotifiedBuildNumber = allUpdates[0].buildNumber; // Don't save prefs is we have overridden values if (!usingOverrides) { PreferencesManager.setViewState("lastNotifiedBuildNumber", lastNotifiedBuildNumber); } } } else if (force) { // No updates are available. If force == true, let the user know. Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, Strings.NO_UPDATE_TITLE, Strings.NO_UPDATE_MESSAGE ); } if (oldValues) { if (oldValues.hasOwnProperty("_buildNumber")) { _buildNumber = oldValues._buildNumber; } if (oldValues.hasOwnProperty("lastNotifiedBuildNumber")) { lastNotifiedBuildNumber = oldValues.lastNotifiedBuildNumber; } } result.resolve(); }) .fail(function () { // Error fetching the update data. If this is a forced check, alert the user if (force) { Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, Strings.ERROR_FETCHING_UPDATE_INFO_TITLE, Strings.ERROR_FETCHING_UPDATE_INFO_MSG ); } result.reject(); }); return result.promise(); }
resultProvider: function (query) { var asyncResult = new $.Deferred(); asyncResult.resolve(PreferencesManager.getViewState("searchHistory")); return asyncResult.promise(); },
/** * A search filter is an array of one or more glob strings. The filter must be 'compiled' via compile() * before passing to filterPath()/filterFileList(). * @return {!Array.<string>} */ function getLastFilter() { return PreferencesManager.getViewState("search.exclusions") || []; }
MultiRangeInlineEditor.prototype.load = function (hostEditor) { MultiRangeInlineEditor.prototype.parentClass.load.apply(this, arguments); // Create the message area this.$messageDiv = $("<div/>") .addClass("inline-editor-message"); // Prevent touch scroll events from bubbling up to the parent editor. this.$editorHolder.on("mousewheel.MultiRangeInlineEditor", function (e) { e.stopPropagation(); }); // Outer container for border-left and scrolling this.$relatedContainer = $("<div/>").addClass("related-container"); // List "selection" highlight this.$selectedMarker = $("<div/>").appendTo(this.$relatedContainer).addClass("selection"); // Inner container this.$related = $("<div/>").appendTo(this.$relatedContainer).addClass("related"); // Range list this.$rangeList = $("<ul/>").appendTo(this.$related); // Determine which sections are initially collapsed (the actual collapsing happens after onAdded(), // because jQuery.hide() requires the computed value of 'display' to work properly) var toCollapse = PreferencesManager.getViewState("inlineEditor.collapsedFiles", _getPrefsContext()) || {}; Object.keys(toCollapse).forEach(function (fullPath) { this._collapsedFiles[fullPath] = true; }.bind(this)); // Render list & section headers (matching collapsed state set above) this._renderList(); if (this._ranges.length > 1) { // attach to main container this.$wrapper.before(this.$relatedContainer); } // Add TextRange listeners to update UI as text changes var self = this; this._ranges.forEach(function (range, index) { // Update list item as TextRange changes range.textRange.on("change", function () { _updateRangeLabel(range.$listItem, range); }).on("contentChange", function () { _updateRangeLabel(range.$listItem, range, self._labelCB); }); // If TextRange lost sync, remove it from the list (and close the widget if no other ranges are left) range.textRange.on("lostSync", function () { self._removeRange(range); }); }); // Initial selection is the first non-collapsed result item var indexToSelect = _.findIndex(this._ranges, function (range) { return !this._collapsedFiles[range.textRange.document.file.fullPath]; }.bind(this)); if (this._ranges.length === 1 && indexToSelect === -1) { // If no right-hand rule list shown, select the one result even if it's in a collapsed file (since no way to expand) indexToSelect = 0; } if (indexToSelect !== -1) { // select the first visible range this.setSelectedIndex(indexToSelect); } else { // force the message div to show this.setSelectedIndex(-1); } // Listen for clicks directly on us, so we can set focus back to the editor var clickHandler = this._onClick.bind(this); this.$htmlContent.on("click.MultiRangeInlineEditor", clickHandler); // Also handle mouseup in case the user drags a little bit this.$htmlContent.on("mouseup.MultiRangeInlineEditor", clickHandler); // Update the rule list navigation menu items when we gain/lose focus. this.$htmlContent .on("focusin.MultiRangeInlineEditor", this._updateCommands.bind(this)) .on("focusout.MultiRangeInlineEditor", this._updateCommands.bind(this)); };
ProjectManager.openProject(initialProjectPath).always(function () { _initTest(); // If this is the first launch, and we have an index.html file in the project folder (which should be // the samples folder on first launch), open it automatically. (We explicitly check for the // samples folder in case this is the first time we're launching Brackets after upgrading from // an old version that might not have set the "afterFirstLaunch" pref.) var deferred = new $.Deferred(); if (!params.get("skipSampleProjectLoad") && !PreferencesManager.getViewState("afterFirstLaunch")) { PreferencesManager.setViewState("afterFirstLaunch", "true"); if (ProjectManager.isWelcomeProjectPath(initialProjectPath)) { FileSystem.resolve(initialProjectPath + "index.html", function (err, file) { if (!err) { var promise = CommandManager.execute(Commands.CMD_ADD_TO_WORKINGSET_AND_OPEN, { fullPath: file.fullPath }); promise.then(deferred.resolve, deferred.reject); } else { deferred.reject(); } }); } else { deferred.resolve(); } } else { deferred.resolve(); } deferred.always(function () { // Signal that Brackets is loaded AppInit._dispatchReady(AppInit.APP_READY); PerfUtils.addMeasurement("Application Startup"); if (PreferencesManager._isUserScopeCorrupt()) { var userPrefFullPath = PreferencesManager.getUserPrefFile(); // user scope can get corrupt only if the file exists, is readable, // but malformed. no need to check for its existance. var info = MainViewManager.findInAllWorkingSets(userPrefFullPath); var paneId; if (info.length) { paneId = info[0].paneId; } FileViewController.openFileAndAddToWorkingSet(userPrefFullPath, paneId) .done(function () { Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, Strings.ERROR_PREFS_CORRUPT_TITLE, Strings.ERROR_PREFS_CORRUPT ).done(function () { // give the focus back to the editor with the pref file MainViewManager.focusActivePane(); }); }); } }); // See if any startup files were passed to the application if (brackets.app.getPendingFilesToOpen) { brackets.app.getPendingFilesToOpen(function (err, paths) { DragAndDrop.openDroppedFiles(paths); }); } });
FindBar.prototype._updateSearchBarFromPrefs = function () { // Have to make sure we explicitly cast the second parameter to a boolean, because // toggleClass expects literal true/false. this.$("#find-case-sensitive").toggleClass("active", !!PreferencesManager.getViewState("caseSensitive")); this.$("#find-regexp").toggleClass("active", !!PreferencesManager.getViewState("regexp")); };
/** * Return all health data logged till now stored in the state prefs * @return {Object} Health Data aggregated till now */ function getStoredHealthData() { var storedData = PreferencesManager.getViewState(HEALTH_DATA_STATE_KEY) || {}; return storedData; }
.on("change", function () { config.highlight = PreferencesManager.getViewState("livedev.highlight"); _updateHighlightCheckmark(); });
.on("keydown", "#find-what, #replace-with", function (e) { lastTypedTime = new Date().getTime(); lastKeyCode = e.keyCode; var executeSearchIfNeeded = function () { // We only do instant search via node. if (FindUtils.isNodeSearchDisabled() || FindUtils.isInstantSearchDisabled()) { // we still keep the intrval timer up as instant search could get enabled/disabled based on node busy state return; } if (self._closed) { return; } currentTime = new Date().getTime(); if (lastTypedTime && (currentTime - lastTypedTime >= 100) && self.getQueryInfo().query !== lastQueriedText && !FindUtils.isNodeSearchInProgress()) { // init Search if (self._options.multifile) { if ($(e.target).is("#find-what")) { if (!self._options.replace) { HealthLogger.searchDone(HealthLogger.SEARCH_INSTANT); self.trigger("doFind"); lastQueriedText = self.getQueryInfo().query; } } } } }; if (intervalId === 0) { intervalId = window.setInterval(executeSearchIfNeeded, 50); } var searchHistory = PreferencesManager.getViewState("searchHistory"); var maxCount = PreferencesManager.get("maxSearchHistory"); if (e.keyCode === KeyEvent.DOM_VK_RETURN) { e.preventDefault(); e.stopPropagation(); var searchVal = self.$("#find-what").val(); var searchQueryIndex = searchHistory.indexOf(searchVal); if (searchQueryIndex !== -1) { searchHistory.splice(searchQueryIndex, 1); } else { if (searchHistory.length === maxCount) { searchHistory.pop(); } } if (searchVal) { searchHistory.unshift(searchVal); } PreferencesManager.setViewState("searchHistory", searchHistory); lastQueriedText = self.getQueryInfo().query; if (self._options.multifile) { if ($(e.target).is("#find-what")) { if (self._options.replace) { // Just set focus to the Replace field. self.focusReplace(); } else { HealthLogger.searchDone(HealthLogger.SEARCH_ON_RETURN_KEY); // Trigger a Find (which really means "Find All" in this context). self.trigger("doFind"); } } else { HealthLogger.searchDone(HealthLogger.SEARCH_REPLACE_ALL); self.trigger("doReplaceBatch"); } } else { // In the single file case, we just want to trigger a Find Next (or Find Previous // if Shift is held down). self.trigger("doFind", e.shiftKey); } historyIndex = 0; } else if (e.keyCode === KeyEvent.DOM_VK_DOWN || e.keyCode === KeyEvent.DOM_VK_UP) { var quickSearchContainer = $(".quick-search-container"); if (!self.searchField) { self.showSearchHints(); } else if (!quickSearchContainer.is(':visible')) { quickSearchContainer.show(); } } });
define(function (require, exports, module) { "use strict"; var Dialogs = require("widgets/Dialogs"), DefaultDialogs = require("widgets/DefaultDialogs"), ExtensionManager = require("extensibility/ExtensionManager"), PreferencesManager = require("preferences/PreferencesManager"), Global = require("utils/Global"), NativeApp = require("utils/NativeApp"), StringUtils = require("utils/StringUtils"), Strings = require("strings"), UpdateDialogTemplate = require("text!htmlContent/update-dialog.html"), UpdateListTemplate = require("text!htmlContent/update-list.html"); // duration of one day in milliseconds var ONE_DAY = 1000 * 60 * 60 * 24; // duration of two minutes in milliseconds var TWO_MINUTES = 1000 * 60 * 2; // Extract current build number from package.json version field 0.0.0-0 var _buildNumber = Number(/-([0-9]+)/.exec(brackets.metadata.version)[1]); // Init default last build number PreferencesManager.stateManager.definePreference("lastNotifiedBuildNumber", "number", 0); // Time of last registry check for update PreferencesManager.stateManager.definePreference("lastExtensionRegistryCheckTime", "number", 0); // Data about available updates in the registry PreferencesManager.stateManager.definePreference("extensionUpdateInfo", "Array", []); PreferencesManager.convertPreferences(module, { "lastNotifiedBuildNumber": "user", "lastInfoURLFetchTime": "user", "updateInfo": "user" }, true); // This is the last version we notified the user about. If checkForUpdate() // is called with "false", only show the update notification dialog if there // is an update newer than this one. This value is saved in preferences. var _lastNotifiedBuildNumber = PreferencesManager.getViewState("lastNotifiedBuildNumber"); // Last time the versionInfoURL was fetched var _lastInfoURLFetchTime = PreferencesManager.getViewState("lastInfoURLFetchTime"); // URL to load version info from. By default this is loaded no more than once a day. If // you force an update check it is always loaded. // Information on all posted builds of Brackets. This is an Array, where each element is // an Object with the following fields: // // {Number} buildNumber Number of the build // {String} versionString String representation of the build number (ie "Sprint 14") // {String} dateString Date of the build // {String} releaseNotesURL URL of the release notes for this build // {String} downloadURL URL to download this build // {Array} newFeatures Array of new features in this build. Each entry has two fields: // {String} name Name of the feature // {String} description Description of the feature // // This array must be reverse sorted by buildNumber (newest build info first) /** * @private * Flag that indicates if we've added a click handler to the update notification icon. */ var _addedClickHandler = false; /** * Construct a new version update url with the given locale. * * @param {string=} locale - optional locale, defaults to 'brackets.getLocale()' when omitted. * @param {boolean=} removeCountryPartOfLocale - optional, remove existing country information from locale 'en-gb' => 'en' * return {string} the new version update url */ function _getVersionInfoUrl(locale, removeCountryPartOfLocale) { locale = locale || brackets.getLocale(); if (removeCountryPartOfLocale) { locale = locale.substring(0, 2); } return brackets.config.update_info_url + locale + ".json"; } /** * Get a data structure that has information for all builds of Brackets. * * If force is true, the information is always fetched from _versionInfoURL. * If force is false, we try to use cached information. If more than * 24 hours have passed since the last fetch, or if cached data can't be found, * the data is fetched again. * * If new data is fetched and dontCache is false, the data is saved in preferences * for quick fetching later. * _versionInfoUrl is used for unit testing. */ function _getUpdateInformation(force, dontCache, _versionInfoUrl) { var result = new $.Deferred(); var fetchData = false; var data; // If force is true, always fetch if (force) { fetchData = true; } // If we don't have data saved in prefs, fetch data = PreferencesManager.getViewState("updateInfo"); if (!data) { fetchData = true; } // If more than 24 hours have passed since our last fetch, fetch again if ((new Date()).getTime() > _lastInfoURLFetchTime + ONE_DAY) { fetchData = true; } if (fetchData) { var lookupPromise = new $.Deferred(), localVersionInfoUrl; // If the current locale isn't "en" or "en-US", check whether we actually have a // locale-specific update notification, and fall back to "en" if not. // Note: we check for both "en" and "en-US" to watch for the general case or // country-specific English locale. The former appears default on Mac, while // the latter appears default on Windows. var locale = brackets.getLocale().toLowerCase(); if (locale !== "en" && locale !== "en-us") { localVersionInfoUrl = _versionInfoUrl || _getVersionInfoUrl(); $.ajax({ url: localVersionInfoUrl, cache: false, type: "HEAD" }).fail(function (jqXHR, status, error) { // get rid of any country information from locale and try again var tmpUrl = _getVersionInfoUrl(brackets.getLocale(), true); if (tmpUrl !== localVersionInfoUrl) { $.ajax({ url: tmpUrl, cache: false, type: "HEAD" }).fail(function (jqXHR, status, error) { localVersionInfoUrl = _getVersionInfoUrl("en"); }).done(function (jqXHR, status, error) { localVersionInfoUrl = tmpUrl; }).always(function (jqXHR, status, error) { lookupPromise.resolve(); }); } else { localVersionInfoUrl = _getVersionInfoUrl("en"); lookupPromise.resolve(); } }).done(function (jqXHR, status, error) { lookupPromise.resolve(); }); } else { localVersionInfoUrl = _versionInfoUrl || _getVersionInfoUrl("en"); lookupPromise.resolve(); } lookupPromise.done(function () { $.ajax({ url: localVersionInfoUrl, dataType: "json", cache: false }).done(function (updateInfo, textStatus, jqXHR) { if (!dontCache) { _lastInfoURLFetchTime = (new Date()).getTime(); PreferencesManager.setViewState("lastInfoURLFetchTime", _lastInfoURLFetchTime); PreferencesManager.setViewState("updateInfo", updateInfo); } result.resolve(updateInfo); }).fail(function (jqXHR, status, error) { // When loading data for unit tests, the error handler is // called but the responseText is valid. Try to use it here, // but *don't* save the results in prefs. if (!jqXHR.responseText) { // Text is NULL or empty string, reject(). result.reject(); return; } try { data = JSON.parse(jqXHR.responseText); result.resolve(data); } catch (e) { result.reject(); } }); }); } else { result.resolve(data); } return result.promise(); } /** * Return a new array of version information that is newer than "buildNumber". * Returns null if there is no new version information. */ function _stripOldVersionInfo(versionInfo, buildNumber) { // Do a simple linear search. Since we are going in reverse-chronological order, we // should get through the search quickly. var lastIndex = 0; var len = versionInfo.length; while (lastIndex < len) { if (versionInfo[lastIndex].buildNumber <= buildNumber) { break; } lastIndex++; } if (lastIndex > 0) { return versionInfo.slice(0, lastIndex); } // No new version info return null; } /** * Show a dialog that shows the update */ function _showUpdateNotificationDialog(updates) { Dialogs.showModalDialogUsingTemplate(Mustache.render(UpdateDialogTemplate, Strings)) .done(function (id) { if (id === Dialogs.DIALOG_BTN_DOWNLOAD) { // The first entry in the updates array has the latest download link NativeApp.openURLInDefaultBrowser(updates[0].downloadURL); } }); // Populate the update data var $dlg = $(".update-dialog.instance"), $updateList = $dlg.find(".update-info"); updates.Strings = Strings; $updateList.html(Mustache.render(UpdateListTemplate, updates)); } /** * Calculate state of notification everytime registries are downloaded - no matter who triggered the download */ function _onRegistryDownloaded() { var availableUpdates = ExtensionManager.getAvailableUpdates(); PreferencesManager.setViewState("extensionUpdateInfo", availableUpdates); PreferencesManager.setViewState("lastExtensionRegistryCheckTime", (new Date()).getTime()); $("#toolbar-extension-manager").toggleClass("updatesAvailable", availableUpdates.length > 0); } /** * Every 24 hours downloads registry information to check for update, but only if the registry download * wasn't triggered by another action (like opening extension manager) * If there isn't 24 hours elapsed from the last download, use cached information from last download * to determine state of the update notification. */ function checkForExtensionsUpdate() { var lastExtensionRegistryCheckTime = PreferencesManager.getViewState("lastExtensionRegistryCheckTime"), timeOfNextCheck = lastExtensionRegistryCheckTime + ONE_DAY, currentTime = (new Date()).getTime(); // update icon according to previously saved information var availableUpdates = PreferencesManager.getViewState("extensionUpdateInfo"); availableUpdates = ExtensionManager.cleanAvailableUpdates(availableUpdates); $("#toolbar-extension-manager").toggleClass("updatesAvailable", availableUpdates.length > 0); if (availableUpdates.length === 0) { // icon is gray, no updates available if (currentTime > timeOfNextCheck) { // downloadRegistry, will be resolved in _onRegistryDownloaded ExtensionManager.downloadRegistry().done(function () { // schedule another check in 24 hours + 2 minutes setTimeout(checkForExtensionsUpdate, ONE_DAY + TWO_MINUTES); }); } else { // schedule the download of the registry in appropriate time setTimeout(checkForExtensionsUpdate, (timeOfNextCheck - currentTime) + TWO_MINUTES); } } } /** * Check for updates. If "force" is true, update notification dialogs are always displayed * (if an update is available). If "force" is false, the update notification is only * displayed for newly available updates. * * If an update is available, show the "update available" notification icon in the title bar. * * @param {boolean} force If true, always show the notification dialog. * @param {Object} _testValues This should only be used for testing purposes. See comments for details. * @return {$.Promise} jQuery Promise object that is resolved or rejected after the update check is complete. */ function checkForUpdate(force, _testValues) { // The second param, if non-null, is an Object containing value overrides. Values // in the object temporarily override the local values. This should *only* be used for testing. // If any overrides are set, permanent changes are not made (including showing // the update notification icon and saving prefs). var oldValues; var usingOverrides = false; // true if any of the values are overridden. var result = new $.Deferred(); var versionInfoUrl; if (_testValues) { oldValues = {}; if (_testValues.hasOwnProperty("_buildNumber")) { oldValues._buildNumber = _buildNumber; _buildNumber = _testValues._buildNumber; usingOverrides = true; } if (_testValues.hasOwnProperty("_lastNotifiedBuildNumber")) { oldValues._lastNotifiedBuildNumber = _lastNotifiedBuildNumber; _lastNotifiedBuildNumber = _testValues._lastNotifiedBuildNumber; usingOverrides = true; } if (_testValues.hasOwnProperty("_versionInfoURL")) { versionInfoUrl = _testValues._versionInfoURL; usingOverrides = true; } } _getUpdateInformation(force || usingOverrides, usingOverrides, versionInfoUrl) .done(function (versionInfo) { // Get all available updates var allUpdates = _stripOldVersionInfo(versionInfo, _buildNumber); // When running directly from GitHub source (as opposed to // an installed build), _buildNumber is 0. In this case, if the // test is not forced, don't show the update notification icon or // dialog. if (_buildNumber === 0 && !force) { result.resolve(); return; } if (allUpdates) { // Always show the "update available" icon if any updates are available var $updateNotification = $("#update-notification"); $updateNotification.css("display", "block"); if (!_addedClickHandler) { _addedClickHandler = true; $updateNotification.on("click", function () { checkForUpdate(true); }); } // Only show the update dialog if force = true, or if the user hasn't been // alerted of this update if (force || allUpdates[0].buildNumber > _lastNotifiedBuildNumber) { _showUpdateNotificationDialog(allUpdates); // Update prefs with the last notified build number _lastNotifiedBuildNumber = allUpdates[0].buildNumber; // Don't save prefs is we have overridden values if (!usingOverrides) { PreferencesManager.setViewState("lastNotifiedBuildNumber", _lastNotifiedBuildNumber); } } } else if (force) { // No updates are available. If force == true, let the user know. Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, Strings.NO_UPDATE_TITLE, Strings.NO_UPDATE_MESSAGE ); } if (oldValues) { if (oldValues.hasOwnProperty("_buildNumber")) { _buildNumber = oldValues._buildNumber; } if (oldValues.hasOwnProperty("_lastNotifiedBuildNumber")) { _lastNotifiedBuildNumber = oldValues._lastNotifiedBuildNumber; } } result.resolve(); }) .fail(function () { // Error fetching the update data. If this is a forced check, alert the user if (force) { Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, Strings.ERROR_FETCHING_UPDATE_INFO_TITLE, Strings.ERROR_FETCHING_UPDATE_INFO_MSG ); } result.reject(); }); return result.promise(); } /** * Launches both check for Brackets update and check for installed extensions update */ function launchAutomaticUpdate() { // launch immediately and then every 24 hours + 2 minutes checkForUpdate(); checkForExtensionsUpdate(); window.setInterval(checkForUpdate, ONE_DAY + TWO_MINUTES); } // Events listeners $(ExtensionManager).on("registryDownload", _onRegistryDownloaded); // Define public API exports.launchAutomaticUpdate = launchAutomaticUpdate; exports.checkForUpdate = checkForUpdate; });
/** * Get a data structure that has information for all builds of Brackets. * * If force is true, the information is always fetched from _versionInfoURL. * If force is false, we try to use cached information. If more than * 24 hours have passed since the last fetch, or if cached data can't be found, * the data is fetched again. * * If new data is fetched and dontCache is false, the data is saved in preferences * for quick fetching later. * _versionInfoUrl is used for unit testing. */ function _getUpdateInformation(force, dontCache, _versionInfoUrl) { // Last time the versionInfoURL was fetched var lastInfoURLFetchTime = PreferencesManager.getViewState("lastInfoURLFetchTime"); var result = new $.Deferred(); var fetchData = false; var data; // If force is true, always fetch if (force) { fetchData = true; } // If we don't have data saved in prefs, fetch data = PreferencesManager.getViewState("updateInfo"); if (!data) { fetchData = true; } // If more than 24 hours have passed since our last fetch, fetch again if ((new Date()).getTime() > lastInfoURLFetchTime + ONE_DAY) { fetchData = true; } if (fetchData) { var lookupPromise = new $.Deferred(), localVersionInfoUrl; // If the current locale isn't "en" or "en-US", check whether we actually have a // locale-specific update notification, and fall back to "en" if not. // Note: we check for both "en" and "en-US" to watch for the general case or // country-specific English locale. The former appears default on Mac, while // the latter appears default on Windows. var locale = brackets.getLocale().toLowerCase(); if (locale !== "en" && locale !== "en-us") { localVersionInfoUrl = _versionInfoUrl || _getVersionInfoUrl(); $.ajax({ url: localVersionInfoUrl, cache: false, type: "HEAD" }).fail(function (jqXHR, status, error) { // get rid of any country information from locale and try again var tmpUrl = _getVersionInfoUrl(brackets.getLocale(), true); if (tmpUrl !== localVersionInfoUrl) { $.ajax({ url: tmpUrl, cache: false, type: "HEAD" }).fail(function (jqXHR, status, error) { localVersionInfoUrl = _getVersionInfoUrl("en"); }).done(function (jqXHR, status, error) { localVersionInfoUrl = tmpUrl; }).always(function (jqXHR, status, error) { lookupPromise.resolve(); }); } else { localVersionInfoUrl = _getVersionInfoUrl("en"); lookupPromise.resolve(); } }).done(function (jqXHR, status, error) { lookupPromise.resolve(); }); } else { localVersionInfoUrl = _versionInfoUrl || _getVersionInfoUrl("en"); lookupPromise.resolve(); } lookupPromise.done(function () { $.ajax({ url: localVersionInfoUrl, dataType: "json", cache: false }).done(function (updateInfo, textStatus, jqXHR) { if (!dontCache) { lastInfoURLFetchTime = (new Date()).getTime(); PreferencesManager.setViewState("lastInfoURLFetchTime", lastInfoURLFetchTime); PreferencesManager.setViewState("updateInfo", updateInfo); } result.resolve(updateInfo); }).fail(function (jqXHR, status, error) { // When loading data for unit tests, the error handler is // called but the responseText is valid. Try to use it here, // but *don't* save the results in prefs. if (!jqXHR.responseText) { // Text is NULL or empty string, reject(). result.reject(); return; } try { data = JSON.parse(jqXHR.responseText); result.resolve(data); } catch (e) { result.reject(); } }); }); } else { result.resolve(data); } return result.promise(); }
/** * Adds resizing and (optionally) expand/collapse capabilities to a given html element. The element's size * & visibility are automatically saved & restored as a view-state preference. * * Resizing can be configured in two directions: * - Vertical ("vert"): Resizes the height of the element * - Horizontal ("horz"): Resizes the width of the element * * Resizer handlers can be positioned on the element at: * - Top ("top") or bottom ("bottom") for vertical resizing * - Left ("left") or right ("right") for horizontal resizing * * A resizable element triggers the following events while resizing: * - panelResizeStart: When the resize starts. Passed the new size. * - panelResizeUpdate: When the resize gets updated. Passed the new size. * - panelResizeEnd: When the resize ends. Passed the final size. * - panelCollapsed: When the panel gets collapsed (or hidden). Passed the last size * before collapse. May occur without any resize events. * - panelExpanded: When the panel gets expanded (or shown). Passed the initial size. * May occur without any resize events. * * @param {!DOMNode} element DOM element which should be made resizable. Must have an id attribute, for * use as a preferences key. * @param {!string} direction Direction of the resize action: one of the DIRECTION_* constants. * @param {!string} position Which side of the element can be dragged: one of the POSITION_* constants * (TOP/BOTTOM for vertical resizing or LEFT/RIGHT for horizontal). * @param {?number} minSize Minimum size (width or height) of the element's outer dimensions, including * border & padding. Defaults to DEFAULT_MIN_SIZE. * @param {?boolean} collapsible Indicates the panel is collapsible on double click on the * resizer. Defaults to false. * @param {?string} forceLeft CSS selector indicating element whose 'left' should be locked to the * the resizable element's size (useful for siblings laid out to the right of * the element). Must lie in element's parent's subtree. * @param {?boolean} createdByWorkspaceManager For internal use only * @param {?boolean} usePercentages Maintain the size of the element as a percentage of its parent * the default is to maintain the size of the element in pixels * @param {?boolean} _attachToParent Attaches the resizer element to parent of the element rather than * to element itself. Attach the resizer to the parent *ONLY* if element has the * same offset as parent otherwise the resizer will be incorrectly positioned. * FOR INTERNAL USE ONLY */ function makeResizable(element, direction, position, minSize, collapsible, forceLeft, createdByWorkspaceManager, usePercentages, _attachToParent) { var $resizer = $('<div class="' + direction + '-resizer"></div>'), $element = $(element), $parent = $element.parent(), $resizableElement = $($element.find(".resizable-content:first")[0]), $body = $(window.document.body), elementID = $element.attr("id"), elementPrefs = PreferencesManager.getViewState(elementID) || {}, animationRequest = null, directionProperty = direction === DIRECTION_HORIZONTAL ? "clientX" : "clientY", directionIncrement = (position === POSITION_TOP || position === POSITION_LEFT) ? 1 : -1, parentSizeFunction = direction === DIRECTION_HORIZONTAL ? $parent.innerWidth : $parent.innerHeight, elementSizeFunction = function (newSize) { if (!newSize) { // calling the function as a getter if (direction === DIRECTION_HORIZONTAL) { return this.width(); } else { return this.height(); } } else if (!usePercentages) { if (direction === DIRECTION_HORIZONTAL) { return this.width(newSize); } else { return this.height(newSize); } } else { // calling the function as a setter var parentSize = parentSizeFunction.apply($parent), percentage, prop; if (direction === DIRECTION_HORIZONTAL) { prop = "width"; } else { prop = "height"; } percentage = newSize / parentSize; this.css(prop, (percentage * 100) + "%"); return this; // chainable } }, resizerCSSPosition = direction === DIRECTION_HORIZONTAL ? "left" : "top", contentSizeFunction = direction === DIRECTION_HORIZONTAL ? $resizableElement.width : $resizableElement.height; if (PreferencesManager.get(PREFS_PURE_CODE) && ($element.hasClass("bottom-panel") || $element.hasClass("sidebar"))) { elementPrefs.visible = false; } if (!elementID) { console.error("Resizable panels must have a DOM id to use as a preferences key:", element); return; } // Detect legacy cases where panels in the editor area are created without using WorkspaceManager APIs if ($parent[0] && $parent.is(".content") && !createdByWorkspaceManager) { console.error("Resizable panels within the editor area should be created via WorkspaceManager.createBottomPanel(). \nElement:", element); return; } if (minSize === undefined) { minSize = DEFAULT_MIN_SIZE; } collapsible = collapsible || false; if (_attachToParent) { $parent.prepend($resizer); } else { $element.prepend($resizer); } // Important so min/max sizes behave predictably $element.css("box-sizing", "border-box"); function adjustSibling(size) { if (forceLeft !== undefined) { $(forceLeft, $parent).css("left", size); } } function resizeElement(elementSize, contentSize) { elementSizeFunction.apply($element, [elementSize]); if ($resizableElement.length) { contentSizeFunction.apply($resizableElement, [contentSize]); } } // If the resizer is positioned right or bottom of the panel, we need to listen to // reposition it if the element size changes externally function repositionResizer(elementSize) { var resizerPosition = elementSize || 1; if (position === POSITION_RIGHT || position === POSITION_BOTTOM) { $resizer.css(resizerCSSPosition, resizerPosition); } } $element.data("removeSizable", function () { $resizer.off(".resizer"); $element.removeData("show"); $element.removeData("hide"); $element.removeData("resyncSizer"); $element.removeData("removeSizable"); $resizer.remove(); }); $element.data("resyncSizer", function () { repositionResizer(elementSizeFunction.apply($element)); }); $element.data("show", function () { var elementOffset = $element.offset(), elementSize = elementSizeFunction.apply($element) || elementPrefs.size, contentSize = contentSizeFunction.apply($resizableElement) || elementPrefs.contentSize; // Resize the element before showing it again. If the panel was collapsed by dragging // the resizer, the size of the element should be 0, so we restore size in preferences resizeElement(elementSize, contentSize); $element.show(); elementPrefs.visible = true; if (collapsible) { if (_attachToParent) { $parent.prepend($resizer); } else { $element.prepend($resizer); } if (position === POSITION_TOP) { $resizer.css(resizerCSSPosition, ""); } else if (position === POSITION_RIGHT) { $resizer.css(resizerCSSPosition, elementOffset[resizerCSSPosition] + elementSize); } } adjustSibling(elementSize); $element.trigger("panelExpanded", [elementSize]); PreferencesManager.setViewState(elementID, elementPrefs, null, isResizing); }); $element.data("hide", function () { var elementOffset = $element.offset(), elementSize = elementSizeFunction.apply($element), resizerSize = elementSizeFunction.apply($resizer); $element.hide(); elementPrefs.visible = false; if (collapsible) { $resizer.insertBefore($element); if (position === POSITION_RIGHT) { $resizer.css(resizerCSSPosition, ""); } else if (position === POSITION_TOP) { $resizer.css(resizerCSSPosition, elementOffset[resizerCSSPosition] + elementSize - resizerSize); } } adjustSibling(0); $element.trigger("panelCollapsed", [elementSize]); PreferencesManager.setViewState(elementID, elementPrefs, null, isResizing); }); $resizer.on("mousedown.resizer", function (e) { var $resizeShield = $("<div class='resizing-container " + direction + "-resizing' />"), startPosition = e[directionProperty], startSize = $element.is(":visible") ? elementSizeFunction.apply($element) : 0, newSize = startSize, previousSize = startSize, baseSize = 0, resizeStarted = false; isResizing = true; $body.append($resizeShield); if ($resizableElement.length) { $element.children().not(".horz-resizer, .vert-resizer, .resizable-content").each(function (index, child) { if (direction === DIRECTION_HORIZONTAL) { baseSize += $(child).outerWidth(); } else { baseSize += $(child).outerHeight(); } }); } function doRedraw() { // only run this if the mouse is down so we don't constantly loop even // after we're done resizing. if (!isResizing) { return; } // Check for real size changes to avoid unnecessary resizing and events if (newSize !== previousSize) { previousSize = newSize; if ($element.is(":visible")) { if (newSize < 10) { toggle($element); elementSizeFunction.apply($element, [0]); } else { // Trigger resizeStarted just before the first successful resize update if (!resizeStarted) { resizeStarted = true; $element.trigger("panelResizeStart", newSize); } // Resize the main element to the new size. If there is a content element, // its size is the new size minus the size of the non-resizable elements resizeElement(newSize, (newSize - baseSize)); adjustSibling(newSize); $element.trigger("panelResizeUpdate", [newSize]); } } else if (newSize > 10) { elementSizeFunction.apply($element, [newSize]); toggle($element); // Trigger resizeStarted after expanding the element if it was previously collapsed if (!resizeStarted) { resizeStarted = true; $element.trigger("panelResizeStart", newSize); } } } animationRequest = window.requestAnimationFrame(doRedraw); } function onMouseMove(e) { // calculate newSize adding to startSize the difference // between starting and current position, capped at minSize newSize = Math.max(startSize + directionIncrement * (startPosition - e[directionProperty]), minSize); // respect max size if one provided (e.g. by WorkspaceManager) var maxSize = $element.data("maxsize"); if (maxSize !== undefined) { newSize = Math.min(newSize, maxSize); } e.preventDefault(); if (animationRequest === null) { animationRequest = window.requestAnimationFrame(doRedraw); } } $(window.document).on("mousemove", onMouseMove); // If the element is marked as collapsible, check for double click // to toggle the element visibility if (collapsible) { $resizeShield.on("mousedown", function (e) { $(window.document).off("mousemove", onMouseMove); $resizeShield.off("mousedown"); $resizeShield.remove(); animationRequest = null; toggle($element); }); } function endResize(e) { if (isResizing) { var elementSize = elementSizeFunction.apply($element); if ($element.is(":visible")) { elementPrefs.size = elementSize; if ($resizableElement.length) { elementPrefs.contentSize = contentSizeFunction.apply($resizableElement); } PreferencesManager.setViewState(elementID, elementPrefs); repositionResizer(elementSize); } isResizing = false; if (resizeStarted) { $element.trigger("panelResizeEnd", [elementSize]); } // We wait 300ms to remove the resizer container to capture a mousedown // on the container that would account for double click window.setTimeout(function () { $(window.document).off("mousemove", onMouseMove); $resizeShield.off("mousedown"); $resizeShield.remove(); animationRequest = null; }, 300); } } $(window.document).one("mouseup", endResize); e.preventDefault(); }); // Panel preferences initialization if (elementPrefs) { if (elementPrefs.size !== undefined) { elementSizeFunction.apply($element, [elementPrefs.size]); } if (elementPrefs.contentSize !== undefined) { contentSizeFunction.apply($resizableElement, [elementPrefs.contentSize]); } if (elementPrefs.visible !== undefined && !elementPrefs.visible) { hide($element); } else { adjustSibling(elementSizeFunction.apply($element)); repositionResizer(elementSizeFunction.apply($element)); } } }
define(function main(require, exports, module) { "use strict"; var DocumentManager = require("document/DocumentManager"), Commands = require("command/Commands"), AppInit = require("utils/AppInit"), LiveDevelopment = require("LiveDevelopment/LiveDevelopment"), Inspector = require("LiveDevelopment/Inspector/Inspector"), CommandManager = require("command/CommandManager"), PreferencesManager = require("preferences/PreferencesManager"), Dialogs = require("widgets/Dialogs"), DefaultDialogs = require("widgets/DefaultDialogs"), UrlParams = require("utils/UrlParams").UrlParams, Strings = require("strings"), ExtensionUtils = require("utils/ExtensionUtils"), StringUtils = require("utils/StringUtils"); var params = new UrlParams(); var config = { experimental: false, // enable experimental features debug: true, // enable debug output and helpers autoconnect: false, // go live automatically after startup? highlight: true, // enable highlighting? highlightConfig: { // the highlight configuration for the Inspector borderColor: {r: 255, g: 229, b: 153, a: 0.66}, contentColor: {r: 111, g: 168, b: 220, a: 0.55}, marginColor: {r: 246, g: 178, b: 107, a: 0.66}, paddingColor: {r: 147, g: 196, b: 125, a: 0.66}, showInfo: true } }; var _checkMark = "✓"; // Check mark character // Status labels/styles are ordered: error, not connected, progress1, progress2, connected. var _statusTooltip = [ Strings.LIVE_DEV_STATUS_TIP_NOT_CONNECTED, Strings.LIVE_DEV_STATUS_TIP_NOT_CONNECTED, Strings.LIVE_DEV_STATUS_TIP_PROGRESS1, Strings.LIVE_DEV_STATUS_TIP_PROGRESS2, Strings.LIVE_DEV_STATUS_TIP_CONNECTED, Strings.LIVE_DEV_STATUS_TIP_OUT_OF_SYNC, Strings.LIVE_DEV_STATUS_TIP_SYNC_ERROR ]; var _statusStyle = ["warning", "", "info", "info", "success", "out-of-sync", "sync-error"]; // Status indicator's CSS class var _allStatusStyles = _statusStyle.join(" "); var _$btnGoLive; // reference to the GoLive button var _$btnHighlight; // reference to the HighlightButton /** Load Live Development LESS Style */ function _loadStyles() { var lessText = require("text!LiveDevelopment/main.less"), parser = new less.Parser(); parser.parse(lessText, function onParse(err, tree) { console.assert(!err, err); ExtensionUtils.addEmbeddedStyleSheet(tree.toCSS()); }); } /** * Change the appearance of a button. Omit text to remove any extra text; omit style to return to default styling; * omit tooltip to leave tooltip unchanged. */ function _setLabel($btn, text, style, tooltip) { // Clear text/styles from previous status $("span", $btn).remove(); $btn.removeClass(_allStatusStyles); // Set text/styles for new status if (text && text.length > 0) { $("<span class=\"label\">") .addClass(style) .text(text) .appendTo($btn); } else { $btn.addClass(style); } if (tooltip) { $btn.attr("title", tooltip); } } /** * Toggles LiveDevelopment and synchronizes the state of UI elements that reports LiveDevelopment status * * Stop Live Dev when in an active state (ACTIVE, OUT_OF_SYNC, SYNC_ERROR). * Start Live Dev when in an inactive state (ERROR, INACTIVE). * Do nothing when in a connecting state (CONNECTING, LOADING_AGENTS). */ function _handleGoLiveCommand() { if (LiveDevelopment.status >= LiveDevelopment.STATUS_ACTIVE) { LiveDevelopment.close(); } else if (LiveDevelopment.status <= LiveDevelopment.STATUS_INACTIVE) { if (!params.get("skipLiveDevelopmentInfo") && !PreferencesManager.getViewState("livedev.afterFirstLaunch")) { PreferencesManager.setViewState("livedev.afterFirstLaunch", "true"); Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_INFO, Strings.LIVE_DEVELOPMENT_INFO_TITLE, Strings.LIVE_DEVELOPMENT_INFO_MESSAGE ).done(function (id) { LiveDevelopment.open(); }); } else { LiveDevelopment.open(); } } } /** Called on status change */ function _showStatusChangeReason(reason) { // Destroy the previous twipsy (options are not updated otherwise) _$btnGoLive.twipsy("hide").removeData("twipsy"); // If there was no reason or the action was an explicit request by the user, don't show a twipsy if (!reason || reason === "explicit_close") { return; } // Translate the reason var translatedReason = Strings["LIVE_DEV_" + reason.toUpperCase()]; if (!translatedReason) { translatedReason = StringUtils.format(Strings.LIVE_DEV_CLOSED_UNKNOWN_REASON, reason); } // Configure the twipsy var options = { placement: "left", trigger: "manual", autoHideDelay: 5000, title: function () { return translatedReason; } }; // Show the twipsy with the explanation _$btnGoLive.twipsy(options).twipsy("show"); } /** Create the menu item "Go Live" */ function _setupGoLiveButton() { _$btnGoLive = $("#toolbar-go-live"); _$btnGoLive.click(function onGoLive() { _handleGoLiveCommand(); }); $(LiveDevelopment).on("statusChange", function statusChange(event, status, reason) { // status starts at -1 (error), so add one when looking up name and style // See the comments at the top of LiveDevelopment.js for details on the // various status codes. _setLabel(_$btnGoLive, null, _statusStyle[status + 1], _statusTooltip[status + 1]); _showStatusChangeReason(reason); if (config.autoconnect) { window.sessionStorage.setItem("live.enabled", status === 3); } }); // Initialize tooltip for 'not connected' state _setLabel(_$btnGoLive, null, _statusStyle[1], _statusTooltip[1]); } /** Maintains state of the Live Preview menu item */ function _setupGoLiveMenu() { $(LiveDevelopment).on("statusChange", function statusChange(event, status) { // Update the checkmark next to 'Live Preview' menu item // Add checkmark when status is STATUS_ACTIVE; otherwise remove it CommandManager.get(Commands.FILE_LIVE_FILE_PREVIEW).setChecked(status === LiveDevelopment.STATUS_ACTIVE); CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setEnabled(status === LiveDevelopment.STATUS_ACTIVE); }); } function _updateHighlightCheckmark() { CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setChecked(config.highlight); } function _handlePreviewHighlightCommand() { config.highlight = !config.highlight; _updateHighlightCheckmark(); if (config.highlight) { LiveDevelopment.showHighlight(); } else { LiveDevelopment.hideHighlight(); } PreferencesManager.setViewState("livedev.highlight", config.highlight); } /** Setup window references to useful LiveDevelopment modules */ function _setupDebugHelpers() { window.ld = LiveDevelopment; window.i = Inspector; window.report = function report(params) { window.params = params; console.info(params); }; } /** Initialize LiveDevelopment */ AppInit.appReady(function () { params.parse(); Inspector.init(config); LiveDevelopment.init(config); _loadStyles(); _setupGoLiveButton(); _setupGoLiveMenu(); _updateHighlightCheckmark(); if (config.debug) { _setupDebugHelpers(); } // trigger autoconnect if (config.autoconnect && window.sessionStorage.getItem("live.enabled") === "true" && DocumentManager.getCurrentDocument()) { _handleGoLiveCommand(); } // Redraw highlights when window gets focus. This ensures that the highlights // will be in sync with any DOM changes that may have occurred. $(window).focus(function () { if (Inspector.connected() && config.highlight) { LiveDevelopment.redrawHighlight(); } }); }); // init prefs PreferencesManager.stateManager.definePreference("livedev.highlight", "boolean", true) .on("change", function () { config.highlight = PreferencesManager.getViewState("livedev.highlight"); _updateHighlightCheckmark(); }); PreferencesManager.convertPreferences(module, { "highlight": "user livedev.highlight", "afterFirstLaunch": "user livedev.afterFirstLaunch" }, true); config.highlight = PreferencesManager.getViewState("livedev.highlight"); // init commands CommandManager.register(Strings.CMD_LIVE_FILE_PREVIEW, Commands.FILE_LIVE_FILE_PREVIEW, _handleGoLiveCommand); CommandManager.register(Strings.CMD_LIVE_HIGHLIGHT, Commands.FILE_LIVE_HIGHLIGHT, _handlePreviewHighlightCommand); CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setEnabled(false); // Export public functions });