diff --git a/package.json b/package.json index 59ec68ffd..d2cd05346 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "season": "^1.0.2", "semver": "1.1.4", "serializable": "^1", - "space-pen": "3.4.7", + "space-pen": "3.6.1", "temp": "0.7.0", "text-buffer": "^3.2.6", "theorist": "^1.0.2", @@ -74,11 +74,11 @@ "archive-view": "0.37.0", "autocomplete": "0.32.0", "autoflow": "0.18.0", - "autosave": "0.17.0", + "autosave": "0.18.0", "background-tips": "0.17.0", "bookmarks": "0.28.0", "bracket-matcher": "0.61.0", - "command-palette": "0.26.0", + "command-palette": "0.27.0", "deprecation-cop": "0.10.0", "dev-live-reload": "0.34.0", "exception-reporting": "0.20.0", @@ -100,7 +100,7 @@ "settings-view": "0.149.0", "snippets": "0.53.0", "spell-check": "0.42.0", - "status-bar": "0.45.0", + "status-bar": "0.46.0", "styleguide": "0.30.0", "symbols-view": "0.66.0", "tabs": "0.54.0", diff --git a/spec/command-installer-spec.coffee b/spec/command-installer-spec.coffee index 7a97094f4..b87a08edc 100644 --- a/spec/command-installer-spec.coffee +++ b/spec/command-installer-spec.coffee @@ -20,7 +20,7 @@ describe "install(commandPath, callback)", -> installDone = false installError = null - installer.install commandFilePath, false, (error) -> + installer.createSymlink commandFilePath, false, (error) -> installDone = true installError = error diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 6f3a76f69..697c5b732 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -3,7 +3,7 @@ Package = require '../src/package' describe "PackageManager", -> beforeEach -> - atom.workspaceView = new WorkspaceView + atom.workspaceView = atom.workspace.getView(atom.workspace).__spacePenView describe "::loadPackage(name)", -> it "continues if the package has an invalid package.json", -> diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-spec.coffee index 1701e3f03..85b9e32c0 100644 --- a/spec/pane-container-spec.coffee +++ b/spec/pane-container-spec.coffee @@ -115,3 +115,29 @@ describe "PaneContainer", -> pane3.addItems([new Object, new Object]) expect(observed).toEqual container.getPaneItems() + + describe "::confirmClose()", -> + [container, pane1, pane2] = [] + + beforeEach -> + class TestItem + shouldPromptToSave: -> true + getUri: -> 'test' + + container = new PaneContainer + container.getRoot().splitRight() + [pane1, pane2] = container.getPanes() + pane1.addItem(new TestItem) + pane2.addItem(new TestItem) + + it "returns true if the user saves all modified files when prompted", -> + spyOn(atom, "confirm").andReturn(0) + saved = container.confirmClose() + expect(saved).toBeTruthy() + expect(atom.confirm).toHaveBeenCalled() + + it "returns false if the user cancels saving any modified file", -> + spyOn(atom, "confirm").andReturn(1) + saved = container.confirmClose() + expect(saved).toBeFalsy() + expect(atom.confirm).toHaveBeenCalled() diff --git a/spec/pane-container-view-spec.coffee b/spec/pane-container-view-spec.coffee index a48269a1a..fb069cad5 100644 --- a/spec/pane-container-view-spec.coffee +++ b/spec/pane-container-view-spec.coffee @@ -1,5 +1,6 @@ path = require 'path' temp = require 'temp' +PaneContainer = require '../src/pane-container' PaneContainerView = require '../src/pane-container-view' PaneView = require '../src/pane-view' {$, View, $$} = require 'atom' @@ -18,7 +19,7 @@ describe "PaneContainerView", -> save: -> @saved = true isEqual: (other) -> @name is other?.name - container = new PaneContainerView + container = atom.workspace.getView(atom.workspace.paneContainer).__spacePenView pane1 = container.getRoot() pane1.activateItem(new TestView('1')) pane2 = pane1.splitRight(new TestView('2')) @@ -70,32 +71,9 @@ describe "PaneContainerView", -> for item in pane.getItems() expect(item.saved).toBeTruthy() - describe ".confirmClose()", -> - it "returns true after modified files are saved", -> - pane1.itemAtIndex(0).shouldPromptToSave = -> true - pane2.itemAtIndex(0).shouldPromptToSave = -> true - spyOn(atom, "confirm").andReturn(0) - - saved = container.confirmClose() - - runs -> - expect(saved).toBeTruthy() - expect(atom.confirm).toHaveBeenCalled() - - it "returns false if the user cancels saving", -> - pane1.itemAtIndex(0).shouldPromptToSave = -> true - pane2.itemAtIndex(0).shouldPromptToSave = -> true - spyOn(atom, "confirm").andReturn(1) - - saved = container.confirmClose() - - runs -> - expect(saved).toBeFalsy() - expect(atom.confirm).toHaveBeenCalled() - describe "serialization", -> it "can be serialized and deserialized, and correctly adjusts dimensions of deserialized panes after attach", -> - newContainer = new PaneContainerView(container.model.testSerialization()) + newContainer = atom.workspace.getView(container.model.testSerialization()).__spacePenView expect(newContainer.find('.pane-row > :contains(1)')).toExist() expect(newContainer.find('.pane-row > .pane-column > :contains(2)')).toExist() expect(newContainer.find('.pane-row > .pane-column > :contains(3)')).toExist() @@ -111,14 +89,14 @@ describe "PaneContainerView", -> describe "if the 'core.destroyEmptyPanes' config option is false (the default)", -> it "leaves the empty panes intact", -> - newContainer = new PaneContainerView(container.model.testSerialization()) + newContainer = atom.workspace.getView(container.model.testSerialization()).__spacePenView expect(newContainer.find('.pane-row > :contains(1)')).toExist() expect(newContainer.find('.pane-row > .pane-column > .pane').length).toBe 2 describe "if the 'core.destroyEmptyPanes' config option is true", -> it "removes empty panes on deserialization", -> atom.config.set('core.destroyEmptyPanes', true) - newContainer = new PaneContainerView(container.model.testSerialization()) + newContainer = atom.workspace.getView(container.model.testSerialization()).__spacePenView expect(newContainer.find('.pane-row, .pane-column')).not.toExist() expect(newContainer.find('> :contains(1)')).toExist() @@ -131,7 +109,7 @@ describe "PaneContainerView", -> item2b = new TestView('2b') item3a = new TestView('3a') - container = new PaneContainerView + container = atom.workspace.getView(new PaneContainer).__spacePenView pane1 = container.getRoot() pane1.activateItem(item1a) container.attachToDom() @@ -281,7 +259,7 @@ describe "PaneContainerView", -> # |7|8|9| # ------- - container = new PaneContainerView + container = atom.workspace.getView(new PaneContainer).__spacePenView pane1 = container.getRoot() pane1.activateItem(new TestView('1')) pane4 = pane1.splitDown(new TestView('4')) diff --git a/spec/pane-view-spec.coffee b/spec/pane-view-spec.coffee index 5aa8c4ffc..6867dbd14 100644 --- a/spec/pane-view-spec.coffee +++ b/spec/pane-view-spec.coffee @@ -1,4 +1,4 @@ -PaneContainerView = require '../src/pane-container-view' +PaneContainer = require '../src/pane-container' PaneView = require '../src/pane-view' fs = require 'fs-plus' {Emitter} = require 'event-kit' @@ -24,7 +24,7 @@ describe "PaneView", -> beforeEach -> deserializerDisposable = atom.deserializers.add(TestView) - container = new PaneContainerView + container = atom.workspace.getView(new PaneContainer).__spacePenView containerModel = container.model view1 = new TestView(id: 'view-1', text: 'View 1') view2 = new TestView(id: 'view-2', text: 'View 2') @@ -311,13 +311,13 @@ describe "PaneView", -> container.attachToDom() pane.focus() - container2 = new PaneContainerView(container.model.testSerialization()) + container2 = atom.workspace.getView(container.model.testSerialization()).__spacePenView pane2 = container2.getRoot() container2.attachToDom() expect(pane2).toMatchSelector(':has(:focus)') $(document.activeElement).blur() - container3 = new PaneContainerView(container.model.testSerialization()) + container3 = atom.workspace.getView(container.model.testSerialization()).__spacePenView pane3 = container3.getRoot() container3.attachToDom() expect(pane3).not.toMatchSelector(':has(:focus)') diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 199959f19..68c382d47 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -29,12 +29,18 @@ atom.keymaps.loadBundledKeymaps() keyBindingsToRestore = atom.keymaps.getKeyBindings() commandsToRestore = atom.commands.getSnapshot() -$(window).on 'core:close', -> window.close() -$(window).on 'beforeunload', -> +window.addEventListener 'core:close', -> window.close() +window.addEventListener 'beforeunload', -> atom.storeWindowDimensions() atom.saveSync() $('html,body').css('overflow', 'auto') +# Allow document.title to be assigned in specs without screwing up spec window title +documentTitle = null +Object.defineProperty document, 'title', + get: -> documentTitle + set: (title) -> documentTitle = title + jasmine.getEnv().addEqualityTester(_.isEqual) # Use underscore's definition of equality for toEqual assertions if process.platform is 'win32' and process.env.JANKY_SHA1 @@ -61,6 +67,7 @@ isCoreSpec = specDirectory == fs.realpathSync(__dirname) beforeEach -> Grim.clearDeprecations() if isCoreSpec $.fx.off = true + documentTitle = null projectPath = specProjectPath ? path.join(@specDirectory, 'fixtures') atom.project = new Project(paths: [projectPath]) atom.workspace = new Workspace() @@ -106,7 +113,7 @@ beforeEach -> spyOn(TextEditorView.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay() TextEditorComponent.performSyncUpdates = true - spyOn(WorkspaceView.prototype, 'setTitle').andCallFake (@title) -> + spyOn(atom, "setRepresentedFilename") spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout spyOn(pathwatcher.File.prototype, "detectResurrectionAfterDelay").andCallFake -> @detectResurrection() @@ -351,7 +358,7 @@ $.fn.enableKeymap = -> not e.originalEvent.defaultPrevented $.fn.attachToDom = -> - @appendTo($('#jasmine-content')) + @appendTo($('#jasmine-content')) unless @isOnDom() $.fn.simulateDomAttachment = -> $('').append(this) diff --git a/spec/window-spec.coffee b/spec/window-spec.coffee index 0a60c302f..b5bf3ddc7 100644 --- a/spec/window-spec.coffee +++ b/spec/window-spec.coffee @@ -63,9 +63,9 @@ describe "Window", -> beforeUnloadEvent = $.Event(new Event('beforeunload')) describe "when pane items are are modified", -> - it "prompts user to save and and calls workspaceView.confirmClose", -> + it "prompts user to save and calls atom.workspace.confirmClose", -> editor = null - spyOn(atom.workspaceView, 'confirmClose').andCallThrough() + spyOn(atom.workspace, 'confirmClose').andCallThrough() spyOn(atom, "confirm").andReturn(2) waitsForPromise -> @@ -74,7 +74,7 @@ describe "Window", -> runs -> editor.insertText("I look different, I feel different.") $(window).trigger(beforeUnloadEvent) - expect(atom.workspaceView.confirmClose).toHaveBeenCalled() + expect(atom.workspace.confirmClose).toHaveBeenCalled() expect(atom.confirm).toHaveBeenCalled() it "prompts user to save and handler returns true if don't save", -> diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 9e4ea2b7c..a156d80f7 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -1,3 +1,5 @@ +path = require 'path' +temp = require 'temp' Workspace = require '../src/workspace' {View} = require '../src/space-pen-extensions' @@ -369,3 +371,90 @@ describe "Workspace", -> workspace2 = Workspace.deserialize(state) expect(jsPackage.loadGrammarsSync.callCount).toBe 1 expect(coffeePackage.loadGrammarsSync.callCount).toBe 1 + + describe "document.title", -> + describe "when the project has no path", -> + it "sets the title to 'untitled'", -> + atom.project.setPath(undefined) + expect(document.title).toBe 'untitled' + + describe "when the project has a path", -> + beforeEach -> + waitsForPromise -> + atom.workspace.open('b') + + describe "when there is an active pane item", -> + it "sets the title to the pane item's title plus the project path", -> + item = atom.workspace.getActivePaneItem() + console.log item.getTitle() + expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]}" + + describe "when the title of the active pane item changes", -> + it "updates the window title based on the item's new title", -> + editor = atom.workspace.getActivePaneItem() + editor.buffer.setPath(path.join(temp.dir, 'hi')) + expect(document.title).toBe "#{editor.getTitle()} - #{atom.project.getPaths()[0]}" + + describe "when the active pane's item changes", -> + it "updates the title to the new item's title plus the project path", -> + atom.workspace.getActivePane().activateNextItem() + item = atom.workspace.getActivePaneItem() + expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]}" + + describe "when the last pane item is removed", -> + it "updates the title to contain the project's path", -> + atom.workspace.getActivePane().destroy() + expect(atom.workspace.getActivePaneItem()).toBeUndefined() + expect(document.title).toBe atom.project.getPaths()[0] + + describe "when an inactive pane's item changes", -> + it "does not update the title", -> + pane = atom.workspace.getActivePane() + pane.splitRight() + initialTitle = document.title + pane.activateNextItem() + expect(document.title).toBe initialTitle + + describe "when the workspace is deserialized", -> + beforeEach -> + waitsForPromise -> atom.workspace.open('a') + + it "updates the title to contain the project's path", -> + document.title = null + console.log atom.workspace.getActivePaneItem() + workspace2 = atom.workspace.testSerialization() + item = atom.workspace.getActivePaneItem() + expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]}" + workspace2.destroy() + + describe "document edited status", -> + [item1, item2] = [] + + beforeEach -> + waitsForPromise -> atom.workspace.open('a') + waitsForPromise -> atom.workspace.open('b') + runs -> + [item1, item2] = atom.workspace.getPaneItems() + spyOn(atom, 'setDocumentEdited') + + it "calls atom.setDocumentEdited when the active item changes", -> + expect(atom.workspace.getActivePaneItem()).toBe item2 + item1.insertText('a') + expect(item1.isModified()).toBe true + atom.workspace.getActivePane().activateNextItem() + + expect(atom.setDocumentEdited).toHaveBeenCalledWith(true) + + it "calls atom.setDocumentEdited when the active item's modified status changes", -> + expect(atom.workspace.getActivePaneItem()).toBe item2 + item2.insertText('a') + advanceClock(item2.getBuffer().getStoppedChangingDelay()) + + expect(item2.isModified()).toBe true + expect(atom.setDocumentEdited).toHaveBeenCalledWith(true) + + item2.undo() + advanceClock(item2.getBuffer().getStoppedChangingDelay()) + + expect(item2.isModified()).toBe false + expect(atom.setDocumentEdited).toHaveBeenCalledWith(false) diff --git a/spec/workspace-view-spec.coffee b/spec/workspace-view-spec.coffee index f2abe04f5..379856137 100644 --- a/spec/workspace-view-spec.coffee +++ b/spec/workspace-view-spec.coffee @@ -49,7 +49,7 @@ describe "WorkspaceView", -> expect(atom.workspaceView.getEditorViews().length).toBe 2 expect(atom.workspaceView.getActivePaneView()).toBe atom.workspaceView.getPaneViews()[1] - expect(atom.workspaceView.title).toBe "untitled - #{atom.project.getPaths()[0]}" + expect(document.title).toBe "untitled - #{atom.project.getPaths()[0]}" describe "when there are open editors", -> it "constructs the view with the same panes", -> @@ -106,7 +106,7 @@ describe "WorkspaceView", -> expect(editorView3).not.toHaveFocus() expect(editorView4).not.toHaveFocus() - expect(atom.workspaceView.title).toBe "#{path.basename(editorView2.getEditor().getPath())} - #{atom.project.getPaths()[0]}" + expect(document.title).toBe "#{path.basename(editorView2.getEditor().getPath())} - #{atom.project.getPaths()[0]}" describe "where there are no open editors", -> it "constructs the view with no open editors", -> @@ -141,56 +141,6 @@ describe "WorkspaceView", -> atom.workspaceView.trigger(event) expect(commandHandler).toHaveBeenCalled() - describe "window title", -> - describe "when the project has no path", -> - it "sets the title to 'untitled'", -> - atom.project.setPaths([]) - expect(atom.workspaceView.title).toBe 'untitled' - - describe "when the project has a path", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('b') - - describe "when there is an active pane item", -> - it "sets the title to the pane item's title plus the project path", -> - item = atom.workspace.getActivePaneItem() - expect(atom.workspaceView.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]}" - - describe "when the title of the active pane item changes", -> - it "updates the window title based on the item's new title", -> - editor = atom.workspace.getActivePaneItem() - editor.buffer.setPath(path.join(temp.dir, 'hi')) - expect(atom.workspaceView.title).toBe "#{editor.getTitle()} - #{atom.project.getPaths()[0]}" - - describe "when the active pane's item changes", -> - it "updates the title to the new item's title plus the project path", -> - atom.workspaceView.getActivePaneView().activateNextItem() - item = atom.workspace.getActivePaneItem() - expect(atom.workspaceView.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]}" - - describe "when the last pane item is removed", -> - it "updates the title to contain the project's path", -> - atom.workspaceView.getActivePaneView().remove() - expect(atom.workspace.getActivePaneItem()).toBeUndefined() - expect(atom.workspaceView.title).toBe atom.project.getPaths()[0] - - describe "when an inactive pane's item changes", -> - it "does not update the title", -> - pane = atom.workspaceView.getActivePaneView() - pane.splitRight() - initialTitle = atom.workspaceView.title - pane.activateNextItem() - expect(atom.workspaceView.title).toBe initialTitle - - describe "when the root view is deserialized", -> - it "updates the title to contain the project's path", -> - workspace2 = atom.workspace.testSerialization() - workspaceView2 = workspace2.getView(workspace2).__spacePenView - item = atom.workspace.getActivePaneItem() - expect(workspaceView2.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]}" - workspaceView2.remove() - describe "window:toggle-invisibles event", -> it "shows/hides invisibles in all open and future editors", -> atom.workspaceView.height(200) diff --git a/src/command-installer.coffee b/src/command-installer.coffee index f1a6ab6e3..a18ab7af1 100644 --- a/src/command-installer.coffee +++ b/src/command-installer.coffee @@ -30,14 +30,41 @@ module.exports = getInstallDirectory: -> "/usr/local/bin" - install: (commandPath, askForPrivilege, callback) -> + installShellCommandsInteractively: -> + showErrorDialog = (error) -> + atom.confirm + message: "Failed to install shell commands" + detailedMessage: error.message + + resourcePath = atom.getLoadSettings().resourcePath + @installAtomCommand resourcePath, true, (error) => + if error? + showErrorDialog(error) + else + @installApmCommand resourcePath, true, (error) => + if error? + showErrorDialog(error) + else + atom.confirm + message: "Commands installed." + detailedMessage: "The shell commands `atom` and `apm` are installed." + + installAtomCommand: (resourcePath, askForPrivilege, callback) -> + commandPath = path.join(resourcePath, 'atom.sh') + @createSymlink commandPath, askForPrivilege, callback + + installApmCommand: (resourcePath, askForPrivilege, callback) -> + commandPath = path.join(resourcePath, 'apm', 'node_modules', '.bin', 'apm') + @createSymlink commandPath, askForPrivilege, callback + + createSymlink: (commandPath, askForPrivilege, callback) -> return unless process.platform is 'darwin' commandName = path.basename(commandPath, path.extname(commandPath)) destinationPath = path.join(@getInstallDirectory(), commandName) fs.readlink destinationPath, (error, realpath) -> - if realpath == commandPath + if realpath is commandPath callback() return @@ -49,11 +76,3 @@ module.exports = catch error callback?(error) - - installAtomCommand: (resourcePath, askForPrivilege, callback) -> - commandPath = path.join(resourcePath, 'atom.sh') - @install commandPath, askForPrivilege, callback - - installApmCommand: (resourcePath, askForPrivilege, callback) -> - commandPath = path.join(resourcePath, 'apm', 'node_modules', '.bin', 'apm') - @install commandPath, askForPrivilege, callback diff --git a/src/pane-axis-element.coffee b/src/pane-axis-element.coffee new file mode 100644 index 000000000..fa517e716 --- /dev/null +++ b/src/pane-axis-element.coffee @@ -0,0 +1,44 @@ +{CompositeDisposable} = require 'event-kit' +{callAttachHooks} = require './space-pen-extensions' + +class PaneAxisElement extends HTMLElement + createdCallback: -> + @subscriptions = new CompositeDisposable + + detachedCallback: -> + @subscriptions.dispose() + + setModel: (@model) -> + @subscriptions.add @model.onDidAddChild(@childAdded.bind(this)) + @subscriptions.add @model.onDidRemoveChild(@childRemoved.bind(this)) + @subscriptions.add @model.onDidReplaceChild(@childReplaced.bind(this)) + + @childAdded({child, index}) for child, index in @model.getChildren() + + switch @model.getOrientation() + when 'horizontal' + @classList.add('pane-row') + when 'vertical' + @classList.add('pane-column') + + childAdded: ({child, index}) -> + view = @model.getView(child) + @insertBefore(view, @children[index]) + callAttachHooks(view) # for backward compatibility with SpacePen views + + childRemoved: ({child}) -> + view = @model.getView(child) + view.remove() + + childReplaced: ({index, oldChild, newChild}) -> + focusedElement = document.activeElement if @hasFocus() + @childRemoved({child: oldChild, index}) + @childAdded({child: newChild, index}) + focusedElement?.focus() if document.activeElement is document.body + + hasFocus: -> + this is document.activeElement or @contains(document.activeElement) + +module.exports = PaneAxisElement = document.registerElement 'atom-pane-axis', + prototype: PaneAxisElement.prototype + extends: 'div' diff --git a/src/pane-axis-view.coffee b/src/pane-axis-view.coffee deleted file mode 100644 index b192817a2..000000000 --- a/src/pane-axis-view.coffee +++ /dev/null @@ -1,37 +0,0 @@ -{CompositeDisposable} = require 'event-kit' -{View} = require './space-pen-extensions' -PaneView = null - -module.exports = -class PaneAxisView extends View - initialize: (@model) -> - @subscriptions = new CompositeDisposable - - @onChildAdded({child, index}) for child, index in @model.getChildren() - - @subscriptions.add @model.onDidAddChild(@onChildAdded) - @subscriptions.add @model.onDidRemoveChild(@onChildRemoved) - @subscriptions.add @model.onDidReplaceChild(@onChildReplaced) - - afterAttach: -> - @container = @closest('.panes').view() - - onChildReplaced: ({index, oldChild, newChild}) => - focusedElement = document.activeElement if @hasFocus() - @onChildRemoved({child: oldChild, index}) - @onChildAdded({child: newChild, index}) - focusedElement?.focus() if document.activeElement is document.body - - onChildAdded: ({child, index}) => - view = @model.getView(child).__spacePenView - @insertAt(index, view) - - onChildRemoved: ({child}) => - view = @model.getView(child).__spacePenView - view.detach() - PaneView ?= require './pane-view' - if view instanceof PaneView and view.model.isDestroyed() - @container?.trigger 'pane:removed', [view] - - beforeRemove: -> - @subscriptions.dispose() diff --git a/src/pane-axis.coffee b/src/pane-axis.coffee index df2a17a26..f960368ea 100644 --- a/src/pane-axis.coffee +++ b/src/pane-axis.coffee @@ -3,9 +3,6 @@ {flatten} = require 'underscore-plus' Serializable = require 'serializable' -PaneRowView = null -PaneColumnView = null - module.exports = class PaneAxis extends Model atom.deserializers.add(this) @@ -40,11 +37,7 @@ class PaneAxis extends Model setContainer: (@container) -> @container - getViewClass: -> - if @orientation is 'vertical' - PaneColumnView ?= require './pane-column-view' - else - PaneRowView ?= require './pane-row-view' + getOrientation: -> @orientation getView: (object) -> @container.getView(object) diff --git a/src/pane-column-view.coffee b/src/pane-column-view.coffee deleted file mode 100644 index fca1938e2..000000000 --- a/src/pane-column-view.coffee +++ /dev/null @@ -1,12 +0,0 @@ -{$} = require './space-pen-extensions' -_ = require 'underscore-plus' -PaneAxisView = require './pane-axis-view' - -module.exports = -class PaneColumnView extends PaneAxisView - - @content: -> - @div class: 'pane-column' - - className: -> - "PaneColumn" diff --git a/src/pane-container-element.coffee b/src/pane-container-element.coffee new file mode 100644 index 000000000..cdde59f90 --- /dev/null +++ b/src/pane-container-element.coffee @@ -0,0 +1,80 @@ +{CompositeDisposable} = require 'event-kit' +{callAttachHooks} = require './space-pen-extensions' +PaneContainerView = null +_ = require 'underscore-plus' + +module.exports = +class PaneContainerElement extends HTMLElement + createdCallback: -> + @subscriptions = new CompositeDisposable + @classList.add 'panes' + PaneContainerView ?= require './pane-container-view' + @__spacePenView = new PaneContainerView(this) + + setModel: (@model) -> + @subscriptions.add @model.observeRoot(@rootChanged.bind(this)) + @__spacePenView.setModel(@model) + + rootChanged: (root) -> + focusedElement = document.activeElement if @hasFocus() + @firstChild?.remove() + if root? + view = @model.getView(root) + @appendChild(view) + callAttachHooks(view) + focusedElement?.focus() + + hasFocus: -> + this is document.activeElement or @contains(document.activeElement) + + focusPaneViewAbove: -> + @nearestPaneInDirection('above')?.focus() + + focusPaneViewBelow: -> + @nearestPaneInDirection('below')?.focus() + + focusPaneViewOnLeft: -> + @nearestPaneInDirection('left')?.focus() + + focusPaneViewOnRight: -> + @nearestPaneInDirection('right')?.focus() + + nearestPaneInDirection: (direction) -> + distance = (pointA, pointB) -> + x = pointB.x - pointA.x + y = pointB.y - pointA.y + Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) + + paneView = @model.getView(@model.getActivePane()) + box = @boundingBoxForPaneView(paneView) + + paneViews = _.toArray(@querySelectorAll('.pane')) + .filter (otherPaneView) => + otherBox = @boundingBoxForPaneView(otherPaneView) + switch direction + when 'left' then otherBox.right.x <= box.left.x + when 'right' then otherBox.left.x >= box.right.x + when 'above' then otherBox.bottom.y <= box.top.y + when 'below' then otherBox.top.y >= box.bottom.y + .sort (paneViewA, paneViewB) => + boxA = @boundingBoxForPaneView(paneViewA) + boxB = @boundingBoxForPaneView(paneViewB) + switch direction + when 'left' then distance(box.left, boxA.right) - distance(box.left, boxB.right) + when 'right' then distance(box.right, boxA.left) - distance(box.right, boxB.left) + when 'above' then distance(box.top, boxA.bottom) - distance(box.top, boxB.bottom) + when 'below' then distance(box.bottom, boxA.top) - distance(box.bottom, boxB.top) + + paneViews[0] + + boundingBoxForPaneView: (paneView) -> + boundingBox = paneView.getBoundingClientRect() + + left: {x: boundingBox.left, y: boundingBox.top} + right: {x: boundingBox.right, y: boundingBox.top} + top: {x: boundingBox.left, y: boundingBox.top} + bottom: {x: boundingBox.left, y: boundingBox.bottom} + +module.exports = PaneContainerElement = document.registerElement 'atom-pane-container', + prototype: PaneContainerElement.prototype + extends: 'div' diff --git a/src/pane-container-view.coffee b/src/pane-container-view.coffee index 71166fd59..bd04f38e2 100644 --- a/src/pane-container-view.coffee +++ b/src/pane-container-view.coffee @@ -1,7 +1,7 @@ {deprecate} = require 'grim' Delegator = require 'delegato' {CompositeDisposable} = require 'event-kit' -{$, View} = require './space-pen-extensions' +{$, View, callAttachHooks} = require './space-pen-extensions' PaneView = require './pane-view' PaneContainer = require './pane-container' @@ -15,50 +15,22 @@ class PaneContainerView extends View @content: -> @div class: 'panes' - initialize: (params) -> + constructor: (@element) -> + super @subscriptions = new CompositeDisposable - if params instanceof PaneContainer - @model = params - else - @model = new PaneContainer({root: params?.root?.model}) - - @subscriptions.add @model.observeRoot(@onRootChanged) + setModel: (@model) -> @subscriptions.add @model.onDidChangeActivePaneItem(@onActivePaneItemChanged) getRoot: -> - @children().first().view() - - onRootChanged: (root) => - focusedElement = document.activeElement if @hasFocus() - - oldRoot = @getRoot() - if oldRoot instanceof PaneView and oldRoot.model.isDestroyed() - @trigger 'pane:removed', [oldRoot] - oldRoot?.detach() - if root? - view = @model.getView(root).__spacePenView - @append(view) - focusedElement?.focus() - else - atom.workspaceView?.focus() if focusedElement? + view = @model.getView(@model.getRoot()) + view.__spacePenView ? view onActivePaneItemChanged: (activeItem) => @trigger 'pane-container:active-pane-item-changed', [activeItem] - removeChild: (child) -> - throw new Error("Removing non-existant child") unless @getRoot() is child - @setRoot(null) - @trigger 'pane:removed', [child] if child instanceof PaneView - confirmClose: -> - saved = true - for paneView in @getPaneViews() - for item in paneView.getItems() - if not paneView.promptToSaveItem(item) - saved = false - break - saved + @model.confirmClose() getPaneViews: -> @find('.pane').views() @@ -101,56 +73,17 @@ class PaneContainerView extends View @model.activatePreviousPane() focusPaneViewAbove: -> - @nearestPaneInDirection('above')?.focus() + @element.focusPaneViewAbove() focusPaneViewBelow: -> - @nearestPaneInDirection('below')?.focus() + @element.focusPaneViewBelow() focusPaneViewOnLeft: -> - @nearestPaneInDirection('left')?.focus() + @element.focusPaneViewOnLeft() focusPaneViewOnRight: -> - @nearestPaneInDirection('right')?.focus() + @element.focusPaneViewOnRight() - nearestPaneInDirection: (direction) -> - distance = (pointA, pointB) -> - x = pointB.x - pointA.x - y = pointB.y - pointA.y - Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) - - paneView = @getActivePaneView() - box = @boundingBoxForPaneView(paneView) - paneViews = @getPaneViews() - .filter (otherPaneView) => - otherBox = @boundingBoxForPaneView(otherPaneView) - switch direction - when 'left' then otherBox.right.x <= box.left.x - when 'right' then otherBox.left.x >= box.right.x - when 'above' then otherBox.bottom.y <= box.top.y - when 'below' then otherBox.top.y >= box.bottom.y - .sort (paneViewA, paneViewB) => - boxA = @boundingBoxForPaneView(paneViewA) - boxB = @boundingBoxForPaneView(paneViewB) - switch direction - when 'left' then distance(box.left, boxA.right) - distance(box.left, boxB.right) - when 'right' then distance(box.right, boxA.left) - distance(box.right, boxB.left) - when 'above' then distance(box.top, boxA.bottom) - distance(box.top, boxB.bottom) - when 'below' then distance(box.bottom, boxA.top) - distance(box.bottom, boxB.top) - - paneViews[0] - - boundingBoxForPaneView: (paneView) -> - boundingBox = paneView[0].getBoundingClientRect() - - left: {x: boundingBox.left, y: boundingBox.top} - right: {x: boundingBox.right, y: boundingBox.top} - top: {x: boundingBox.left, y: boundingBox.top} - bottom: {x: boundingBox.left, y: boundingBox.bottom} - - # Deprecated getPanes: -> deprecate("Use PaneContainerView::getPaneViews() instead") @getPaneViews() - - beforeRemove: -> - @subscriptions.dispose() diff --git a/src/pane-container.coffee b/src/pane-container.coffee index 53475f34c..aa9f17d97 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -3,9 +3,12 @@ {Emitter, CompositeDisposable} = require 'event-kit' Serializable = require 'serializable' Pane = require './pane' +PaneElement = require './pane-element' +PaneContainerElement = require './pane-container-element' +PaneAxisElement = require './pane-axis-element' +PaneAxis = require './pane-axis' ViewRegistry = require './view-registry' ItemRegistry = require './item-registry' -PaneContainerView = null module.exports = class PaneContainer extends Model @@ -30,8 +33,10 @@ class PaneContainer extends Model @emitter = new Emitter @subscriptions = new CompositeDisposable - @viewRegistry = params?.viewRegistry ? new ViewRegistry @itemRegistry = new ItemRegistry + @viewRegistry = params?.viewRegistry ? new ViewRegistry + @registerViewProviders() + @setRoot(params?.root ? new Pane) @destroyEmptyPanes() if params?.destroyEmptyPanes @@ -48,8 +53,18 @@ class PaneContainer extends Model root: @root?.serialize() activePaneId: @activePane.id - getViewClass: -> - PaneContainerView ?= require './pane-container-view' + registerViewProviders: -> + @viewRegistry.addViewProvider + modelConstructor: PaneContainer + viewConstructor: PaneContainerElement + + @viewRegistry.addViewProvider + modelConstructor: PaneAxis + viewConstructor: PaneAxisElement + + @viewRegistry.addViewProvider + modelConstructor: Pane + viewConstructor: PaneElement getView: (object) -> @viewRegistry.getView(object) @@ -129,6 +144,17 @@ class PaneContainer extends Model saveAll: -> pane.saveItems() for pane in @getPanes() + confirmClose: -> + allSaved = true + + for pane in @getPanes() + for item in pane.getItems() + unless pane.promptToSaveItem(item) + allSaved = false + break + + allSaved + activateNextPane: -> panes = @getPanes() if panes.length > 1 diff --git a/src/pane-element.coffee b/src/pane-element.coffee new file mode 100644 index 000000000..1050982c7 --- /dev/null +++ b/src/pane-element.coffee @@ -0,0 +1,100 @@ +{CompositeDisposable} = require 'event-kit' +{$} = require './space-pen-extensions' +PaneView = require './pane-view' + +class PaneElement extends HTMLElement + createdCallback: -> + @subscriptions = new CompositeDisposable + @initializeContent() + @subscribeToDOMEvents() + @createSpacePenShim() + + attachedCallback: -> + @focus() if @model.isFocused() + + initializeContent: -> + @setAttribute 'class', 'pane' + @setAttribute 'tabindex', -1 + @appendChild @itemViews = document.createElement('div') + @itemViews.setAttribute 'class', 'item-views' + + subscribeToDOMEvents: -> + @addEventListener 'focusin', => @model.focus() + @addEventListener 'focusout', => @model.blur() + @addEventListener 'focus', => @getActiveView()?.focus() + + createSpacePenShim: -> + @__spacePenView = new PaneView(this) + + getModel: -> @model + + setModel: (@model) -> + @subscriptions.add @model.onDidActivate(@activated.bind(this)) + @subscriptions.add @model.observeActive(@activeStatusChanged.bind(this)) + @subscriptions.add @model.observeActiveItem(@activeItemChanged.bind(this)) + @subscriptions.add @model.onDidRemoveItem(@itemRemoved.bind(this)) + @subscriptions.add @model.onDidDestroy(@paneDestroyed.bind(this)) + @__spacePenView.setModel(@model) + + activated: -> + @focus() unless @hasFocus() + + activeStatusChanged: (active) -> + if active + @classList.add('active') + else + @classList.remove('active') + + activeItemChanged: (item) -> + return unless item? + + $itemViews = $(@itemViews) + view = @model.getView(item).__spacePenView + otherView.hide() for otherView in $itemViews.children().not(view).views() + $itemViews.append(view) unless view.parent().is($itemViews) + view.show() + view.focus() if @hasFocus() + + itemRemoved: ({item, index, destroyed}) -> + if item instanceof $ + viewToRemove = item + else + viewToRemove = @model.getView(item).__spacePenView + + if viewToRemove? + if destroyed + viewToRemove.remove() + else + viewToRemove.detach() + + paneDestroyed: -> + @subscriptions.dispose() + + getActiveView: -> @model.getView(@model.getActiveItem()) + + hasFocus: -> + this is document.activeElement or @contains(document.activeElement) + +atom.commands.add '.pane', + 'pane:save-items': -> @getModel().saveItems() + 'pane:show-next-item': -> @getModel().activateNextItem() + 'pane:show-previous-item': -> @getModel().activatePreviousItem() + 'pane:show-item-1': -> @getModel().activateItemAtIndex(0) + 'pane:show-item-2': -> @getModel().activateItemAtIndex(1) + 'pane:show-item-3': -> @getModel().activateItemAtIndex(2) + 'pane:show-item-4': -> @getModel().activateItemAtIndex(3) + 'pane:show-item-5': -> @getModel().activateItemAtIndex(4) + 'pane:show-item-6': -> @getModel().activateItemAtIndex(5) + 'pane:show-item-7': -> @getModel().activateItemAtIndex(6) + 'pane:show-item-8': -> @getModel().activateItemAtIndex(7) + 'pane:show-item-9': -> @getModel().activateItemAtIndex(8) + 'pane:split-left': -> @getModel().splitLeft(copyActiveItem: true) + 'pane:split-right': -> @getModel().splitRight(copyActiveItem: true) + 'pane:split-up': -> @getModel().splitUp(copyActiveItem: true) + 'pane:split-down': -> @getModel().splitDown(copyActiveItem: true) + 'pane:close': -> @getModel().destroy() + 'pane:close-other-items': -> @getModel().destroyInactiveItems() + +module.exports = PaneElement = document.registerElement 'atom-pane', + prototype: PaneElement.prototype + extends: 'div' diff --git a/src/pane-row-view.coffee b/src/pane-row-view.coffee deleted file mode 100644 index 1ad73d318..000000000 --- a/src/pane-row-view.coffee +++ /dev/null @@ -1,11 +0,0 @@ -{$} = require './space-pen-extensions' -_ = require 'underscore-plus' -PaneAxisView = require './pane-axis-view' - -module.exports = -class PaneRowView extends PaneAxisView - @content: -> - @div class: 'pane-row' - - className: -> - "PaneRow" diff --git a/src/pane-view.coffee b/src/pane-view.coffee index 127135134..8edeefcc6 100644 --- a/src/pane-view.coffee +++ b/src/pane-view.coffee @@ -17,12 +17,6 @@ class PaneView extends View Delegator.includeInto(this) PropertyAccessors.includeInto(this) - @version: 1 - - @content: (wrappedView) -> - @div class: 'pane', tabindex: -1, => - @div class: 'item-views', outlet: 'itemViews' - @delegatesProperties 'items', 'activeItem', toProperty: 'model' @delegatesMethods 'getItems', 'activateNextItem', 'activatePreviousItem', 'getActiveItemIndex', 'activateItemAtIndex', 'activateItem', 'addItem', 'itemAtIndex', 'moveItem', 'moveItemToPane', @@ -32,49 +26,33 @@ class PaneView extends View 'activate', 'getActiveItem', toProperty: 'model' previousActiveItem: null + attached: false - initialize: (@model) -> + constructor: (@element) -> + @itemViews = $(element.itemViews) + super + + setModel: (@model) -> @subscriptions = new CompositeDisposable - @onItemAdded(item) for item in @items - @handleEvents() - - handleEvents: -> @subscriptions.add @model.observeActiveItem(@onActiveItemChanged) @subscriptions.add @model.onDidAddItem(@onItemAdded) @subscriptions.add @model.onDidRemoveItem(@onItemRemoved) @subscriptions.add @model.onDidMoveItem(@onItemMoved) @subscriptions.add @model.onWillDestroyItem(@onBeforeItemDestroyed) - @subscriptions.add @model.onDidActivate(@onActivated) @subscriptions.add @model.observeActive(@onActiveStatusChanged) + @subscriptions.add @model.onDidDestroy(@onPaneDestroyed) - @subscribe this, 'focusin', => @model.focus() - @subscribe this, 'focusout', => @model.blur() - @subscribe this, 'focus', => - @activeView?.focus() - false + afterAttach: -> + @container ?= @closest('.panes').view() + @trigger('pane:attached', [this]) unless @attached + @attached = true - @command 'pane:save-items', => @saveItems() - @command 'pane:show-next-item', => @activateNextItem() - @command 'pane:show-previous-item', => @activatePreviousItem() + onPaneDestroyed: => + @container?.trigger 'pane:removed', [this] + @subscriptions.dispose() - @command 'pane:show-item-1', => @activateItemAtIndex(0) - @command 'pane:show-item-2', => @activateItemAtIndex(1) - @command 'pane:show-item-3', => @activateItemAtIndex(2) - @command 'pane:show-item-4', => @activateItemAtIndex(3) - @command 'pane:show-item-5', => @activateItemAtIndex(4) - @command 'pane:show-item-6', => @activateItemAtIndex(5) - @command 'pane:show-item-7', => @activateItemAtIndex(6) - @command 'pane:show-item-8', => @activateItemAtIndex(7) - @command 'pane:show-item-9', => @activateItemAtIndex(8) - - @command 'pane:split-left', => @model.splitLeft(copyActiveItem: true) - @command 'pane:split-right', => @model.splitRight(copyActiveItem: true) - @command 'pane:split-up', => @model.splitUp(copyActiveItem: true) - @command 'pane:split-down', => @model.splitDown(copyActiveItem: true) - @command 'pane:close', => - @model.destroyItems() - @model.destroy() - @command 'pane:close-other-items', => @destroyInactiveItems() + remove: -> + @model.destroy() unless @model.isDestroyed() # Essential: Returns the {Pane} model underlying this pane view getModel: -> @model @@ -109,23 +87,10 @@ class PaneView extends View deprecate("Use PaneView::activatePreviousItem instead") @activatePreviousItem() - afterAttach: (onDom) -> - @focus() if @model.focused and onDom - - return if @attached - @container = @closest('.panes').view() - @attached = true - @trigger 'pane:attached', [this] - - onActivated: => - @focus() unless @hasFocus() - onActiveStatusChanged: (active) => if active - @addClass('active') @trigger 'pane:became-active' else - @removeClass('active') @trigger 'pane:became-inactive' # Public: Returns the next pane, ordered by creation. @@ -179,23 +144,12 @@ class PaneView extends View @trigger 'pane:item-added', [item, index] onItemRemoved: ({item, index, destroyed}) => - if item instanceof $ - viewToRemove = item - else - viewToRemove = @model.getView(item).__spacePenView - - if viewToRemove? - if destroyed - viewToRemove.remove() - else - viewToRemove.detach() - @trigger 'pane:item-removed', [item, index] onItemMoved: ({item, newIndex}) => @trigger 'pane:item-moved', [item, newIndex] - onBeforeItemDestroyed: (item) => + onBeforeItemDestroyed: ({item}) => @unsubscribe(item) if typeof item.off is 'function' @trigger 'pane:before-item-destroyed', [item] @@ -215,17 +169,7 @@ class PaneView extends View splitDown: (items...) -> @model.getView(@model.splitDown({items})).__spacePenView - # Public: Get the container view housing this pane. - # - # Returns a {View}. - getContainer: -> - @closest('.panes').view() + getContainer: -> @closest('.panes').view() - beforeRemove: -> - @subscriptions.dispose() - @model.destroy() unless @model.isDestroyed() - - remove: (selector, keepData) -> - return super if keepData - @unsubscribe() - super + focus: -> + @element.focus() diff --git a/src/pane.coffee b/src/pane.coffee index 92a97a45e..a501d8a9d 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -53,9 +53,6 @@ class Pane extends Model params.activeItem = find params.items, (item) -> item.getUri?() is activeItemUri params - # Called by the view layer to construct a view for this model. - getViewClass: -> PaneView ?= require './pane-view' - getView: (object) -> @container.getView(object) @@ -238,6 +235,8 @@ class Pane extends Model @focused = false true # if this is called from an event handler, don't cancel it + isFocused: -> @focused + getPanes: -> [this] ### @@ -414,9 +413,8 @@ class Pane extends Model @destroyItem(item) for item in @getItems() when item isnt @activeItem promptToSaveItem: (item) -> - return true unless item.shouldPromptToSave?() + return true unless typeof item.getUri is 'function' and item.shouldPromptToSave?() - uri = item.getUri() chosen = atom.confirm message: "'#{item.getTitle?() ? item.getUri()}' has changes, do you want to save them?" detailedMessage: "Your changes will be lost if you close this item without saving." diff --git a/src/window-bootstrap.coffee b/src/window-bootstrap.coffee index 42ec7cd91..704596915 100644 --- a/src/window-bootstrap.coffee +++ b/src/window-bootstrap.coffee @@ -9,3 +9,9 @@ atom.initialize() atom.startEditorWindow() window.atom.loadTime = Date.now() - startTime console.log "Window load time: #{atom.getWindowLoadTime()}ms" + +# Workaround for focus getting cleared upon window creation +windowFocused = -> + window.removeEventListener('focus', windowFocused) + setTimeout (-> document.querySelector('.workspace').focus()), 0 +window.addEventListener('focus', windowFocused) diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 5fe4a9ec6..31ce35ec5 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -37,7 +37,7 @@ class WindowEventHandler atom.workspace?.open(pathToOpen, {initialLine, initialColumn}) @subscribe $(window), 'beforeunload', => - confirmed = atom.workspaceView?.confirmClose() + confirmed = atom.workspace?.confirmClose() atom.hide() if confirmed and not @reloadRequested and atom.getCurrentWindow().isWebViewFocused() @reloadRequested = false diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee new file mode 100644 index 000000000..3933eb718 --- /dev/null +++ b/src/workspace-element.coffee @@ -0,0 +1,137 @@ +ipc = require 'ipc' +path = require 'path' +{Disposable, CompositeDisposable} = require 'event-kit' +Grim = require 'grim' +scrollbarStyle = require 'scrollbar-style' +{callAttachHooks} = require 'space-pen' +WorkspaceView = null + +module.exports = +class WorkspaceElement extends HTMLElement + createdCallback: -> + @subscriptions = new CompositeDisposable + atom.commands.setRootNode(this) + @initializeContent() + @observeScrollbarStyle() + @observeTextEditorFontConfig() + @createSpacePenShim() + + attachedCallback: -> + callAttachHooks(this) + @focus() + + detachedCallback: -> + @model.destroy() + + initializeContent: -> + @classList.add 'workspace' + @setAttribute 'tabindex', -1 + + + @verticalAxis = document.createElement('div') + @verticalAxis.classList.add('vertical') + + @horizontalAxis = document.createElement('div') + @horizontalAxis.classList.add('horizontal') + @horizontalAxis.appendChild(@verticalAxis) + + @appendChild(@horizontalAxis) + + observeScrollbarStyle: -> + @subscriptions.add scrollbarStyle.onValue (style) => + switch style + when 'legacy' + @classList.remove('scrollbars-visible-when-scrolling') + @classList.add("scrollbars-visible-always") + when 'overlay' + @classList.remove('scrollbars-visible-always') + @classList.add("scrollbars-visible-when-scrolling") + + observeTextEditorFontConfig: -> + @subscriptions.add atom.config.observe 'editor.fontSize', @setTextEditorFontSize + @subscriptions.add atom.config.observe 'editor.fontFamily', @setTextEditorFontFamily + @subscriptions.add atom.config.observe 'editor.lineHeight', @setTextEditorLineHeight + + createSpacePenShim: -> + WorkspaceView ?= require './workspace-view' + @__spacePenView = new WorkspaceView(this) + + getModel: -> @model + + setModel: (@model) -> + @paneContainer = @model.getView(@model.paneContainer) + @verticalAxis.appendChild(@paneContainer) + + @addEventListener 'focus', @handleFocus.bind(this) + handleWindowFocus = @handleWindowFocus.bind(this) + window.addEventListener 'focus', handleWindowFocus + @subscriptions.add(new Disposable => window.removeEventListener 'focus', handleWindowFocus) + + @__spacePenView.setModel(@model) + + setTextEditorFontSize: (fontSize) -> + atom.themes.updateGlobalEditorStyle('font-size', fontSize + 'px') + + setTextEditorFontFamily: (fontFamily) -> + atom.themes.updateGlobalEditorStyle('font-family', fontFamily) + + setTextEditorLineHeight: (lineHeight) -> + atom.themes.updateGlobalEditorStyle('line-height', lineHeight) + + handleFocus: (event) -> + @model.getActivePane().activate() + + handleWindowFocus: (event) -> + @handleFocus(event) if document.activeElement is document.body + +if process.platform is 'darwin' + atom.commands.add '.workspace', 'window:install-shell-commands', -> @getModel().installShellCommands() + +atom.commands.add '.workspace', + 'window:increase-font-size': -> @getModel().increaseFontSize() + 'window:decrease-font-size': -> @getModel().decreaseFontSize() + 'window:reset-font-size': -> @getModel().resetFontSize() + 'application:about': -> ipc.send('command', 'application:about') + 'application:run-all-specs': -> ipc.send('command', 'application:run-all-specs') + 'application:run-benchmarks': -> ipc.send('command', 'application:run-benchmarks') + 'application:show-settings': -> ipc.send('command', 'application:show-settings') + 'application:quit': -> ipc.send('command', 'application:quit') + 'application:hide': -> ipc.send('command', 'application:hide') + 'application:hide-other-applications': -> ipc.send('command', 'application:hide-other-applications') + 'application:install-update': -> ipc.send('command', 'application:install-update') + 'application:unhide-all-applications': -> ipc.send('command', 'application:unhide-all-applications') + 'application:new-window': -> ipc.send('command', 'application:new-window') + 'application:new-file': -> ipc.send('command', 'application:new-file') + 'application:open': -> ipc.send('command', 'application:open') + 'application:open-file': -> ipc.send('command', 'application:open-file') + 'application:open-folder': -> ipc.send('command', 'application:open-folder') + 'application:open-dev': -> ipc.send('command', 'application:open-dev') + 'application:open-safe': -> ipc.send('command', 'application:open-safe') + 'application:minimize': -> ipc.send('command', 'application:minimize') + 'application:zoom': -> ipc.send('command', 'application:zoom') + 'application:bring-all-windows-to-front': -> ipc.send('command', 'application:bring-all-windows-to-front') + 'application:open-your-config': -> ipc.send('command', 'application:open-your-config') + 'application:open-your-init-script': -> ipc.send('command', 'application:open-your-init-script') + 'application:open-your-keymap': -> ipc.send('command', 'application:open-your-keymap') + 'application:open-your-snippets': -> ipc.send('command', 'application:open-your-snippets') + 'application:open-your-stylesheet': -> ipc.send('command', 'application:open-your-stylesheet') + 'application:open-license': -> @getModel().openLicense() + 'window:run-package-specs': -> ipc.send('run-package-specs', path.join(atom.project.getPath(), 'spec')) + 'window:focus-next-pane': -> @getModel().activateNextPane() + 'window:focus-previous-pane': -> @getModel().activatePreviousPane() + 'window:focus-pane-above': -> @focusPaneViewAbove() + 'window:focus-pane-below': -> @focusPaneViewBelow() + 'window:focus-pane-on-left': -> @focusPaneViewOnLeft() + 'window:focus-pane-on-right': -> @focusPaneViewOnRight() + 'window:save-all': -> @getModel().saveAll() + 'window:toggle-invisibles': -> atom.config.toggle("editor.showInvisibles") + 'window:log-deprecation-warnings': -> Grim.logDeprecationWarnings() + 'window:toggle-auto-indent': -> atom.config.toggle("editor.autoIndent") + 'pane:reopen-closed-item': -> @getModel().reopenItem() + 'core:close': -> @getModel().destroyActivePaneItemOrEmptyPane() + 'core:save': -> @getModel().saveActivePaneItem() + 'core:save-as': -> @getModel().saveActivePaneItemAs() + +module.exports = WorkspaceElement = document.registerElement 'atom-workspace', + prototype: WorkspaceElement.prototype + extends: 'div' diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index b4a395bca..cee296bf6 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -6,19 +6,12 @@ Delegator = require 'delegato' {deprecate, logDeprecationWarnings} = require 'grim' scrollbarStyle = require 'scrollbar-style' {$, $$, View} = require './space-pen-extensions' +fs = require 'fs-plus' Workspace = require './workspace' -CommandInstaller = require './command-installer' PaneView = require './pane-view' -PaneColumnView = require './pane-column-view' -PaneRowView = require './pane-row-view' PaneContainerView = require './pane-container-view' TextEditor = require './text-editor' -atom.commands.add '.workspace', - 'window:increase-font-size': -> @getModel().increaseFontSize() - 'window:decrease-font-size': -> @getModel().decreaseFontSize() - 'window:reset-font-size': -> @getModel().resetFontSize() - # Extended: The top-level view for the entire window. An instance of this class is # available via the `atom.workspaceView` global. # @@ -64,100 +57,20 @@ class WorkspaceView extends View 'saveActivePaneItem', 'saveActivePaneItemAs', 'saveAll', 'destroyActivePaneItem', 'destroyActivePane', 'increaseFontSize', 'decreaseFontSize', toProperty: 'model' - @version: 4 - - @content: -> - @div class: 'workspace', tabindex: -1, => - @div class: 'horizontal', outlet: 'horizontal', => - @div class: 'vertical', outlet: 'vertical', => - @div class: 'panes', outlet: 'panes' - - initialize: (model) -> - @model = model ? atom.workspace ? new Workspace unless @model? - @element.getModel = -> model - atom.commands.setRootNode(@[0]) - - panes = @model.getView(@model.paneContainer).__spacePenView - @panes.replaceWith(panes) - @panes = panes + constructor: (@element) -> + unless @element? + return atom.workspace.getView(atom.workspace).__spacePenView + super + @deprecateViewEvents() + setModel: (@model) -> + @horizontal = @find('.horizontal') + @vertical = @find('.vertical') + @panes = @find('.panes').view() @subscribe @model.onDidOpen => @trigger 'uri-opened' - @subscribe scrollbarStyle, (style) => - @removeClass('scrollbars-visible-always scrollbars-visible-when-scrolling') - switch style - when 'legacy' - @addClass("scrollbars-visible-always") - when 'overlay' - @addClass("scrollbars-visible-when-scrolling") - - - @subscribe atom.config.observe 'editor.fontSize', @setEditorFontSize - @subscribe atom.config.observe 'editor.fontFamily', @setEditorFontFamily - @subscribe atom.config.observe 'editor.lineHeight', @setEditorLineHeight - - @updateTitle() - - @on 'focus', (e) => @handleFocus(e) - @subscribe $(window), 'focus', (e) => - @handleFocus(e) if document.activeElement is document.body - - atom.project.onDidChangePaths => @updateTitle() - @on 'pane-container:active-pane-item-changed', => @updateTitle() - @on 'pane:active-item-title-changed', '.active.pane', => @updateTitle() - @on 'pane:active-item-modified-status-changed', '.active.pane', => @updateDocumentEdited() - - @command 'application:about', -> ipc.send('command', 'application:about') - @command 'application:run-all-specs', -> ipc.send('command', 'application:run-all-specs') - @command 'application:run-benchmarks', -> ipc.send('command', 'application:run-benchmarks') - @command 'application:show-settings', -> ipc.send('command', 'application:show-settings') - @command 'application:quit', -> ipc.send('command', 'application:quit') - @command 'application:hide', -> ipc.send('command', 'application:hide') - @command 'application:hide-other-applications', -> ipc.send('command', 'application:hide-other-applications') - @command 'application:install-update', -> ipc.send('command', 'application:install-update') - @command 'application:unhide-all-applications', -> ipc.send('command', 'application:unhide-all-applications') - @command 'application:new-window', -> ipc.send('command', 'application:new-window') - @command 'application:new-file', -> ipc.send('command', 'application:new-file') - @command 'application:open', -> ipc.send('command', 'application:open') - @command 'application:open-file', -> ipc.send('command', 'application:open-file') - @command 'application:open-folder', -> ipc.send('command', 'application:open-folder') - @command 'application:open-dev', -> ipc.send('command', 'application:open-dev') - @command 'application:open-safe', -> ipc.send('command', 'application:open-safe') - @command 'application:minimize', -> ipc.send('command', 'application:minimize') - @command 'application:zoom', -> ipc.send('command', 'application:zoom') - @command 'application:bring-all-windows-to-front', -> ipc.send('command', 'application:bring-all-windows-to-front') - @command 'application:open-your-config', -> ipc.send('command', 'application:open-your-config') - @command 'application:open-your-init-script', -> ipc.send('command', 'application:open-your-init-script') - @command 'application:open-your-keymap', -> ipc.send('command', 'application:open-your-keymap') - @command 'application:open-your-snippets', -> ipc.send('command', 'application:open-your-snippets') - @command 'application:open-your-stylesheet', -> ipc.send('command', 'application:open-your-stylesheet') - @command 'application:open-license', => @model.openLicense() - - if process.platform is 'darwin' - @command 'window:install-shell-commands', => @installShellCommands() - - @command 'window:run-package-specs', -> ipc.send('run-package-specs', path.join(atom.project.getPaths()[0], 'spec')) - - @command 'window:focus-next-pane', => @focusNextPaneView() - @command 'window:focus-previous-pane', => @focusPreviousPaneView() - @command 'window:focus-pane-above', => @focusPaneViewAbove() - @command 'window:focus-pane-below', => @focusPaneViewBelow() - @command 'window:focus-pane-on-left', => @focusPaneViewOnLeft() - @command 'window:focus-pane-on-right', => @focusPaneViewOnRight() - @command 'window:save-all', => @saveAll() - @command 'window:toggle-invisibles', -> atom.config.set("editor.showInvisibles", not atom.config.get("editor.showInvisibles")) - @command 'window:log-deprecation-warnings', -> logDeprecationWarnings() - - @command 'window:toggle-auto-indent', -> - atom.config.set("editor.autoIndent", not atom.config.get("editor.autoIndent")) - - @command 'pane:reopen-closed-item', => @getModel().reopenItem() - - @command 'core:close', => if @getModel().getActivePaneItem()? then @destroyActivePaneItem() else @destroyActivePane() - @command 'core:save', => @saveActivePaneItem() - @command 'core:save-as', => @saveActivePaneItemAs() - - @deprecatedViewEvents() + beforeRemove: -> + @model?.destroy() ### Section: Accessing the Workspace Model @@ -309,83 +222,9 @@ class WorkspaceView extends View Section: Private ### - afterAttach: (onDom) -> - @focus() if onDom - - # Called by SpacePen - beforeRemove: -> - @model.destroy() - - setEditorFontSize: (fontSize) -> - atom.themes.updateGlobalEditorStyle('font-size', fontSize + 'px') - - setEditorFontFamily: (fontFamily) -> - atom.themes.updateGlobalEditorStyle('font-family', fontFamily) - - setEditorLineHeight: (lineHeight) -> - atom.themes.updateGlobalEditorStyle('line-height', lineHeight) - - # Install the Atom shell commands on the user's system. - installShellCommands: -> - showErrorDialog = (error) -> - installDirectory = CommandInstaller.getInstallDirectory() - atom.confirm - message: "Failed to install shell commands" - detailedMessage: error.message - - resourcePath = atom.getLoadSettings().resourcePath - CommandInstaller.installAtomCommand resourcePath, true, (error) -> - if error? - showErrorDialog(error) - else - CommandInstaller.installApmCommand resourcePath, true, (error) -> - if error? - showErrorDialog(error) - else - atom.confirm - message: "Commands installed." - detailedMessage: "The shell commands `atom` and `apm` are installed." - - handleFocus: -> - if @getActivePaneView() - @getActivePaneView().focus() - false - else - @updateTitle() - focusableChild = @find("[tabindex=-1]:visible:first") - if focusableChild.length - focusableChild.focus() - false - else - $(document.body).focus() - true - # Prompts to save all unsaved items confirmClose: -> - @panes.confirmClose() - - # Updates the application's title and proxy icon based on whichever file is - # open. - updateTitle: -> - if projectPath = atom.project.getPaths()[0] - if item = @getModel().getActivePaneItem() - title = "#{item.getTitle?() ? 'untitled'} - #{projectPath}" - @setTitle(title, item.getPath?()) - else - @setTitle(projectPath, projectPath) - else - @setTitle('untitled') - - # Sets the application's title (and the proxy icon on OS X) - setTitle: (title, proxyIconPath='') -> - document.title = title - atom.setRepresentedFilename(proxyIconPath) - - # On OS X, fades the application window's proxy icon when the current file - # has been modified. - updateDocumentEdited: -> - modified = @model.getActivePaneItem()?.isModified?() ? false - atom.setDocumentEdited(modified) + @model.confirmClose() # Get all editor views. # @@ -403,7 +242,7 @@ class WorkspaceView extends View Section: Deprecated ### - deprecatedViewEvents: -> + deprecateViewEvents: -> originalWorkspaceViewOn = @on @on = (eventName) => diff --git a/src/workspace.coffee b/src/workspace.coffee index a1420ea7f..e225763e3 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -5,13 +5,13 @@ _ = require 'underscore-plus' Q = require 'q' Serializable = require 'serializable' Delegator = require 'delegato' -{Emitter, Disposable} = require 'event-kit' +{Emitter, Disposable, CompositeDisposable} = require 'event-kit' Grim = require 'grim' TextEditor = require './text-editor' PaneContainer = require './pane-container' Pane = require './pane' ViewRegistry = require './view-registry' -WorkspaceView = null +WorkspaceElement = require './workspace-element' # Essential: Represents the state of the user interface for the entire window. # An instance of this class is available via the `atom.workspace` global. @@ -30,11 +30,12 @@ class Workspace extends Model @delegatesProperty 'activePane', 'activePaneItem', toProperty: 'paneContainer' @properties + viewRegistry: null paneContainer: null fullScreen: false destroyedItemUris: -> [] - constructor: -> + constructor: (params) -> super @emitter = new Emitter @@ -44,6 +45,8 @@ class Workspace extends Model @paneContainer ?= new PaneContainer({@viewRegistry}) @paneContainer.onDidDestroyPaneItem(@onPaneItemDestroyed) + @subscribeToActiveItem() + @addOpener (filePath) => switch filePath when 'atom://.atom/stylesheet' @@ -55,6 +58,10 @@ class Workspace extends Model when 'atom://.atom/init-script' @open(atom.getUserInitScriptPath()) + @addViewProvider + modelConstructor: Workspace + viewConstructor: WorkspaceElement + # Called by the Serializable mixin during deserialization deserializeParams: (params) -> for packageName in params.packagesWithActiveGrammars ? [] @@ -71,9 +78,6 @@ class Workspace extends Model fullScreen: atom.isFullScreen() packagesWithActiveGrammars: @getPackageNamesWithActiveGrammars() - getViewClass: -> - WorkspaceView ?= require './workspace-view' - getPackageNamesWithActiveGrammars: -> packageNames = [] addGrammar = ({includedGrammarScopes, packageName}={}) -> @@ -97,6 +101,58 @@ class Workspace extends Model editorAdded: (editor) -> @emit 'editor-created', editor + installShellCommands: -> + CommandInstaller.installShellCommandsInteractively() + + subscribeToActiveItem: -> + @updateWindowTitle() + @updateDocumentEdited() + atom.project.onDidChangePaths @updateWindowTitle + + @observeActivePaneItem (item) => + @updateWindowTitle() + @updateDocumentEdited() + + @activeItemSubscriptions?.dispose() + @activeItemSubscriptions = new CompositeDisposable + + if typeof item?.onDidChangeTitle is 'function' + titleSubscription = item.onDidChangeTitle(@updateWindowTitle) + else if typeof item?.on is 'function' + titleSubscription = item.on('title-changed', @updateWindowTitle) + unless typeof titleSubscription?.dispose is 'function' + titleSubscription = new Disposable => item.off('title-changed', @updateWindowTitle) + + if typeof item?.onDidChangeModified is 'function' + modifiedSubscription = item.onDidChangeModified(@updateDocumentEdited) + else if typeof item?.on? is 'function' + modifiedSubscription = item.on('modified-status-changed', @updateDocumentEdited) + unless typeof modifiedSubscription?.dispose is 'function' + modifiedSubscription = new Disposable => item.off('modified-status-changed', @updateDocumentEdited) + + @activeItemSubscriptions.add(titleSubscription) if titleSubscription? + @activeItemSubscriptions.add(modifiedSubscription) if modifiedSubscription? + + # Updates the application's title and proxy icon based on whichever file is + # open. + updateWindowTitle: => + if projectPath = atom.project?.getPaths()[0] + if item = @getActivePaneItem() + document.title = "#{item.getTitle?() ? 'untitled'} - #{projectPath}" + atom.setRepresentedFilename(item.getPath?() ? projectPath) + else + document.title = projectPath + atom.setRepresentedFilename(projectPath) + else + document.title = 'untitled' + atom.setRepresentedFilename('') + + # On OS X, fades the application window's proxy icon when the current file + # has been modified. + updateDocumentEdited: => + modified = @getActivePaneItem()?.isModified?() ? false + atom.setDocumentEdited(modified) + ### Section: Event Subscription ### @@ -113,8 +169,8 @@ class Workspace extends Model callback(textEditor) for textEditor in @getTextEditors() @onDidAddTextEditor ({textEditor}) -> callback(textEditor) - # Essential: Invoke the given callback with all current and future panes items in - # the workspace. + # Essential: Invoke the given callback with all current and future panes items + # in the workspace. # # * `callback` {Function} to be called with current and future pane items. # * `item` An item that is present in {::getPaneItems} at the time of @@ -132,6 +188,15 @@ class Workspace extends Model # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeActivePaneItem: (callback) -> @paneContainer.onDidChangeActivePaneItem(callback) + # Essential: Invoke the given callback with the current active pane item and + # with all future active pane items in the workspace. + # + # * `callback` {Function} to be called when the active pane item changes. + # * `item` The current active pane item. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePaneItem: (callback) -> @paneContainer.observeActivePaneItem(callback) + # Essential: Invoke the given callback whenever an item is opened. Unlike # {::onDidAddPaneItem}, observers will be notified for items that are already # present in the workspace when they are reopened. @@ -417,6 +482,9 @@ class Workspace extends Model saveAll: -> @paneContainer.saveAll() + confirmClose: -> + @paneContainer.confirmClose() + # Save the active pane item. # # If the active pane item currently has a URI according to the item's @@ -477,6 +545,10 @@ class Workspace extends Model destroyActivePane: -> @activePane?.destroy() + # Destroy the active pane item or the active pane if it is empty. + destroyActivePaneItemOrEmptyPane: -> + if @getActivePaneItem()? then @destroyActivePaneItem() else @destroyActivePane() + # Increase the editor font size by 1px. increaseFontSize: -> atom.config.set("editor.fontSize", atom.config.get("editor.fontSize") + 1) @@ -503,6 +575,7 @@ class Workspace extends Model # Called by Model superclass when destroyed destroyed: -> @paneContainer.destroy() + @activeItemSubscriptions?.dispose() ### Section: View Management