Merge pull request #13046 from atom/dg-reopen-project

Project history api, reopen project menu and command
This commit is contained in:
Damien Guard
2016-10-26 10:21:43 -07:00
committed by GitHub
13 changed files with 822 additions and 2 deletions

View File

@@ -244,6 +244,17 @@ class ApplicationDelegate
didCancelWindowUnload: ->
ipcRenderer.send('did-cancel-window-unload')
onDidChangeHistoryManager: (callback) ->
outerCallback = (event, message) ->
callback(event)
ipcRenderer.on('did-change-history-manager', outerCallback)
new Disposable ->
ipcRenderer.removeListener('did-change-history-manager', outerCallback)
didChangeHistoryManager: ->
ipcRenderer.send('did-change-history-manager')
openExternal: (url) ->
shell.openExternal(url)

View File

@@ -22,6 +22,8 @@ KeymapManager = require './keymap-extensions'
TooltipManager = require './tooltip-manager'
CommandRegistry = require './command-registry'
GrammarRegistry = require './grammar-registry'
{HistoryManager, HistoryProject} = require './history-manager'
ReopenProjectMenuManager = require './reopen-project-menu-manager'
StyleManager = require './style-manager'
PackageManager = require './package-manager'
ThemeManager = require './theme-manager'
@@ -94,6 +96,9 @@ class AtomEnvironment extends Model
# Public: A {GrammarRegistry} instance
grammars: null
# Public: A {HistoryManager} instance
history: null
# Public: A {PackageManager} instance
packages: null
@@ -226,6 +231,14 @@ class AtomEnvironment extends Model
@observeAutoHideMenuBar()
@history = new HistoryManager({@project, @commands, localStorage})
# Keep instances of HistoryManager in sync
@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)})
checkPortableHomeWritable = =>
responseChannel = "check-portable-home-writable-response"
ipcRenderer.on responseChannel, (event, response) ->

View File

@@ -126,6 +126,11 @@ const configSchema = {
type: 'boolean',
default: true
},
reopenProjectMenuCount: {
description: 'How many recent projects to show in the Reopen Project menu.',
type: 'integer',
default: 15
},
automaticallyUpdate: {
description: 'Automatically update Atom when a new release is available.',
type: 'boolean',

141
src/history-manager.js Normal file
View File

@@ -0,0 +1,141 @@
/** @babel */
import {Emitter} from 'event-kit'
// Extended: History manager for remembering which projects have been opened.
//
// An instance of this class is always available as the `atom.history` global.
//
// The project history is used to enable the 'Reopen Project' menu.
export class HistoryManager {
constructor ({project, commands, localStorage}) {
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))
}
// Public: Obtain a list of previously opened projects.
//
// Returns an {Array} of {HistoryProject} objects, most recent first.
getProjects () {
return this.projects.map(p => new HistoryProject(p.paths, p.lastOpened))
}
// Public: Clear all projects from the history.
//
// Note: This is not a privacy function - other traces will still exist,
// e.g. window state.
clearProjects () {
this.projects = []
this.saveState()
this.didChangeProjects()
}
// Public: Invoke the given callback when the list of projects changes.
//
// * `callback` {Function}
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeProjects (callback) {
return this.emitter.on('did-change-projects', callback)
}
didChangeProjects (args) {
this.emitter.emit('did-change-projects', args || { reloaded: false })
}
addProject (paths, lastOpened) {
let project = this.getProject(paths)
if (!project) {
project = new HistoryProject(paths)
this.projects.push(project)
}
project.lastOpened = lastOpened || new Date()
this.projects.sort((a, b) => b.lastOpened - a.lastOpened)
this.saveState()
this.didChangeProjects()
}
getProject (paths) {
const pathsString = paths.toString()
for (var i = 0; i < this.projects.length; i++) {
if (this.projects[i].paths.toString() === pathsString) {
return this.projects[i]
}
}
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 })
} 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()
}
}
export class HistoryProject {
constructor (paths, lastOpened) {
this.paths = paths
this.lastOpened = lastOpened || new Date()
}
set paths (paths) { this._paths = paths }
get paths () { return this._paths }
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)
}
}
})
}
}

View File

@@ -142,8 +142,8 @@ class ApplicationMenu
item.metadata ?= {}
if item.command
item.accelerator = @acceleratorForCommand(item.command, keystrokesByCommand)
item.click = -> global.atomApplication.sendCommand(item.command)
item.metadata.windowSpecific = true unless /^application:/.test(item.command)
item.click = -> global.atomApplication.sendCommand(item.command, item.commandDetail)
item.metadata.windowSpecific = true unless /^application:/.test(item.command, item.commandDetail)
@translateTemplate(item.submenu, keystrokesByCommand) if item.submenu
template

