diff --git a/spec/history-manager-spec.js b/spec/history-manager-spec.js index 425f1efe0..ed6aac626 100644 --- a/spec/history-manager-spec.js +++ b/spec/history-manager-spec.js @@ -4,27 +4,24 @@ import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' import {Emitter, Disposable, CompositeDisposable} from 'event-kit' import {HistoryManager, HistoryProject} from '../src/history-manager' +import StateStore from '../src/state-store' describe("HistoryManager", () => { - let historyManager, commandRegistry, project, localStorage, stateStore + let historyManager, commandRegistry, project, stateStore let commandDisposable, projectDisposable - beforeEach(() => { + beforeEach(async () => { commandDisposable = jasmine.createSpyObj('Disposable', ['dispose']) commandRegistry = jasmine.createSpyObj('CommandRegistry', ['add']) commandRegistry.add.andReturn(commandDisposable) - localStorage = jasmine.createSpyObj('LocalStorage', ['getItem', 'setItem']) - localStorage.items = { - history: JSON.stringify({ - projects: [ - { paths: ['/1', 'c:\\2'], lastOpened: new Date(2016, 9, 17, 17, 16, 23) }, - { paths: ['/test'], lastOpened: new Date(2016, 9, 17, 11, 12, 13) } - ] - }) - } - localStorage.getItem.andCallFake((key) => localStorage.items[key]) - localStorage.setItem.andCallFake((key, value) => localStorage.items[key] = value) + stateStore = new StateStore('history-manager-test', 1) + await stateStore.save('history-manager', { + projects: [ + {paths: ['/1', 'c:\\2'], lastOpened: new Date(2016, 9, 17, 17, 16, 23)}, + {paths: ['/test'], lastOpened: new Date(2016, 9, 17, 11, 12, 13)} + ] + }) projectDisposable = jasmine.createSpyObj('Disposable', ['dispose']) project = jasmine.createSpyObj('Project', ['onDidChangePaths']) @@ -33,7 +30,12 @@ describe("HistoryManager", () => { return projectDisposable }) - historyManager = new HistoryManager({project, commands:commandRegistry, localStorage}) + historyManager = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry}) + await historyManager.loadState() + }) + + afterEach(async () => { + await stateStore.clear() }) describe("constructor", () => { @@ -65,33 +67,28 @@ describe("HistoryManager", () => { }) describe("clearProjects", () => { - it("clears the list of projects", () => { + it("clears the list of projects", async () => { expect(historyManager.getProjects().length).not.toBe(0) - historyManager.clearProjects() + await historyManager.clearProjects() expect(historyManager.getProjects().length).toBe(0) }) - it("saves the state", () => { - expect(localStorage.setItem).not.toHaveBeenCalled() - historyManager.clearProjects() - expect(localStorage.setItem).toHaveBeenCalled() - expect(localStorage.setItem.calls[0].args[0]).toBe('history') + it("saves the state", async () => { + await historyManager.clearProjects() + const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry}) + await historyManager2.loadState() expect(historyManager.getProjects().length).toBe(0) }) - it("fires the onDidChangeProjects event", () => { - expect(localStorage.setItem).not.toHaveBeenCalled() - historyManager.clearProjects() - expect(localStorage.setItem).toHaveBeenCalled() - expect(localStorage.setItem.calls[0].args[0]).toBe('history') + it("fires the onDidChangeProjects event", async () => { + const didChangeSpy = jasmine.createSpy() + historyManager.onDidChangeProjects(didChangeSpy) + await historyManager.clearProjects() expect(historyManager.getProjects().length).toBe(0) + expect(didChangeSpy).toHaveBeenCalled() }) }) - it("loads state", () => { - expect(localStorage.getItem).toHaveBeenCalledWith('history') - }) - it("listens to project.onDidChangePaths adding a new project", () => { const start = new Date() project.didChangePathsListener(['/a/new', '/path/or/two']) @@ -112,61 +109,61 @@ describe("HistoryManager", () => { }) describe("loadState", () => { - it("defaults to an empty array if no state", () => { - localStorage.items.history = null - historyManager.loadState() + it("defaults to an empty array if no state", async () => { + await stateStore.clear() + await historyManager.loadState() expect(historyManager.getProjects()).toEqual([]) }) - it("defaults to an empty array if no projects", () => { - localStorage.items.history = JSON.stringify('') - historyManager.loadState() + it("defaults to an empty array if no projects", async () => { + await stateStore.save('history-manager', {}) + await historyManager.loadState() expect(historyManager.getProjects()).toEqual([]) }) }) describe("addProject", () => { - it("adds a new project to the end", () => { + it("adds a new project to the end", async () => { const date = new Date(2010, 10, 9, 8, 7, 6) - historyManager.addProject(['/a/b'], date) + await historyManager.addProject(['/a/b'], date) const projects = historyManager.getProjects() expect(projects.length).toBe(3) expect(projects[2].paths).toEqual(['/a/b']) expect(projects[2].lastOpened).toBe(date) }) - it("adds a new project to the start", () => { + it("adds a new project to the start", async () => { const date = new Date() - historyManager.addProject(['/so/new'], date) + await historyManager.addProject(['/so/new'], date) const projects = historyManager.getProjects() expect(projects.length).toBe(3) expect(projects[0].paths).toEqual(['/so/new']) expect(projects[0].lastOpened).toBe(date) }) - it("updates an existing project and moves it to the start", () => { + it("updates an existing project and moves it to the start", async () => { const date = new Date() - historyManager.addProject(['/test'], date) + await historyManager.addProject(['/test'], date) const projects = historyManager.getProjects() expect(projects.length).toBe(2) expect(projects[0].paths).toEqual(['/test']) expect(projects[0].lastOpened).toBe(date) }) - it("fires the onDidChangeProjects event when adding a project", () => { + it("fires the onDidChangeProjects event when adding a project", async () => { const didChangeSpy = jasmine.createSpy() const beforeCount = historyManager.getProjects().length historyManager.onDidChangeProjects(didChangeSpy) - historyManager.addProject(['/test-new'], new Date()) + await historyManager.addProject(['/test-new'], new Date()) expect(didChangeSpy).toHaveBeenCalled() expect(historyManager.getProjects().length).toBe(beforeCount + 1) }) - it("fires the onDidChangeProjects event when updating a project", () => { + it("fires the onDidChangeProjects event when updating a project", async () => { const didChangeSpy = jasmine.createSpy() const beforeCount = historyManager.getProjects().length historyManager.onDidChangeProjects(didChangeSpy) - historyManager.addProject(['/test'], new Date()) + await historyManager.addProject(['/test'], new Date()) expect(didChangeSpy).toHaveBeenCalled() expect(historyManager.getProjects().length).toBe(beforeCount) }) @@ -186,14 +183,12 @@ describe("HistoryManager", () => { }) describe("saveState" ,() => { - it("saves the state", () => { - historyManager.addProject(["/save/state"]) - historyManager.saveState() - expect(localStorage.setItem).toHaveBeenCalled() - expect(localStorage.setItem.calls[0].args[0]).toBe('history') - expect(localStorage.items['history']).toContain('/save/state') - historyManager.loadState() - expect(historyManager.getProjects()[0].paths).toEqual(['/save/state']) + it("saves the state", async () => { + await historyManager.addProject(["/save/state"]) + await historyManager.saveState() + const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry}) + await historyManager2.loadState() + expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state']) }) }) }) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 766ba7aa8..b247fe7c2 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -1,5 +1,5 @@ _ = require 'underscore-plus' -{screen, ipcRenderer, remote, shell, webFrame} = require 'electron' +{ipcRenderer, remote, shell} = require 'electron' ipcHelpers = require './ipc-helpers' {Disposable} = require 'event-kit' getWindowLoadSettings = require './get-window-load-settings' @@ -80,6 +80,12 @@ class ApplicationDelegate setWindowFullScreen: (fullScreen=false) -> ipcHelpers.call('window-method', 'setFullScreen', fullScreen) + onDidEnterFullScreen: (callback) -> + ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback) + + onDidLeaveFullScreen: (callback) -> + ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback) + openWindowDevTools: -> # Defer DevTools interaction to the next tick, because using them during # event handling causes some wrong input events to be triggered on @@ -254,20 +260,6 @@ class ApplicationDelegate openExternal: (url) -> shell.openExternal(url) - disableZoom: -> - outerCallback = -> - webFrame.setZoomLevelLimits(1, 1) - - outerCallback() - # Set the limits every time a display is added or removed, otherwise the - # configuration gets reset to the default, which allows zooming the - # webframe. - screen.on('display-added', outerCallback) - screen.on('display-removed', outerCallback) - new Disposable -> - screen.removeListener('display-added', outerCallback) - screen.removeListener('display-removed', outerCallback) - checkForUpdate: -> ipcRenderer.send('command', 'application:check-for-update') diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 70a414dcb..4a503aca4 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -215,8 +215,6 @@ class AtomEnvironment extends Model @stylesElement = @styles.buildStylesElement() @document.head.appendChild(@stylesElement) - @disposables.add(@applicationDelegate.disableZoom()) - @keymaps.subscribeToFileReadFailure() @keymaps.loadBundledKeymaps() @@ -231,14 +229,12 @@ class AtomEnvironment extends Model @observeAutoHideMenuBar() - @history = new HistoryManager({@project, @commands, localStorage}) + @history = new HistoryManager({@project, @commands, @stateStore, localStorage: window.localStorage}) # Keep instances of HistoryManager in sync - @history.onDidChangeProjects (e) => + @disposables.add @history.onDidChangeProjects (e) => @applicationDelegate.didChangeHistoryManager() unless e.reloaded @disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState()) - (new ReopenProjectMenuManager({@menu, @commands, @history, @config, open: (paths) => @open(pathsToOpen: paths)})).update() - attachSaveStateListeners: -> saveState = _.debounce((=> window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded @@ -716,7 +712,14 @@ class AtomEnvironment extends Model @openInitialEmptyEditorIfNecessary() - Promise.all([loadStatePromise, updateProcessEnvPromise]) + loadHistoryPromise = @history.loadState().then => + @reopenProjectMenuManager = new ReopenProjectMenuManager({ + @menu, @commands, @history, @config, + open: (paths) => @open(pathsToOpen: paths) + }) + @reopenProjectMenuManager.update() + + Promise.all([loadStatePromise, loadHistoryPromise, updateProcessEnvPromise]) serialize: (options) -> version: @constructor.version diff --git a/src/get-window-load-settings.js b/src/get-window-load-settings.js index 7ee465141..d35b24213 100644 --- a/src/get-window-load-settings.js +++ b/src/get-window-load-settings.js @@ -4,7 +4,7 @@ let windowLoadSettings = null module.exports = () => { if (!windowLoadSettings) { - windowLoadSettings = remote.getCurrentWindow().loadSettings + windowLoadSettings = JSON.parse(remote.getCurrentWindow().loadSettingsJSON) } return windowLoadSettings } diff --git a/src/history-manager.js b/src/history-manager.js index f013957b9..e3dd46653 100644 --- a/src/history-manager.js +++ b/src/history-manager.js @@ -1,6 +1,6 @@ /** @babel */ -import {Emitter} from 'event-kit' +import {Emitter, CompositeDisposable} from 'event-kit' // Extended: History manager for remembering which projects have been opened. // @@ -8,12 +8,18 @@ import {Emitter} from 'event-kit' // // The project history is used to enable the 'Reopen Project' menu. export class HistoryManager { - constructor ({project, commands, localStorage}) { + constructor ({stateStore, localStorage, project, commands}) { + this.stateStore = stateStore this.localStorage = localStorage - commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)}) this.emitter = new Emitter() - this.loadState() - project.onDidChangePaths((projectPaths) => this.addProject(projectPaths)) + this.projects = [] + this.disposables = new CompositeDisposable() + this.disposables.add(commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)})) + this.disposables.add(project.onDidChangePaths((projectPaths) => this.addProject(projectPaths))) + } + + destroy () { + this.disposables.dispose() } // Public: Obtain a list of previously opened projects. @@ -27,9 +33,12 @@ export class HistoryManager { // // Note: This is not a privacy function - other traces will still exist, // e.g. window state. - clearProjects () { + // + // Return a {Promise} that resolves when the history has been successfully + // cleared. + async clearProjects () { this.projects = [] - this.saveState() + await this.saveState() this.didChangeProjects() } @@ -46,7 +55,7 @@ export class HistoryManager { this.emitter.emit('did-change-projects', args || { reloaded: false }) } - addProject (paths, lastOpened) { + async addProject (paths, lastOpened) { if (paths.length === 0) return let project = this.getProject(paths) @@ -57,11 +66,11 @@ export class HistoryManager { project.lastOpened = lastOpened || new Date() this.projects.sort((a, b) => b.lastOpened - a.lastOpened) - this.saveState() + await this.saveState() this.didChangeProjects() } - removeProject (paths) { + async removeProject (paths) { if (paths.length === 0) return let project = this.getProject(paths) @@ -70,7 +79,7 @@ export class HistoryManager { let index = this.projects.indexOf(project) this.projects.splice(index, 1) - this.saveState() + await this.saveState() this.didChangeProjects() } @@ -84,31 +93,23 @@ export class HistoryManager { return null } - loadState () { - const state = JSON.parse(this.localStorage.getItem('history')) - if (state && state.projects) { - this.projects = state.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened))) - this.didChangeProjects({ reloaded: true }) + async loadState () { + let history = await this.stateStore.load('history-manager') + if (!history) { + history = JSON.parse(this.localStorage.getItem('history')) + } + + if (history && history.projects) { + this.projects = history.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened))) + this.didChangeProjects({reloaded: true}) } else { this.projects = [] } } - saveState () { - const state = JSON.stringify({ - projects: this.projects.map(p => ({ - paths: p.paths, lastOpened: p.lastOpened - })) - }) - this.localStorage.setItem('history', state) - } - - async importProjectHistory () { - for (let project of await HistoryImporter.getAllProjects()) { - this.addProject(project.paths, project.lastOpened) - } - this.saveState() - this.didChangeProjects() + async saveState () { + const projects = this.projects.map(p => ({paths: p.paths, lastOpened: p.lastOpened})) + await this.stateStore.save('history-manager', {projects}) } } @@ -132,32 +133,3 @@ export class HistoryProject { set lastOpened (lastOpened) { this._lastOpened = lastOpened } get lastOpened () { return this._lastOpened } } - -class HistoryImporter { - static async getStateStoreCursor () { - const db = await atom.stateStore.dbPromise - const store = db.transaction(['states']).objectStore('states') - return store.openCursor() - } - - static async getAllProjects (stateStore) { - const request = await HistoryImporter.getStateStoreCursor() - return new Promise((resolve, reject) => { - const rows = [] - request.onerror = reject - request.onsuccess = event => { - const cursor = event.target.result - if (cursor) { - let project = cursor.value.value.project - let storedAt = cursor.value.storedAt - if (project && project.paths && storedAt) { - rows.push(new HistoryProject(project.paths, new Date(Date.parse(storedAt)))) - } - cursor.continue() - } else { - resolve(rows) - } - } - }) - } -} diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index d21ebae45..0e364393a 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -6,8 +6,8 @@ StorageFolder = require '../storage-folder' Config = require '../config' FileRecoveryService = require './file-recovery-service' ipcHelpers = require '../ipc-helpers' -{BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron' -{CompositeDisposable} = require 'event-kit' +{BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron' +{CompositeDisposable, Disposable} = require 'event-kit' fs = require 'fs-plus' path = require 'path' os = require 'os' @@ -94,7 +94,7 @@ class AtomApplication if process.platform is 'darwin' and @config.get('core.useCustomTitleBar') @config.unset('core.useCustomTitleBar') @config.set('core.titleBar', 'custom') - + @config.onDidChange 'core.titleBar', @promptForRestart.bind(this) process.nextTick => @autoUpdateManager.initialize() @@ -397,6 +397,8 @@ class AtomApplication @disposable.add ipcHelpers.on ipcMain, 'did-change-paths', => @saveState(false) + @disposable.add(@disableZoomOnDisplayChange()) + setupDockMenu: -> if process.platform is 'darwin' dockMenu = Menu.buildFromTemplate [ @@ -815,3 +817,17 @@ class AtomApplication args.push("--resource-path=#{@resourcePath}") app.relaunch({args}) app.quit() + + disableZoomOnDisplayChange: -> + outerCallback = => + for window in @windows + window.disableZoom() + + # Set the limits every time a display is added or removed, otherwise the + # configuration gets reset to the default, which allows zooming the + # webframe. + screen.on('display-added', outerCallback) + screen.on('display-removed', outerCallback) + new Disposable -> + screen.removeListener('display-added', outerCallback) + screen.removeListener('display-removed', outerCallback) diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index 03386d31a..bbc235bc5 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -83,12 +83,18 @@ class AtomWindow @representedDirectoryPaths = loadSettings.initialPaths @env = loadSettings.env if loadSettings.env? - @browserWindow.loadSettings = loadSettings + @browserWindow.loadSettingsJSON = JSON.stringify(loadSettings) @browserWindow.on 'window:loaded', => @emit 'window:loaded' @resolveLoadedPromise() + @browserWindow.on 'enter-full-screen', => + @browserWindow.webContents.send('did-enter-full-screen') + + @browserWindow.on 'leave-full-screen', => + @browserWindow.webContents.send('did-leave-full-screen') + @browserWindow.loadURL url.format protocol: 'file' pathname: "#{@resourcePath}/static/index.html" @@ -101,6 +107,7 @@ class AtomWindow hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?) @openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow() + @disableZoom() @atomApplication.addWindow(this) @@ -303,3 +310,6 @@ class AtomWindow @atomApplication.saveState() copy: -> @browserWindow.copy() + + disableZoom: -> + @browserWindow.webContents.setZoomLevelLimits(1, 1) diff --git a/src/reopen-project-menu-manager.js b/src/reopen-project-menu-manager.js index 79acbba66..3f88e41f0 100644 --- a/src/reopen-project-menu-manager.js +++ b/src/reopen-project-menu-manager.js @@ -58,7 +58,7 @@ export default class ReopenProjectMenuManager { // Windows users can right-click Atom taskbar and remove project from the jump list. // We have to honor that or the group stops working. As we only get a partial list // each time we remove them from history entirely. - applyWindowsJumpListRemovals () { + async applyWindowsJumpListRemovals () { if (process.platform !== 'win32') return if (this.app === undefined) { this.app = require('remote').app @@ -68,7 +68,7 @@ export default class ReopenProjectMenuManager { if (removed.length === 0) return for (let project of this.historyManager.getProjects()) { if (removed.includes(ReopenProjectMenuManager.taskDescription(project.paths))) { - this.historyManager.removeProject(project.paths) + await this.historyManager.removeProject(project.paths) } } } diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 62ce4527a..95cd45de9 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -20,14 +20,8 @@ class WindowEventHandler @subscriptions.add listen(@document, 'click', 'a', @handleLinkClick) @subscriptions.add listen(@document, 'submit', 'form', @handleFormSubmit) - browserWindow = @applicationDelegate.getCurrentWindow() - browserWindow.on 'enter-full-screen', @handleEnterFullScreen - @subscriptions.add new Disposable => - browserWindow.removeListener('enter-full-screen', @handleEnterFullScreen) - - browserWindow.on 'leave-full-screen', @handleLeaveFullScreen - @subscriptions.add new Disposable => - browserWindow.removeListener('leave-full-screen', @handleLeaveFullScreen) + @subscriptions.add(@applicationDelegate.onDidEnterFullScreen(@handleEnterFullScreen)) + @subscriptions.add(@applicationDelegate.onDidLeaveFullScreen(@handleLeaveFullScreen)) @subscriptions.add @atomEnvironment.commands.add @window, 'window:toggle-full-screen': @handleWindowToggleFullScreen