mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Merge pull request #13046 from atom/dg-reopen-project
Project history api, reopen project menu and command
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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
141
src/history-manager.js
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
64
src/reopen-project-list-view.js
Normal file
64
src/reopen-project-list-view.js
Normal 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(', ')
|
||||
}
|
||||
}
|
||||
93
src/reopen-project-menu-manager.js
Normal file
93
src/reopen-project-menu-manager.js
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user