View File

@@ -279,6 +279,12 @@ class AtomApplication
@disposable.add ipcHelpers.on ipcMain, 'restart-application', =>
@restart()
@disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) =>
for atomWindow in @windows
webContents = atomWindow.browserWindow.webContents
if webContents isnt event.sender
webContents.send('did-change-history-manager')
# A request from the associated render process to open a new render process.
@disposable.add ipcHelpers.on ipcMain, 'open', (event, options) =>
window = @atomWindowForEvent(event)

View File

@@ -0,0 +1,64 @@
/** @babel */
import { SelectListView } from 'atom-space-pen-views'
export default class ReopenProjectListView extends SelectListView {
initialize (callback) {
this.callback = callback
super.initialize()
this.addClass('reopen-project')
this.list.addClass('mark-active')
}
getFilterKey () {
return 'name'
}
destroy () {
this.cancel()
}
viewForItem (project) {
let element = document.createElement('li')
if (project.name === this.currentProjectName) {
element.classList.add('active')
}
element.textContent = project.name
return element
}
cancelled () {
if (this.panel != null) {
this.panel.destroy()
}
this.panel = null
this.currentProjectName = null
}
confirmed (project) {
this.cancel()
this.callback(project.value)
}
attach () {
this.storeFocusedElement()
if (this.panel == null) {
this.panel = atom.workspace.addModalPanel({item: this})
}
this.focusFilterEditor()
}
toggle () {
if (this.panel != null) {
this.cancel()
} else {
this.currentProjectName = atom.project != null ? this.makeName(atom.project.getPaths()) : null
this.setItems(atom.history.getProjects().map(p => ({ name: this.makeName(p.paths), value: p.paths })))
this.attach()
}
}
makeName (paths) {
return paths.join(', ')
}
}

View File

@@ -0,0 +1,93 @@
/** @babel */
import {CompositeDisposable} from 'event-kit'
import path from 'path'
export default class ReopenProjectMenuManager {
constructor ({menu, commands, history, config, open}) {
this.menuManager = menu
this.historyManager = history
this.config = config
this.open = open
this.projects = []
this.subscriptions = new CompositeDisposable()
this.subscriptions.add(
history.onDidChangeProjects(this.update.bind(this)),
config.onDidChange('core.reopenProjectMenuCount', ({oldValue, newValue}) => {
this.update()
}),
commands.add('atom-workspace', { 'application:reopen-project': this.reopenProjectCommand.bind(this) })
)
}
reopenProjectCommand (e) {
if (e.detail != null && e.detail.index != null) {
this.open(this.projects[e.detail.index].paths)
} else {
this.createReopenProjectListView()
}
}
createReopenProjectListView () {
if (this.reopenProjectListView == null) {
const ReopenProjectListView = require('./reopen-project-list-view')
this.reopenProjectListView = new ReopenProjectListView(paths => {
if (paths != null) {
this.open(paths)
}
})
}
this.reopenProjectListView.toggle()
}
update () {
this.disposeProjectMenu()
this.projects = this.historyManager.getProjects().slice(0, this.config.get('core.reopenProjectMenuCount'))
const newMenu = ReopenProjectMenuManager.createProjectsMenu(this.projects)
this.lastProjectMenu = this.menuManager.add([newMenu])
}
dispose () {
this.subscriptions.dispose()
this.disposeProjectMenu()
if (this.reopenProjectListView != null) {
this.reopenProjectListView.dispose()
}
}
disposeProjectMenu () {
if (this.lastProjectMenu) {
this.lastProjectMenu.dispose()
this.lastProjectMenu = null
}
}
static createProjectsMenu (projects) {
return {
label: 'File',
submenu: [
{
label: 'Reopen Project',
submenu: projects.map((project, index) => ({
label: this.createLabel(project),
command: 'application:reopen-project',
commandDetail: {index: index}
}))
}
]
}
}
static createLabel (project) {
return project.paths.length === 1
? project.paths[0]
: project.paths.map(this.betterBaseName).join(', ')
}
static betterBaseName (directory) {
// Handles Windows roots better than path.basename which returns '' for 'd:' and 'd:\'
const match = directory.match(/^([a-z]:)[\\]?$/i)
return match ? match[1] + '\\' : path.basename(directory)
}
}