diff --git a/package.json b/package.json index 743b79747..94f02cce8 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "clear-cut": "0.2.0", "coffee-script": "1.6.3", "coffeestack": "0.6.0", - "emissary": "0.19.0", + "emissary": "0.31.0", "first-mate": "0.17.0", "fs-plus": "0.14.0", "fstream": "0.1.24", @@ -36,7 +36,7 @@ "mkdirp": "0.3.5", "keytar": "0.15.0", "less-cache": "0.10.0", - "serializable": "0.1.0", + "serializable": "0.3.0", "nslog": "0.3.0", "oniguruma": "0.26.0", "optimist": "0.4.0", @@ -46,22 +46,24 @@ "scandal": "0.11.0", "season": "0.14.0", "semver": "1.1.4", - "space-pen": "3.0.3", + "space-pen": "3.1.0", "temp": "0.5.0", "text-buffer": "0.12.0", - "theorist": "~0.7.0", "underscore-plus": "0.6.1", - "vm-compatibility-layer": "0.1.0" + "vm-compatibility-layer": "0.1.0", + "theorist": "~0.13.0", + "delegato": "~0.4.0", + "mixto": "~0.4.0" }, "packageDependencies": { "atom-dark-syntax": "0.10.0", - "atom-dark-ui": "0.18.0", + "atom-dark-ui": "0.19.0", "atom-light-syntax": "0.10.0", - "atom-light-ui": "0.17.0", + "atom-light-ui": "0.18.0", "base16-tomorrow-dark-theme": "0.8.0", "solarized-dark-syntax": "0.6.0", "solarized-light-syntax": "0.2.0", - "archive-view": "0.18.0", + "archive-view": "0.19.0", "autocomplete": "0.19.0", "autoflow": "0.11.0", "autosave": "0.10.0", @@ -74,16 +76,16 @@ "editor-stats": "0.12.0", "exception-reporting": "0.11.0", "feedback": "0.22.0", - "find-and-replace": "0.73.0", + "find-and-replace": "0.74.0", "fuzzy-finder": "0.30.0", "gists": "0.14.0", "git-diff": "0.21.0", "github-sign-in": "0.16.0", "go-to-line": "0.14.0", "grammar-selector": "0.16.0", - "image-view": "0.14.0", + "image-view": "0.15.0", "keybinding-resolver": "0.8.0", - "markdown-preview": "0.23.0", + "markdown-preview": "0.24.0", "metrics": "0.21.0", "package-generator": "0.23.0", "release-notes": "0.15.0", @@ -93,15 +95,15 @@ "status-bar": "0.31.0", "styleguide": "0.19.0", "symbols-view": "0.28.0", - "tabs": "0.16.0", + "tabs": "0.17.0", "terminal": "0.24.0", - "timecop": "0.12.0", + "timecop": "0.13.0", "to-the-hubs": "0.17.0", "tree-view": "0.59.0", "visual-bell": "0.6.0", "welcome": "0.4.0", "whitespace": "0.10.0", - "wrap-guide": "0.10.0", + "wrap-guide": "0.11.0", "language-c": "0.2.0", "language-clojure": "0.1.0", "language-coffee-script": "0.4.0", diff --git a/script/clean b/script/clean index d0c7c93ea..44c921a14 100755 --- a/script/clean +++ b/script/clean @@ -17,6 +17,8 @@ var commands = [ killatom, [__dirname, '..', 'node_modules'], [__dirname, '..', 'build', 'node_modules'], + [__dirname, '..', 'apm', 'node_modules'], + [__dirname, '..', 'vendor', 'apm', 'node_modules'], [__dirname, '..', 'atom-shell'], [home, '.atom', '.node-gyp'], [home, '.atom', 'storage'], diff --git a/spec/editor-view-spec.coffee b/spec/editor-view-spec.coffee index 96d10e98c..6ab18ccdf 100644 --- a/spec/editor-view-spec.coffee +++ b/spec/editor-view-spec.coffee @@ -2756,7 +2756,7 @@ describe "EditorView", -> editorView = atom.workspaceView.getActiveView() view = $$ -> @div id: 'view', tabindex: -1, 'View' - editorView.getPane().showItem(view) + editorView.getPane().activateItem(view) expect(editorView.isVisible()).toBeFalsy() editorView.setText('hidden changes') @@ -2764,7 +2764,7 @@ describe "EditorView", -> displayUpdatedHandler = jasmine.createSpy("displayUpdatedHandler") editorView.on 'editor:display-updated', displayUpdatedHandler - editorView.getPane().showItem(editorView.getModel()) + editorView.getPane().activateItem(editorView.getModel()) expect(editorView.isVisible()).toBeTruthy() waitsFor -> @@ -2809,7 +2809,7 @@ describe "EditorView", -> atom.workspaceView.attachToDom() editorView = atom.workspaceView.getActiveView() - willBeRemovedHandler = jasmine.createSpy('fileChange') + willBeRemovedHandler = jasmine.createSpy('willBeRemovedHandler') editorView.on 'editor:will-be-removed', willBeRemovedHandler editorView.getPane().destroyActiveItem() expect(willBeRemovedHandler).toHaveBeenCalled() diff --git a/spec/pane-container-model-spec.coffee b/spec/pane-container-model-spec.coffee new file mode 100644 index 000000000..759b4d7b5 --- /dev/null +++ b/spec/pane-container-model-spec.coffee @@ -0,0 +1,77 @@ +PaneContainer = require '../src/pane-container' +Pane = require '../src/pane' + +describe "PaneContainer", -> + describe "serialization", -> + [containerA, pane1A, pane2A, pane3A] = [] + + beforeEach -> + # This is a dummy item to prevent panes from being empty on deserialization + class Item + atom.deserializers.add(this) + @deserialize: -> new this + serialize: -> deserializer: 'Item' + + pane1A = new Pane(items: [new Item]) + containerA = new PaneContainer(root: pane1A) + pane2A = pane1A.splitRight(items: [new Item]) + pane3A = pane2A.splitDown(items: [new Item]) + + it "preserves the focused pane across serialization", -> + expect(pane3A.focused).toBe true + + containerB = containerA.testSerialization() + [pane1B, pane2B, pane3B] = containerB.getPanes() + expect(pane3B.focused).toBe true + + it "preserves the active pane across serialization, independent of focus", -> + pane3A.activate() + expect(containerA.activePane).toBe pane3A + + containerB = containerA.testSerialization() + [pane1B, pane2B, pane3B] = containerB.getPanes() + expect(containerB.activePane).toBe pane3B + + describe "::activePane", -> + [container, pane1, pane2] = [] + + beforeEach -> + pane1 = new Pane + container = new PaneContainer(root: pane1) + + it "references the first pane if no pane has been made active", -> + expect(container.activePane).toBe pane1 + expect(pane1.active).toBe true + + it "references the most pane on which ::activate was most recently called", -> + pane2 = pane1.splitRight() + pane2.activate() + expect(container.activePane).toBe pane2 + expect(pane1.active).toBe false + expect(pane2.active).toBe true + pane1.activate() + expect(container.activePane).toBe pane1 + expect(pane1.active).toBe true + expect(pane2.active).toBe false + + it "is reassigned to the next pane if the current active pane is destroyed", -> + pane2 = pane1.splitRight() + pane2.activate() + pane2.destroy() + expect(container.activePane).toBe pane1 + expect(pane1.active).toBe true + pane1.destroy() + expect(container.activePane).toBe null + + describe "when the last pane is removed", -> + [container, pane, surrenderedFocusHandler] = [] + + beforeEach -> + pane = new Pane + container = new PaneContainer(root: pane) + container.on 'surrendered-focus', surrenderedFocusHandler = jasmine.createSpy("surrenderedFocusHandler") + + it "assigns null to the root and the activePane", -> + pane.destroy() + expect(container.root).toBe null + expect(container.activePane).toBe null diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-view-spec.coffee similarity index 79% rename from spec/pane-container-spec.coffee rename to spec/pane-container-view-spec.coffee index 13ff182f6..fa0bcb83d 100644 --- a/spec/pane-container-spec.coffee +++ b/spec/pane-container-view-spec.coffee @@ -1,10 +1,10 @@ path = require 'path' temp = require 'temp' -PaneContainer = require '../src/pane-container' -Pane = require '../src/pane' +PaneContainerView = require '../src/pane-container-view' +PaneView = require '../src/pane-view' {_, $, View, $$} = require 'atom' -describe "PaneContainer", -> +describe "PaneContainerView", -> [TestView, container, pane1, pane2, pane3] = [] beforeEach -> @@ -16,10 +16,10 @@ describe "PaneContainer", -> serialize: -> { deserializer: 'TestView', @name } getUri: -> path.join(temp.dir, @name) save: -> @saved = true - isEqual: (other) -> @name is other.name + isEqual: (other) -> @name is other?.name - container = new PaneContainer - pane1 = new Pane(new TestView('1')) + container = new PaneContainerView + pane1 = new PaneView(new TestView('1')) container.setRoot(pane1) pane2 = pane1.splitRight(new TestView('2')) pane3 = pane2.splitDown(new TestView('3')) @@ -42,6 +42,8 @@ describe "PaneContainer", -> describe ".focusPreviousPane()", -> it "focuses the pane preceding the focused pane or the last pane if no pane has focus", -> container.attachToDom() + $(document.body).focus() # clear focus + container.focusPreviousPane() expect(pane3.activeItem).toMatchSelector ':focus' container.focusPreviousPane() @@ -69,10 +71,6 @@ describe "PaneContainer", -> expect(container.getFocusedPane()).toBe pane3 expect(container.getActivePane()).toBe pane3 - # returns the first pane if none have been set to active - container.find('.pane.active').removeClass('active') - expect(container.getActivePane()).toBe pane1 - describe ".eachPane(callback)", -> it "runs the callback with all current and future panes until the subscription is cancelled", -> panes = [] @@ -90,7 +88,7 @@ describe "PaneContainer", -> describe ".saveAll()", -> it "saves all open pane items", -> - pane1.showItem(new TestView('4')) + pane1.activateItem(new TestView('4')) container.saveAll() @@ -124,19 +122,19 @@ describe "PaneContainer", -> describe "serialization", -> it "can be serialized and deserialized, and correctly adjusts dimensions of deserialized panes after attach", -> newContainer = atom.deserializers.deserialize(container.serialize()) - expect(newContainer.find('.row > :contains(1)')).toExist() - expect(newContainer.find('.row > .column > :contains(2)')).toExist() - expect(newContainer.find('.row > .column > :contains(3)')).toExist() + 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() newContainer.height(200).width(300).attachToDom() - expect(newContainer.find('.row > :contains(1)').width()).toBe 150 - expect(newContainer.find('.row > .column > :contains(2)').height()).toBe 100 + expect(newContainer.find('.pane-row > :contains(1)').width()).toBe 150 + expect(newContainer.find('.pane-row > .pane-column > :contains(2)').height()).toBe 100 - xit "removes empty panes on deserialization", -> + it "removes empty panes on deserialization", -> # only deserialize pane 1's view successfully TestView.deserialize = ({name}) -> new TestView(name) if name is '1' newContainer = atom.deserializers.deserialize(container.serialize()) - expect(newContainer.find('.row, .column')).not.toExist() + expect(newContainer.find('.pane-row, .pane-column')).not.toExist() expect(newContainer.find('> :contains(1)')).toExist() describe "pane-container:active-pane-item-changed", -> @@ -148,9 +146,9 @@ describe "PaneContainer", -> item2b = new TestView('2b') item3a = new TestView('3a') - container = new PaneContainer + container = new PaneContainerView container.attachToDom() - pane1 = new Pane(item1a) + pane1 = new PaneView(item1a) container.setRoot(pane1) activeItemChangedHandler = jasmine.createSpy("activeItemChangedHandler") @@ -162,50 +160,50 @@ describe "PaneContainer", -> expect(container.getPanes().length).toBe 0 activeItemChangedHandler.reset() - pane = new Pane(item1a) + pane = new PaneView(item1a) container.setRoot(pane) expect(activeItemChangedHandler.callCount).toBe 1 expect(activeItemChangedHandler.argsForCall[0][1]).toEqual item1a describe "when there is one pane", -> it "is triggered when a new pane item is added", -> - pane1.showItem(item1b) + pane1.activateItem(item1b) expect(activeItemChangedHandler.callCount).toBe 1 expect(activeItemChangedHandler.argsForCall[0][1]).toEqual item1b it "is not triggered when the active pane item is shown again", -> - pane1.showItem(item1a) + pane1.activateItem(item1a) expect(activeItemChangedHandler).not.toHaveBeenCalled() it "is triggered when switching to an existing pane item", -> - pane1.showItem(item1b) + pane1.activateItem(item1b) activeItemChangedHandler.reset() - pane1.showItem(item1a) + pane1.activateItem(item1a) expect(activeItemChangedHandler.callCount).toBe 1 expect(activeItemChangedHandler.argsForCall[0][1]).toEqual item1a - it "is triggered when the active pane item is removed", -> - pane1.showItem(item1b) + it "is triggered when the active pane item is destroyed", -> + pane1.activateItem(item1b) activeItemChangedHandler.reset() - pane1.removeItem(item1b) + pane1.destroyItem(item1b) expect(activeItemChangedHandler.callCount).toBe 1 expect(activeItemChangedHandler.argsForCall[0][1]).toEqual item1a - it "is not triggered when an inactive pane item is removed", -> - pane1.showItem(item1b) + it "is not triggered when an inactive pane item is destroyed", -> + pane1.activateItem(item1b) activeItemChangedHandler.reset() - pane1.removeItem(item1a) + pane1.destroyItem(item1a) expect(activeItemChangedHandler).not.toHaveBeenCalled() - it "is triggered when all pane items are removed", -> - pane1.removeItem(item1a) + it "is triggered when all pane items are destroyed", -> + pane1.destroyItem(item1a) expect(activeItemChangedHandler.callCount).toBe 1 expect(activeItemChangedHandler.argsForCall[0][1]).toBe undefined - it "is triggered when the pane is removed", -> + it "is triggered when the pane is destroyed", -> pane1.remove() expect(activeItemChangedHandler.callCount).toBe 1 expect(activeItemChangedHandler.argsForCall[0][1]).toBe undefined @@ -218,40 +216,40 @@ describe "PaneContainer", -> activeItemChangedHandler.reset() it "is triggered when a new pane item is added to the active pane", -> - pane2.showItem(item2b) + pane2.activateItem(item2b) expect(activeItemChangedHandler.callCount).toBe 1 expect(activeItemChangedHandler.argsForCall[0][1]).toEqual item2b it "is not triggered when a new pane item is added to an inactive pane", -> - pane1.showItem(item1b) + pane1.activateItem(item1b) expect(activeItemChangedHandler).not.toHaveBeenCalled() - it "is triggered when the active pane item removed from the active pane", -> - pane2.showItem(item2b) + it "is triggered when the active pane's active item is destroyed", -> + pane2.activateItem(item2b) activeItemChangedHandler.reset() - pane2.removeItem(item2b) + pane2.destroyItem(item2b) expect(activeItemChangedHandler.callCount).toBe 1 expect(activeItemChangedHandler.argsForCall[0][1]).toEqual item2a - it "is not triggered when the active pane item removed from an inactive pane", -> - pane1.showItem(item1b) + it "is not triggered when an inactive pane's active item is destroyed", -> + pane1.activateItem(item1b) activeItemChangedHandler.reset() - pane1.removeItem(item1b) + pane1.destroyItem(item1b) expect(activeItemChangedHandler).not.toHaveBeenCalled() - it "is triggered when the active pane is removed", -> + it "is triggered when the active pane is destroyed", -> pane2.remove() expect(activeItemChangedHandler.callCount).toBe 1 expect(activeItemChangedHandler.argsForCall[0][1]).toEqual item1a - it "is not triggered when an inactive pane is removed", -> + it "is not triggered when an inactive pane is destroyed", -> pane1.remove() expect(activeItemChangedHandler).not.toHaveBeenCalled() it "is triggered when the active pane is changed", -> - pane1.makeActive() + pane1.activate() expect(activeItemChangedHandler.callCount).toBe 1 expect(activeItemChangedHandler.argsForCall[0][1]).toEqual item1a @@ -265,7 +263,7 @@ describe "PaneContainer", -> expect(activeItemChangedHandler.callCount).toBe 1 expect(activeItemChangedHandler.argsForCall[0][1]).toEqual item3a - it "is not triggered when the non active pane is removed", -> + it "is not triggered when an inactive pane is destroyed", -> pane3 = pane2.splitDown(item3a) activeItemChangedHandler.reset() diff --git a/spec/pane-model-spec.coffee b/spec/pane-model-spec.coffee new file mode 100644 index 000000000..0c15683a4 --- /dev/null +++ b/spec/pane-model-spec.coffee @@ -0,0 +1,134 @@ +{Model} = require 'theorist' +Pane = require '../src/pane' +PaneAxis = require '../src/pane-axis' +PaneContainer = require '../src/pane-container' + +describe "Pane", -> + describe "split methods", -> + [pane1, container] = [] + + beforeEach -> + pane1 = new Pane(items: ["A"]) + container = new PaneContainer(root: pane1) + + describe "::splitLeft(params)", -> + describe "when the parent is the container root", -> + it "replaces itself with a row and inserts a new pane to the left of itself", -> + pane2 = pane1.splitLeft(items: ["B"]) + pane3 = pane1.splitLeft(items: ["C"]) + expect(container.root.orientation).toBe 'horizontal' + expect(container.root.children).toEqual [pane2, pane3, pane1] + + describe "when the parent is a column", -> + it "replaces itself with a row and inserts a new pane to the left of itself", -> + pane1.splitDown() + pane2 = pane1.splitLeft(items: ["B"]) + pane3 = pane1.splitLeft(items: ["C"]) + row = container.root.children[0] + expect(row.orientation).toBe 'horizontal' + expect(row.children).toEqual [pane2, pane3, pane1] + + describe "::splitRight(params)", -> + describe "when the parent is the container root", -> + it "replaces itself with a row and inserts a new pane to the right of itself", -> + pane2 = pane1.splitRight(items: ["B"]) + pane3 = pane1.splitRight(items: ["C"]) + expect(container.root.orientation).toBe 'horizontal' + expect(container.root.children).toEqual [pane1, pane3, pane2] + + describe "when the parent is a column", -> + it "replaces itself with a row and inserts a new pane to the right of itself", -> + pane1.splitDown() + pane2 = pane1.splitRight(items: ["B"]) + pane3 = pane1.splitRight(items: ["C"]) + row = container.root.children[0] + expect(row.orientation).toBe 'horizontal' + expect(row.children).toEqual [pane1, pane3, pane2] + + describe "::splitUp(params)", -> + describe "when the parent is the container root", -> + it "replaces itself with a column and inserts a new pane above itself", -> + pane2 = pane1.splitUp(items: ["B"]) + pane3 = pane1.splitUp(items: ["C"]) + expect(container.root.orientation).toBe 'vertical' + expect(container.root.children).toEqual [pane2, pane3, pane1] + + describe "when the parent is a row", -> + it "replaces itself with a column and inserts a new pane above itself", -> + pane1.splitRight() + pane2 = pane1.splitUp(items: ["B"]) + pane3 = pane1.splitUp(items: ["C"]) + column = container.root.children[0] + expect(column.orientation).toBe 'vertical' + expect(column.children).toEqual [pane2, pane3, pane1] + + describe "::splitDown(params)", -> + describe "when the parent is the container root", -> + it "replaces itself with a column and inserts a new pane below itself", -> + pane2 = pane1.splitDown(items: ["B"]) + pane3 = pane1.splitDown(items: ["C"]) + expect(container.root.orientation).toBe 'vertical' + expect(container.root.children).toEqual [pane1, pane3, pane2] + + describe "when the parent is a row", -> + it "replaces itself with a column and inserts a new pane below itself", -> + pane1.splitRight() + pane2 = pane1.splitDown(items: ["B"]) + pane3 = pane1.splitDown(items: ["C"]) + column = container.root.children[0] + expect(column.orientation).toBe 'vertical' + expect(column.children).toEqual [pane1, pane3, pane2] + + it "sets up the new pane to be focused", -> + expect(pane1.focused).toBe false + pane2 = pane1.splitRight() + expect(pane2.focused).toBe true + + describe "::destroyItem(item)", -> + describe "when the last item is destroyed", -> + it "destroys the pane", -> + pane = new Pane(items: ["A", "B"]) + pane.destroyItem("A") + pane.destroyItem("B") + expect(pane.isDestroyed()).toBe true + + describe "when an item emits a destroyed event", -> + it "removes it from the list of items", -> + pane = new Pane(items: [new Model, new Model, new Model]) + [item1, item2, item3] = pane.items + pane.items[1].destroy() + expect(pane.items).toEqual [item1, item3] + + describe "::destroy()", -> + [pane1, container] = [] + + beforeEach -> + pane1 = new Pane(items: [new Model, new Model]) + container = new PaneContainer(root: pane1) + + it "destroys the pane's destroyable items", -> + [item1, item2] = pane1.items + pane1.destroy() + expect(item1.isDestroyed()).toBe true + expect(item2.isDestroyed()).toBe true + + describe "if the pane's parent has more than two children", -> + it "removes the pane from its parent", -> + pane2 = pane1.splitRight() + pane3 = pane2.splitRight() + + expect(container.root.children).toEqual [pane1, pane2, pane3] + pane2.destroy() + expect(container.root.children).toEqual [pane1, pane3] + + describe "if the pane's parent has two children", -> + it "replaces the parent with its last remaining child", -> + pane2 = pane1.splitRight() + pane3 = pane2.splitDown() + + expect(container.root.children[0]).toBe pane1 + expect(container.root.children[1].children).toEqual [pane2, pane3] + pane3.destroy() + expect(container.root.children).toEqual [pane1, pane2] + pane2.destroy() + expect(container.root).toBe pane1 diff --git a/spec/pane-spec.coffee b/spec/pane-view-spec.coffee similarity index 77% rename from spec/pane-spec.coffee rename to spec/pane-view-spec.coffee index 6851bb8f4..7440f7e73 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-view-spec.coffee @@ -1,10 +1,10 @@ -PaneContainer = require '../src/pane-container' -Pane = require '../src/pane' +PaneContainerView = require '../src/pane-container-view' +PaneView = require '../src/pane-view' {fs, $, View} = require 'atom' path = require 'path' temp = require 'temp' -describe "Pane", -> +describe "PaneView", -> [container, view1, view2, editor1, editor2, pane] = [] class TestView extends View @@ -13,16 +13,16 @@ describe "Pane", -> initialize: ({@id, @text}) -> serialize: -> { deserializer: 'TestView', @id, @text } getUri: -> @id - isEqual: (other) -> @id == other.id and @text == other.text + isEqual: (other) -> other? and @id == other.id and @text == other.text beforeEach -> atom.deserializers.add(TestView) - container = new PaneContainer + container = new PaneContainerView view1 = new TestView(id: 'view-1', text: 'View 1') view2 = new TestView(id: 'view-2', text: 'View 2') editor1 = atom.project.openSync('sample.js') editor2 = atom.project.openSync('sample.txt') - pane = new Pane(view1, editor1, view2, editor2) + pane = new PaneView(view1, editor1, view2, editor2) container.setRoot(pane) afterEach -> @@ -32,49 +32,49 @@ describe "Pane", -> it "displays the first item in the pane", -> expect(pane.itemViews.find('#view-1')).toExist() - describe "::showItem(item)", -> + describe "::activateItem(item)", -> it "hides all item views except the one being shown and sets the activeItem", -> expect(pane.activeItem).toBe view1 - pane.showItem(view2) + pane.activateItem(view2) expect(view1.css('display')).toBe 'none' expect(view2.css('display')).not.toBe 'none' expect(pane.activeItem).toBe view2 it "triggers 'pane:active-item-changed' if the item isn't already the activeItem", -> - pane.makeActive() + pane.activate() itemChangedHandler = jasmine.createSpy("itemChangedHandler") container.on 'pane:active-item-changed', itemChangedHandler expect(pane.activeItem).toBe view1 - pane.showItem(view2) - pane.showItem(view2) + pane.activateItem(view2) + pane.activateItem(view2) expect(itemChangedHandler.callCount).toBe 1 expect(itemChangedHandler.argsForCall[0][1]).toBe view2 itemChangedHandler.reset() - pane.showItem(editor1) + pane.activateItem(editor1) expect(itemChangedHandler).toHaveBeenCalled() expect(itemChangedHandler.argsForCall[0][1]).toBe editor1 itemChangedHandler.reset() - describe "if the pane's active view is focused before calling showItem", -> + describe "if the pane's active view is focused before calling activateItem", -> it "focuses the new active view", -> container.attachToDom() pane.focus() expect(pane.activeView).not.toBe view2 expect(pane.activeView).toMatchSelector ':focus' - pane.showItem(view2) + pane.activateItem(view2) expect(view2).toMatchSelector ':focus' describe "when the given item isn't yet in the items list on the pane", -> view3 = null beforeEach -> view3 = new TestView(id: 'view-3', text: "View 3") - pane.showItem(editor1) + pane.activateItem(editor1) expect(pane.getActiveItemIndex()).toBe 1 it "adds it to the items list after the active item", -> - pane.showItem(view3) + pane.activateItem(view3) expect(pane.getItems()).toEqual [view1, editor1, view3, view2, editor2] expect(pane.activeItem).toBe view3 expect(pane.getActiveItemIndex()).toBe 2 @@ -83,21 +83,21 @@ describe "Pane", -> events = [] container.on 'pane:item-added', (e, item, index) -> events.push(['pane:item-added', item, index]) container.on 'pane:active-item-changed', (e, item) -> events.push(['pane:active-item-changed', item]) - pane.showItem(view3) + pane.activateItem(view3) expect(events).toEqual [['pane:item-added', view3, 2], ['pane:active-item-changed', view3]] describe "when showing a model item", -> describe "when no view has yet been appended for that item", -> it "appends and shows a view to display the item based on its `.getViewClass` method", -> - pane.showItem(editor1) + pane.activateItem(editor1) editorView = pane.activeView expect(editorView.css('display')).not.toBe 'none' expect(editorView.editor).toBe editor1 describe "when a valid view has already been appended for another item", -> it "multiple views are created for multiple items", -> - pane.showItem(editor1) - pane.showItem(editor2) + pane.activateItem(editor1) + pane.activateItem(editor2) expect(pane.itemViews.find('.editor').length).toBe 2 editorView = pane.activeView expect(editorView.css('display')).not.toBe 'none' @@ -118,23 +118,23 @@ describe "Pane", -> serialize: -> {@id, @text} getViewClass: -> TestView - pane.showItem(model1) - pane.showItem(model2) + pane.activateItem(model1) + pane.activateItem(model2) expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 2 - pane.showPreviousItem() + pane.activatePreviousItem() expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 2 - pane.removeItem(model2) + pane.destroyItem(model2) expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 1 - pane.removeItem(model1) + pane.destroyItem(model1) expect(pane.itemViews.find('.test-view').length).toBe initialViewCount describe "when showing a view item", -> it "appends it to the itemViews div if it hasn't already been appended and shows it", -> expect(pane.itemViews.find('#view-2')).not.toExist() - pane.showItem(view2) + pane.activateItem(view2) expect(pane.itemViews.find('#view-2')).toExist() expect(pane.activeView).toBe view2 @@ -196,27 +196,31 @@ describe "Pane", -> expect(pane.getItems().indexOf(editor2)).not.toBe -1 expect(editor2.isDestroyed()).toBe false - describe "::removeItem(item)", -> + it "removes the item's associated view", -> + view1.remove = (selector, keepData) -> @wasRemoved = not keepData + pane.destroyItem(view1) + expect(view1.wasRemoved).toBe true + it "removes the item from the items list and shows the next item if it was showing", -> - pane.removeItem(view1) + pane.destroyItem(view1) expect(pane.getItems()).toEqual [editor1, view2, editor2] expect(pane.activeItem).toBe editor1 - pane.showItem(editor2) - pane.removeItem(editor2) + pane.activateItem(editor2) + pane.destroyItem(editor2) expect(pane.getItems()).toEqual [editor1, view2] expect(pane.activeItem).toBe editor1 it "triggers 'pane:item-removed' with the item and its former index", -> itemRemovedHandler = jasmine.createSpy("itemRemovedHandler") pane.on 'pane:item-removed', itemRemovedHandler - pane.removeItem(editor1) + pane.destroyItem(editor1) expect(itemRemovedHandler).toHaveBeenCalled() expect(itemRemovedHandler.argsForCall[0][1..2]).toEqual [editor1, 1] describe "when removing the last item", -> it "removes the pane", -> - pane.removeItem(item) for item in pane.getItems() + pane.destroyItem(item) for item in pane.getItems() expect(pane.hasParent()).toBeFalsy() describe "when the pane is focused", -> @@ -226,22 +230,22 @@ describe "Pane", -> pane2 = pane.splitRight(new TestView(id: 'view-3', text: 'View 3')) pane.focus() expect(pane).toMatchSelector(':has(:focus)') - pane.removeItem(item) for item in pane.getItems() + pane.destroyItem(item) for item in pane.getItems() expect(pane2).toMatchSelector ':has(:focus)' describe "when the item is a view", -> it "removes the item from the 'item-views' div", -> expect(view1.parent()).toMatchSelector pane.itemViews - pane.removeItem(view1) + pane.destroyItem(view1) expect(view1.parent()).not.toMatchSelector pane.itemViews describe "when the item is a model", -> it "removes the associated view only when all items that require it have been removed", -> - pane.showItem(editor1) - pane.showItem(editor2) - pane.removeItem(editor2) + pane.activateItem(editor1) + pane.activateItem(editor2) + pane.destroyItem(editor2) expect(pane.itemViews.find('.editor')).toExist() - pane.removeItem(editor1) + pane.destroyItem(editor1) expect(pane.itemViews.find('.editor')).not.toExist() describe "::moveItem(item, index)", -> @@ -281,9 +285,9 @@ describe "Pane", -> describe "when it is the last item on the source pane", -> it "removes the source pane, but does not destroy the item", -> - pane.removeItem(view1) - pane.removeItem(view2) - pane.removeItem(editor2) + pane.destroyItem(view1) + pane.destroyItem(view2) + pane.destroyItem(editor2) expect(pane.getItems()).toEqual [editor1] pane.moveItemToPane(editor1, pane2, 1) @@ -296,12 +300,12 @@ describe "Pane", -> it "preserves data by detaching instead of removing", -> view1.data('preservative', 1234) pane.moveItemToPane(view1, pane2, 1) - pane2.showItemAtIndex(1) + pane2.activateItemAtIndex(1) expect(pane2.activeView.data('preservative')).toBe 1234 describe "pane:close", -> it "destroys all items and removes the pane", -> - pane.showItem(editor1) + pane.activateItem(editor1) pane.trigger 'pane:close' expect(pane.hasParent()).toBeFalsy() expect(editor2.isDestroyed()).toBe true @@ -309,7 +313,7 @@ describe "Pane", -> describe "pane:close-other-items", -> it "destroys all items except the current", -> - pane.showItem(editor1) + pane.activateItem(editor1) pane.trigger 'pane:close-other-items' expect(editor2.isDestroyed()).toBe true expect(pane.getItems()).toEqual [editor1] @@ -319,7 +323,7 @@ describe "Pane", -> describe "when the current item has a save method", -> it "saves the current item", -> spyOn(editor2, 'save') - pane.showItem(editor2) + pane.activateItem(editor2) pane.saveActiveItem() expect(editor2.save).toHaveBeenCalled() @@ -337,7 +341,7 @@ describe "Pane", -> it "opens a save dialog and saves the current item as the selected path", -> newEditor = atom.project.openSync() spyOn(newEditor, 'saveAs') - pane.showItem(newEditor) + pane.activateItem(newEditor) pane.saveActiveItem() @@ -357,7 +361,7 @@ describe "Pane", -> describe "when the current item has a saveAs method", -> it "opens the save dialog and calls saveAs on the item with the selected path", -> spyOn(editor2, 'saveAs') - pane.showItem(editor2) + pane.activateItem(editor2) pane.saveActiveItemAs() @@ -405,7 +409,7 @@ describe "Pane", -> expect(activeItemTitleChangedHandler).toHaveBeenCalled() activeItemTitleChangedHandler.reset() - pane.showItem(view2) + pane.activateItem(view2) view2.trigger 'title-changed' expect(activeItemTitleChangedHandler).toHaveBeenCalled() @@ -413,7 +417,7 @@ describe "Pane", -> it "removes the pane item", -> filePath = temp.openSync('atom').path editor = atom.project.openSync(filePath) - pane.showItem(editor) + pane.activateItem(editor) expect(pane.items).toHaveLength(5) fs.removeSync(filePath) @@ -437,29 +441,19 @@ describe "Pane", -> [paneToLeft, paneToRight] = [] beforeEach -> - pane.showItem(editor1) + pane.activateItem(editor1) paneToLeft = pane.splitLeft(pane.copyActiveItem()) paneToRight = pane.splitRight(pane.copyActiveItem()) container.attachToDom() - describe "when the removed pane is focused", -> - it "activates and focuses the next pane", -> - pane.focus() + describe "when the removed pane is active", -> + it "makes the next the next pane active and focuses it", -> + pane.activate() pane.remove() expect(paneToLeft.isActive()).toBeFalsy() expect(paneToRight.isActive()).toBeTruthy() expect(paneToRight).toMatchSelector ':has(:focus)' - describe "when the removed pane is active but not focused", -> - it "activates the next pane, but does not focus it", -> - $(document.activeElement).blur() - expect(pane).not.toMatchSelector ':has(:focus)' - pane.makeActive() - pane.remove() - expect(paneToLeft.isActive()).toBeFalsy() - expect(paneToRight.isActive()).toBeTruthy() - expect(paneToRight).not.toMatchSelector ':has(:focus)' - describe "when the removed pane is not active", -> it "does not affect the active pane or the focus", -> paneToLeft.focus() @@ -491,7 +485,7 @@ describe "Pane", -> describe "::getNextPane()", -> it "returns the next pane if one exists, wrapping around from the last pane to the first", -> - pane.showItem(editor1) + pane.activateItem(editor1) expect(pane.getNextPane()).toBeUndefined pane2 = pane.splitRight(pane.copyActiveItem()) expect(pane.getNextPane()).toBe pane2 @@ -537,7 +531,7 @@ describe "Pane", -> [pane1, view3, view4] = [] beforeEach -> pane1 = pane - pane.showItem(editor1) + pane.activateItem(editor1) view3 = new TestView(id: 'view-3', text: 'View 3') view4 = new TestView(id: 'view-4', text: 'View 4') @@ -545,130 +539,61 @@ describe "Pane", -> it "builds a row if needed, then appends a new pane after itself", -> # creates the new pane with a copy of the active item if none are given pane2 = pane1.splitRight(pane1.copyActiveItem()) - expect(container.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] + expect(container.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] expect(pane2.items).toEqual [editor1] expect(pane2.activeItem).not.toBe editor1 # it's a copy pane3 = pane2.splitRight(view3, view4) expect(pane3.getItems()).toEqual [view3, view4] - expect(container.find('.row .pane').toArray()).toEqual [pane[0], pane2[0], pane3[0]] + expect(container.find('.pane-row .pane').toArray()).toEqual [pane[0], pane2[0], pane3[0]] it "builds a row if needed, then appends a new pane after itself ", -> # creates the new pane with a copy of the active item if none are given pane2 = pane1.splitRight() - expect(container.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] + expect(container.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] expect(pane2.items).toEqual [] - expect(pane2.activeItem).toBe null + expect(pane2.activeItem).toBeUndefined() pane3 = pane2.splitRight() - expect(container.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0], pane3[0]] + expect(container.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0], pane3[0]] expect(pane3.items).toEqual [] - expect(pane3.activeItem).toBe null + expect(pane3.activeItem).toBeUndefined() describe "splitLeft(items...)", -> it "builds a row if needed, then appends a new pane before itself", -> # creates the new pane with a copy of the active item if none are given pane2 = pane.splitLeft(pane1.copyActiveItem()) - expect(container.find('.row .pane').toArray()).toEqual [pane2[0], pane[0]] + expect(container.find('.pane-row .pane').toArray()).toEqual [pane2[0], pane[0]] expect(pane2.items).toEqual [editor1] expect(pane2.activeItem).not.toBe editor1 # it's a copy pane3 = pane2.splitLeft(view3, view4) expect(pane3.getItems()).toEqual [view3, view4] - expect(container.find('.row .pane').toArray()).toEqual [pane3[0], pane2[0], pane[0]] + expect(container.find('.pane-row .pane').toArray()).toEqual [pane3[0], pane2[0], pane[0]] describe "splitDown(items...)", -> it "builds a column if needed, then appends a new pane after itself", -> # creates the new pane with a copy of the active item if none are given pane2 = pane.splitDown(pane1.copyActiveItem()) - expect(container.find('.column .pane').toArray()).toEqual [pane[0], pane2[0]] + expect(container.find('.pane-column .pane').toArray()).toEqual [pane[0], pane2[0]] expect(pane2.items).toEqual [editor1] expect(pane2.activeItem).not.toBe editor1 # it's a copy pane3 = pane2.splitDown(view3, view4) expect(pane3.getItems()).toEqual [view3, view4] - expect(container.find('.column .pane').toArray()).toEqual [pane[0], pane2[0], pane3[0]] + expect(container.find('.pane-column .pane').toArray()).toEqual [pane[0], pane2[0], pane3[0]] describe "splitUp(items...)", -> it "builds a column if needed, then appends a new pane before itself", -> # creates the new pane with a copy of the active item if none are given pane2 = pane.splitUp(pane1.copyActiveItem()) - expect(container.find('.column .pane').toArray()).toEqual [pane2[0], pane[0]] + expect(container.find('.pane-column .pane').toArray()).toEqual [pane2[0], pane[0]] expect(pane2.items).toEqual [editor1] expect(pane2.activeItem).not.toBe editor1 # it's a copy pane3 = pane2.splitUp(view3, view4) expect(pane3.getItems()).toEqual [view3, view4] - expect(container.find('.column .pane').toArray()).toEqual [pane3[0], pane2[0], pane[0]] - - it "lays out nested panes by equally dividing their containing row / column", -> - container.width(520).height(240).attachToDom() - pane1.showItem($("1")) - pane1 - .splitLeft($("2")) - .splitUp($("3")) - .splitLeft($("4")) - .splitDown($("5")) - - row1 = container.children(':eq(0)') - expect(row1.children().length).toBe 2 - column1 = row1.children(':eq(0)').view() - pane1 = row1.children(':eq(1)').view() - expect(column1.outerWidth()).toBe Math.round(2/3 * container.width()) - expect(column1.outerHeight()).toBe container.height() - expect(pane1.outerWidth()).toBe Math.round(1/3 * container.width()) - expect(pane1.outerHeight()).toBe container.height() - expect(Math.round(pane1.position().left)).toBe column1.outerWidth() - - expect(column1.children().length).toBe 2 - row2 = column1.children(':eq(0)').view() - pane2 = column1.children(':eq(1)').view() - expect(row2.outerWidth()).toBe column1.outerWidth() - expect(row2.height()).toBe 2/3 * container.height() - expect(pane2.outerWidth()).toBe column1.outerWidth() - expect(pane2.outerHeight()).toBe 1/3 * container.height() - expect(Math.round(pane2.position().top)).toBe row2.height() - - expect(row2.children().length).toBe 2 - column3 = row2.children(':eq(0)').view() - pane3 = row2.children(':eq(1)').view() - expect(column3.outerWidth()).toBe Math.round(1/3 * container.width()) - expect(column3.outerHeight()).toBe row2.outerHeight() - # the built in rounding seems to be rounding x.5 down, but we need to go up. this sucks. - expect(Math.round(pane3.trueWidth())).toBe Math.round(1/3 * container.width()) - expect(pane3.height()).toBe row2.outerHeight() - expect(Math.round(pane3.position().left)).toBe column3.width() - - expect(column3.children().length).toBe 2 - pane4 = column3.children(':eq(0)').view() - pane5 = column3.children(':eq(1)').view() - expect(pane4.outerWidth()).toBe column3.width() - expect(pane4.outerHeight()).toBe 1/3 * container.height() - expect(pane5.outerWidth()).toBe column3.width() - expect(Math.round(pane5.position().top)).toBe pane4.outerHeight() - expect(pane5.outerHeight()).toBe 1/3 * container.height() - - pane5.remove() - expect(column3.parent()).not.toExist() - expect(pane2.outerHeight()).toBe Math.floor(1/2 * container.height()) - expect(pane3.outerHeight()).toBe Math.floor(1/2 * container.height()) - expect(pane4.outerHeight()).toBe Math.floor(1/2 * container.height()) - - pane4.remove() - expect(row2.parent()).not.toExist() - expect(pane1.outerWidth()).toBe Math.floor(1/2 * container.width()) - expect(pane2.outerWidth()).toBe Math.floor(1/2 * container.width()) - expect(pane3.outerWidth()).toBe Math.floor(1/2 * container.width()) - - pane3.remove() - expect(column1.parent()).not.toExist() - expect(pane2.outerHeight()).toBe container.height() - - pane2.remove() - expect(row1.parent()).not.toExist() - expect(container.children().length).toBe 1 - expect(container.children('.pane').length).toBe 1 - expect(pane1.outerWidth()).toBe container.width() + expect(container.find('.pane-column .pane').toArray()).toEqual [pane3[0], pane2[0], pane[0]] describe "::itemForUri(uri)", -> it "returns the item for which a call to .getUri() returns the given uri", -> @@ -681,7 +606,7 @@ describe "Pane", -> expect(newPane.getItems()).toEqual [view1, editor1, view2, editor2] it "restores the active item on deserialization", -> - pane.showItem(editor2) + pane.activateItem(editor2) newPane = pane.testSerialization() expect(newPane.activeItem).toEqual editor2 @@ -691,7 +616,7 @@ describe "Pane", -> class Unserializable getViewClass: -> TestView - pane.showItem(new Unserializable) + pane.activateItem(new Unserializable) newPane = pane.testSerialization() expect(newPane.activeItem).toEqual pane.items[0] diff --git a/spec/workspace-view-spec.coffee b/spec/workspace-view-spec.coffee index 216466aee..f91fb8845 100644 --- a/spec/workspace-view-spec.coffee +++ b/spec/workspace-view-spec.coffee @@ -2,7 +2,7 @@ Q = require 'q' path = require 'path' temp = require 'temp' -Pane = require '../src/pane' +PaneView = require '../src/pane-view' describe "WorkspaceView", -> pathToOpen = null @@ -48,20 +48,20 @@ describe "WorkspaceView", -> pane2 = pane1.splitRight() pane3 = pane2.splitRight() pane4 = pane2.splitDown() - pane2.showItem(atom.project.openSync('b')) - pane3.showItem(atom.project.openSync('../sample.js')) + pane2.activateItem(atom.project.openSync('b')) + pane3.activateItem(atom.project.openSync('../sample.js')) pane3.activeItem.setCursorScreenPosition([2, 4]) - pane4.showItem(atom.project.openSync('../sample.txt')) + pane4.activateItem(atom.project.openSync('../sample.txt')) pane4.activeItem.setCursorScreenPosition([0, 2]) pane2.focus() simulateReload() expect(atom.workspaceView.getEditorViews().length).toBe 4 - editor1 = atom.workspaceView.panes.find('.row > .pane .editor:eq(0)').view() - editor3 = atom.workspaceView.panes.find('.row > .pane .editor:eq(1)').view() - editor2 = atom.workspaceView.panes.find('.row > .column > .pane .editor:eq(0)').view() - editor4 = atom.workspaceView.panes.find('.row > .column > .pane .editor:eq(1)').view() + editor1 = atom.workspaceView.panes.find('.pane-row > .pane .editor:eq(0)').view() + editor3 = atom.workspaceView.panes.find('.pane-row > .pane .editor:eq(1)').view() + editor2 = atom.workspaceView.panes.find('.pane-row > .pane-column > .pane .editor:eq(0)').view() + editor4 = atom.workspaceView.panes.find('.pane-row > .pane-column > .pane .editor:eq(1)').view() expect(editor1.getPath()).toBe atom.project.resolve('a') expect(editor2.getPath()).toBe atom.project.resolve('b') @@ -212,7 +212,7 @@ describe "WorkspaceView", -> describe ".openSync(filePath, options)", -> describe "when there is no active pane", -> beforeEach -> - spyOn(Pane.prototype, 'focus') + spyOn(PaneView.prototype, 'focus') atom.workspaceView.getActivePane().remove() expect(atom.workspaceView.getActivePane()).toBeUndefined() @@ -298,20 +298,19 @@ describe "WorkspaceView", -> expect(pane2[0]).not.toBe pane1[0] expect(editor.getPath()).toBe require.resolve('./fixtures/dir/b') - expect(atom.workspaceView.panes.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] + expect(atom.workspaceView.panes.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] editor = atom.workspaceView.openSync('file1', split: 'right') pane3 = atom.workspaceView.getActivePane() expect(pane3[0]).toBe pane2[0] expect(editor.getPath()).toBe require.resolve('./fixtures/dir/file1') - expect(atom.workspaceView.panes.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] + expect(atom.workspaceView.panes.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] describe ".openSingletonSync(filePath, options)", -> describe "when there is an active pane", -> [pane1] = [] beforeEach -> - spyOn(Pane.prototype, 'focus').andCallFake -> @makeActive() pane1 = atom.workspaceView.getActivePane() it "creates a new pane and reuses the file when already open", -> @@ -320,9 +319,9 @@ describe "WorkspaceView", -> expect(pane2[0]).not.toBe pane1[0] expect(pane1.itemForUri('b')).toBeFalsy() expect(pane2.itemForUri('b')).not.toBeFalsy() - expect(atom.workspaceView.panes.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] + expect(atom.workspaceView.panes.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] - pane1.focus() + pane1.activate() expect(atom.workspaceView.getActivePane()[0]).toBe pane1[0] atom.workspaceView.openSingletonSync('b', split: 'right') @@ -330,7 +329,7 @@ describe "WorkspaceView", -> expect(pane3[0]).toBe pane2[0] expect(pane1.itemForUri('b')).toBeFalsy() expect(pane2.itemForUri('b')).not.toBeFalsy() - expect(atom.workspaceView.panes.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] + expect(atom.workspaceView.panes.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] it "handles split: left by opening to the left pane when necessary", -> atom.workspaceView.openSingletonSync('b', split: 'right') @@ -344,15 +343,15 @@ describe "WorkspaceView", -> expect(pane1.itemForUri('file1')).toBeTruthy() expect(pane2.itemForUri('file1')).toBeFalsy() - expect(atom.workspaceView.panes.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] + expect(atom.workspaceView.panes.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] - pane2.focus() + pane2.activate() expect(atom.workspaceView.getActivePane()[0]).toBe pane2[0] atom.workspaceView.openSingletonSync('file1', split: 'left') activePane = atom.workspaceView.getActivePane() expect(activePane[0]).toBe pane1[0] - expect(atom.workspaceView.panes.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] + expect(atom.workspaceView.panes.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]] it "reuses the file when already open", -> atom.workspaceView.openSync('b') @@ -361,7 +360,7 @@ describe "WorkspaceView", -> describe ".open(filePath)", -> beforeEach -> - spyOn(Pane.prototype, 'focus') + spyOn(PaneView.prototype, 'focus') describe "when there is no active pane", -> beforeEach -> @@ -571,7 +570,7 @@ describe "WorkspaceView", -> it "saves active editor until there are none", -> editor = atom.project.openSync('../sample.txt') spyOn(editor, 'save') - atom.workspaceView.getActivePane().showItem(editor) + atom.workspaceView.getActivePane().activateItem(editor) atom.workspaceView.trigger('core:save') expect(editor.save).toHaveBeenCalled() @@ -582,6 +581,6 @@ describe "WorkspaceView", -> it "saves active editor until there are none", -> editor = atom.project.openSync('../sample.txt') spyOn(editor, 'saveAs') - atom.workspaceView.getActivePane().showItem(editor) + atom.workspaceView.getActivePane().activateItem(editor) atom.workspaceView.trigger('core:save-as') expect(editor.saveAs).toHaveBeenCalled() diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index a7904fcad..6974ad9ba 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -35,7 +35,7 @@ class DisplayBuffer extends Model @subscribe @buffer, 'markers-updated', @handleBufferMarkersUpdated @subscribe @buffer, 'marker-created', @handleBufferMarkerCreated - @subscribe @$softWrap, 'value', (softWrap) => + @subscribe @$softWrap, (softWrap) => @emit 'soft-wrap-changed', softWrap @updateWrappedScreenLines() diff --git a/src/editor-view.coffee b/src/editor-view.coffee index eb8d2cfbc..9ae326c54 100644 --- a/src/editor-view.coffee +++ b/src/editor-view.coffee @@ -827,6 +827,9 @@ class EditorView extends View @editor.setVisible(true) + @editor.on "destroyed", => + @remove() + @editor.on "contents-conflicted.editor", => @showBufferConflictAlert(@editor) @@ -1070,7 +1073,7 @@ class EditorView extends View # # Returns a {Pane}. getPane: -> - @parent('.item-views').parent('.pane').view() + @parent('.item-views').parents('.pane').view() remove: (selector, keepData) -> return super if keepData or @removed diff --git a/src/editor.coffee b/src/editor.coffee index bbefa2f69..7e72e43ba 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -73,8 +73,8 @@ class Editor extends Model @languageMode = new LanguageMode(this, @buffer.getExtension()) - @subscribe @$scrollTop, 'value', (scrollTop) => @emit 'scroll-top-changed', scrollTop - @subscribe @$scrollLeft, 'value', (scrollLeft) => @emit 'scroll-left-changed', scrollLeft + @subscribe @$scrollTop, (scrollTop) => @emit 'scroll-top-changed', scrollTop + @subscribe @$scrollLeft, (scrollLeft) => @emit 'scroll-left-changed', scrollLeft atom.project.addEditor(this) if registerEditor @@ -174,7 +174,8 @@ class Editor extends Model # Returns a {Boolean}. isEqual: (other) -> return false unless other instanceof Editor - @buffer.getPath() == other.buffer.getPath() and + @isAlive() == other.isAlive() and + @buffer.getPath() == other.buffer.getPath() and @getScrollTop() == other.getScrollTop() and @getScrollLeft() == other.getScrollLeft() and @getCursorScreenPosition().isEqual(other.getCursorScreenPosition()) @@ -313,6 +314,12 @@ class Editor extends Model # {Delegates to: TextBuffer.setText} setText: (text) -> @buffer.setText(text) + # {Delegates to: TextBuffer.getTextInRange} + getTextInRange: (range) -> @buffer.getTextInRange(range) + + # {Delegates to: TextBuffer.getLineCount} + getLineCount: -> @buffer.getLineCount() + # Private: Retrieves the current {TextBuffer}. getBuffer: -> @buffer diff --git a/src/gutter.coffee b/src/gutter.coffee index 7b7ee15a1..be02dc6d4 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -230,6 +230,8 @@ class Gutter extends View @highlightedLineNumbers.push(highlightedLineNumber) highlightLines: -> + return unless @getEditorView().editor?.isAlive() + if @getEditorView().getSelection().isEmpty() row = @getEditorView().getCursorScreenPosition().row rowRange = new Range([row, 0], [row, 0]) diff --git a/src/pane-axis-view.coffee b/src/pane-axis-view.coffee new file mode 100644 index 000000000..f3086cbb1 --- /dev/null +++ b/src/pane-axis-view.coffee @@ -0,0 +1,34 @@ +{View} = require './space-pen-extensions' +PaneView = null + +### Internal ### +module.exports = +class PaneAxisView extends View + initialize: (@model) -> + @onChildAdded(child) for child in @model.children + @subscribe @model.children, 'changed', @onChildrenChanged + + viewForModel: (model) -> + viewClass = model.getViewClass() + model._view ?= new viewClass(model) + + onChildrenChanged: ({index, removedValues, insertedValues}) => + focusedElement = document.activeElement if @hasFocus() + @onChildRemoved(child, index) for child in removedValues + @onChildAdded(child, index + i) for child, i in insertedValues + focusedElement?.focus() if document.activeElement is document.body + + onChildAdded: (child, index) => + view = @viewForModel(child) + @insertAt(index, view) + + onChildRemoved: (child) => + view = @viewForModel(child) + view.detach() + PaneView ?= require './pane-view' + + if view instanceof PaneView and view.model.isDestroyed() + @getContainer()?.trigger 'pane:removed', [view] + + getContainer: -> + @closest('.panes').view() diff --git a/src/pane-axis.coffee b/src/pane-axis.coffee index e256c58be..39657fcf7 100644 --- a/src/pane-axis.coffee +++ b/src/pane-axis.coffee @@ -1,76 +1,66 @@ +{Model, Sequence} = require 'theorist' +{flatten} = require 'underscore-plus' Serializable = require 'serializable' -{$, View} = require './space-pen-extensions' -### Internal ### +PaneRowView = null +PaneColumnView = null + module.exports = -class PaneAxis extends View +class PaneAxis extends Model + atom.deserializers.add(this) Serializable.includeInto(this) - initialize: ({children}={}) -> - @addChild(child) for child in children ? [] + constructor: ({@container, @orientation, children}) -> + @children = Sequence.fromArray(children ? []) - serializeParams: -> - children: @children().views().map (child) -> child.serialize() + @subscribe @children.onEach (child) => + child.parent = this + child.container = @container + @subscribe child, 'destroyed', => @removeChild(child) + + @subscribe @children.onRemoval (child) => @unsubscribe(child) + + @when @children.$length.becomesLessThan(2), 'reparentLastChild' + @when @children.$length.becomesLessThan(1), 'destroy' deserializeParams: (params) -> - params.children = params.children.map (childState) -> atom.deserializers.deserialize(childState) + {container} = params + params.children = params.children.map (childState) -> atom.deserializers.deserialize(childState, {container}) params - addChild: (child, index=@children().length) -> - @insertAt(index, child) - @getContainer()?.adjustPaneDimensions() + serializeParams: -> + children: @children.map (child) -> child.serialize() + orientation: @orientation + + getViewClass: -> + if @orientation is 'vertical' + PaneColumnView ?= require './pane-column-view' + else + PaneRowView ?= require './pane-row-view' + + getPanes: -> + flatten(@children.map (child) -> child.getPanes()) + + addChild: (child, index=@children.length) -> + @children.splice(index, 0, child) removeChild: (child) -> - parent = @parent().view() - container = @getContainer() - childWasInactive = not child.isActive?() + index = @children.indexOf(child) + throw new Error("Removing non-existent child") if index is -1 + @children.splice(index, 1) - primitiveRemove = (child) => - node = child[0] - $.cleanData(node.getElementsByTagName('*')) - $.cleanData([node]) - this[0].removeChild(node) + replaceChild: (oldChild, newChild) -> + index = @children.indexOf(oldChild) + throw new Error("Replacing non-existent child") if index is -1 + @children.splice(index, 1, newChild) - # use primitive .removeChild() dom method instead of .remove() to avoid recursive loop - if @children().length == 2 - primitiveRemove(child) - sibling = @children().view() - siblingFocused = sibling.is(':has(:focus)') - sibling.detach() + insertChildBefore: (currentChild, newChild) -> + index = @children.indexOf(currentChild) + @children.splice(index, 0, newChild) - if parent.setRoot? - parent.setRoot(sibling, suppressPaneItemChangeEvents: childWasInactive) - else - parent.insertChildBefore(this, sibling) - parent.removeChild(this) - sibling.focus() if siblingFocused - else - primitiveRemove(child) + insertChildAfter: (currentChild, newChild) -> + index = @children.indexOf(currentChild) + @children.splice(index + 1, 0, newChild) - container.adjustPaneDimensions() - Pane = require './pane' - container.trigger 'pane:removed', [child] if child instanceof Pane - - detachChild: (child) -> - child.detach() - - getContainer: -> - @closest('.panes').view() - - getActivePaneItem: -> - @getActivePane()?.activeItem - - getActivePane: -> - @find('.pane.active').view() ? @find('.pane:first').view() - - insertChildBefore: (child, newChild) -> - newChild.insertBefore(child) - - insertChildAfter: (child, newChild) -> - newChild.insertAfter(child) - - horizontalChildUnits: -> - $(child).view().horizontalGridUnits() for child in @children() - - verticalChildUnits: -> - $(child).view().verticalGridUnits() for child in @children() + reparentLastChild: -> + @parent.replaceChild(this, @children[0]) diff --git a/src/pane-column-view.coffee b/src/pane-column-view.coffee new file mode 100644 index 000000000..ac9d1df6e --- /dev/null +++ b/src/pane-column-view.coffee @@ -0,0 +1,13 @@ +{$} = require './space-pen-extensions' +_ = require 'underscore-plus' +PaneAxisView = require './pane-axis-view' + +# Internal: +module.exports = +class PaneColumnView extends PaneAxisView + + @content: -> + @div class: 'pane-column' + + className: -> + "PaneColumn" diff --git a/src/pane-column.coffee b/src/pane-column.coffee deleted file mode 100644 index 522e54ca7..000000000 --- a/src/pane-column.coffee +++ /dev/null @@ -1,34 +0,0 @@ -{$} = require './space-pen-extensions' -_ = require 'underscore-plus' -PaneAxis = require './pane-axis' - -# Internal: -module.exports = -class PaneColumn extends PaneAxis - - @content: -> - @div class: 'column' - - className: -> - "PaneColumn" - - adjustDimensions: -> - totalUnits = @verticalGridUnits() - unitsSoFar = 0 - for child in @children() - child = $(child).view() - childUnits = child.verticalGridUnits() - child.css - width: '100%' - height: "#{childUnits / totalUnits * 100}%" - top: "#{unitsSoFar / totalUnits * 100}%" - left: 0 - - child.adjustDimensions() - unitsSoFar += childUnits - - horizontalGridUnits: -> - Math.max(@horizontalChildUnits()...) - - verticalGridUnits: -> - _.sum(@verticalChildUnits()) diff --git a/src/pane-container-view.coffee b/src/pane-container-view.coffee new file mode 100644 index 000000000..a55a1d65b --- /dev/null +++ b/src/pane-container-view.coffee @@ -0,0 +1,132 @@ +Serializable = require 'serializable' +{$, View} = require './space-pen-extensions' +PaneView = require './pane-view' +PaneContainer = require './pane-container' + +# Private: Manages the list of panes within a {WorkspaceView} +module.exports = +class PaneContainerView extends View + atom.deserializers.add(this) + Serializable.includeInto(this) + + @deserialize: (state) -> + new this(PaneContainer.deserialize(state.model)) + + @content: -> + @div class: 'panes' + + initialize: (params) -> + if params instanceof PaneContainer + @model = params + else + @model = new PaneContainer({root: params?.root?.model}) + + @subscribe @model.$root, @onRootChanged + @subscribe @model.$activePaneItem.changes, @onActivePaneItemChanged + + viewForModel: (model) -> + if model? + viewClass = model.getViewClass() + model._view ?= new viewClass(model) + + serializeParams: -> + model: @model.serialize() + + ### Public ### + + itemDestroyed: (item) -> + @trigger 'item-destroyed', [item] + + getRoot: -> + @children().first().view() + + setRoot: (root) -> + @model.root = root?.model + + 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 = @viewForModel(root) + @append(view) + focusedElement?.focus() + else + atom.workspaceView?.focus() if focusedElement? + + 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 + + saveAll: -> + pane.saveItems() for pane in @getPanes() + + confirmClose: -> + saved = true + for pane in @getPanes() + for item in pane.getItems() + if not pane.promptToSaveItem(item) + saved = false + break + saved + + getPanes: -> + @find('.pane').views() + + indexOfPane: (pane) -> + @getPanes().indexOf(pane.view()) + + paneAtIndex: (index) -> + @getPanes()[index] + + eachPane: (callback) -> + callback(pane) for pane in @getPanes() + paneAttached = (e) -> callback($(e.target).view()) + @on 'pane:attached', paneAttached + off: => @off 'pane:attached', paneAttached + + getFocusedPane: -> + @find('.pane:has(:focus)').view() + + getActivePane: -> + @viewForModel(@model.activePane) + + getActivePaneItem: -> + @model.activePaneItem + + getActiveView: -> + @getActivePane()?.activeView + + paneForUri: (uri) -> + for pane in @getPanes() + view = pane.itemForUri(uri) + return pane if view? + null + + focusNextPane: -> + panes = @getPanes() + if panes.length > 1 + currentIndex = panes.indexOf(@getFocusedPane()) + nextIndex = (currentIndex + 1) % panes.length + panes[nextIndex].focus() + true + else + false + + focusPreviousPane: -> + panes = @getPanes() + if panes.length > 1 + currentIndex = panes.indexOf(@getFocusedPane()) + previousIndex = currentIndex - 1 + previousIndex = panes.length - 1 if previousIndex < 0 + panes[previousIndex].focus() + true + else + false diff --git a/src/pane-container.coffee b/src/pane-container.coffee index 7f9bd44f7..14803afc8 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -1,140 +1,66 @@ +{Model} = require 'theorist' Serializable = require 'serializable' -{$, View} = require './space-pen-extensions' Pane = require './pane' -# Private: Manages the list of panes within a {WorkspaceView} module.exports = -class PaneContainer extends View - Serializable.includeInto(this) +class PaneContainer extends Model atom.deserializers.add(this) + Serializable.includeInto(this) - @content: -> - @div class: 'panes' + @properties + root: null + activePane: null - initialize: ({root}={}) -> - @setRoot(root) + previousRoot: null - @subscribe this, 'pane:attached', (event, pane) => - @triggerActiveItemChange() if @getActivePane() is pane + @behavior 'activePaneItem', -> + @$activePane.switch (activePane) -> activePane?.$activeItem - @subscribe this, 'pane:removed', (event, pane) => - @triggerActiveItemChange() unless @getActivePane()? - - @subscribe this, 'pane:became-active', => - @triggerActiveItemChange() - - @subscribe this, 'pane:active-item-changed', (event, item) => - @triggerActiveItemChange() if @getActivePaneItem() is item - - triggerActiveItemChange: -> - @trigger 'pane-container:active-pane-item-changed', [@getActivePaneItem()] - - serializeParams: -> - root: @getRoot()?.serialize() + constructor: (params) -> + super + @subscribe @$root, @onRootChanged + @destroyEmptyPanes() if params?.destroyEmptyPanes deserializeParams: (params) -> - params.root = atom.deserializers.deserialize(params.root) + params.root = atom.deserializers.deserialize(params.root, container: this) + params.destroyEmptyPanes = true params - ### Public ### + serializeParams: (params) -> + root: @root?.serialize() - focusNextPane: -> - panes = @getPanes() - if panes.length > 1 - currentIndex = panes.indexOf(@getFocusedPane()) - nextIndex = (currentIndex + 1) % panes.length - panes[nextIndex].focus() - true - else - false - - focusPreviousPane: -> - panes = @getPanes() - if panes.length > 1 - currentIndex = panes.indexOf(@getFocusedPane()) - previousIndex = currentIndex - 1 - previousIndex = panes.length - 1 if previousIndex < 0 - panes[previousIndex].focus() - true - else - false - - makeNextPaneActive: -> - panes = @getPanes() - currentIndex = panes.indexOf(@getActivePane()) - nextIndex = (currentIndex + 1) % panes.length - panes[nextIndex].makeActive() - - itemDestroyed: (item) -> - @trigger 'item-destroyed', [item] - - getRoot: -> - @children().first().view() - - setRoot: (root, {suppressPaneItemChangeEvents}={}) -> - @empty() - if root? - @append(root) - root.makeActive?() - - removeChild: (child) -> - throw new Error("Removing non-existant child") unless @getRoot() is child - @setRoot(null) - @trigger 'pane:removed', [child] if child instanceof Pane - - saveAll: -> - pane.saveItems() for pane in @getPanes() - - confirmClose: -> - saved = true - for pane in @getPanes() - for item in pane.getItems() - if not pane.promptToSaveItem(item) - saved = false - break - saved + replaceChild: (oldChild, newChild) -> + throw new Error("Replacing non-existent child") if oldChild isnt @root + @root = newChild getPanes: -> - @find('.pane').views() + @root?.getPanes() ? [] - indexOfPane: (pane) -> - @getPanes().indexOf(pane.view()) + activateNextPane: -> + panes = @getPanes() + if panes.length > 1 + currentIndex = panes.indexOf(@activePane) + nextIndex = (currentIndex + 1) % panes.length + panes[nextIndex].activate() + else + @activePane = null - paneAtIndex: (index) -> - @getPanes()[index] + onRootChanged: (root) => + @unsubscribe(@previousRoot) if @previousRoot? + @previousRoot = root - eachPane: (callback) -> - callback(pane) for pane in @getPanes() - paneAttached = (e) -> callback($(e.target).view()) - @on 'pane:attached', paneAttached - off: => @off 'pane:attached', paneAttached + unless root? + @activePane = null + return - getFocusedPane: -> - @find('.pane:has(:focus)').view() + root.parent = this + root.container = this - getActivePane: -> - @find('.pane.active').view() ? @find('.pane:first').view() + if root instanceof Pane + @activePane ?= root + @subscribe root, 'destroyed', => + @activePane = null + @root = null - getActivePaneItem: -> - @getActivePane()?.activeItem - - getActiveView: -> - @getActivePane()?.activeView - - paneForUri: (uri) -> - for pane in @getPanes() - view = pane.itemForUri(uri) - return pane if view? - null - - adjustPaneDimensions: -> - if root = @getRoot() - root.css(width: '100%', height: '100%', top: 0, left: 0) - root.adjustDimensions() - - removeEmptyPanes: -> - for pane in @getPanes() when pane.getItems().length == 0 - pane.remove() - - afterAttach: -> - @adjustPaneDimensions() + destroyEmptyPanes: -> + pane.destroy() for pane in @getPanes() when pane.items.length is 0 diff --git a/src/pane-model.coffee b/src/pane-model.coffee new file mode 100644 index 000000000..a6902968a --- /dev/null +++ b/src/pane-model.coffee @@ -0,0 +1,307 @@ +{find, compact, extend} = require 'underscore-plus' +{dirname} = require 'path' +{Model, Sequence} = require 'theorist' +Serializable = require 'serializable' +PaneAxis = require './pane-axis' +PaneView = null + +# Public: A container for multiple items, one of which is *active* at a given +# time. With the default packages, a tab is displayed for each item and the +# active item's view is displayed. +module.exports = +class PaneModel extends Model + atom.deserializers.add(this) + Serializable.includeInto(this) + + @properties + container: null + activeItem: null + focused: false + + # Public: Only one pane is considered *active* at a time. A pane is activated + # when it is focused, and when focus returns to the pane container after + # moving to another element such as a panel, it returns to the active pane. + @behavior 'active', -> + @$container + .switch((container) -> container?.$activePane) + .map((activePane) => activePane is this) + .distinctUntilChanged() + + # Private: + constructor: (params) -> + super + + @items = Sequence.fromArray(params?.items ? []) + @activeItem ?= @items[0] + + @subscribe @items.onEach (item) => + if typeof item.on is 'function' + @subscribe item, 'destroyed', => @removeItem(item) + + @subscribe @items.onRemoval (item, index) => + @unsubscribe item if typeof item.on is 'function' + + @activate() if params?.active + + # Private: Called by the Serializable mixin during serialization. + serializeParams: -> + items: compact(@items.map((item) -> item.serialize?())) + activeItemUri: @activeItem?.getUri?() + focused: @focused + active: @active + + # Private: Called by the Serializable mixin during deserialization. + deserializeParams: (params) -> + {items, activeItemUri} = params + params.items = compact(items.map (itemState) -> atom.deserializers.deserialize(itemState)) + params.activeItem = find params.items, (item) -> item.getUri?() is activeItemUri + params + + # Private: Called by the view layer to construct a view for this model. + getViewClass: -> PaneView ?= require './pane-view' + + isActive: -> @active + + # Private: Called by the view layer to indicate that the pane has gained focus. + focus: -> + @focused = true + @activate() unless @isActive() + + # Private: Called by the view layer to indicate that the pane has lost focus. + blur: -> + @focused = false + true # if this is called from an event handler, don't cancel it + + # Public: Makes this pane the *active* pane, causing it to gain focus + # immediately. + activate: -> + @container?.activePane = this + @emit 'activated' + + # Private: + getPanes: -> [this] + + # Public: + getItems: -> + @items.slice() + + # Public: Returns the item at the specified index. + itemAtIndex: (index) -> + @items[index] + + # Public: Makes the next item active. + activateNextItem: -> + index = @getActiveItemIndex() + if index < @items.length - 1 + @activateItemAtIndex(index + 1) + else + @activateItemAtIndex(0) + + # Public: Makes the previous item active. + activatePreviousItem: -> + index = @getActiveItemIndex() + if index > 0 + @activateItemAtIndex(index - 1) + else + @activateItemAtIndex(@items.length - 1) + + # Public: Returns the index of the current active item. + getActiveItemIndex: -> + @items.indexOf(@activeItem) + + # Public: Makes the item at the given index active. + activateItemAtIndex: (index) -> + @activateItem(@itemAtIndex(index)) + + # Public: Makes the given item active, adding the item if necessary. + activateItem: (item) -> + if item? + @addItem(item) + @activeItem = item + + # Public: Adds the item to the pane. + # + # * item: + # The item to add. It can be a model with an associated view or a view. + # * index: + # An optional index at which to add the item. If omitted, the item is + # added to the end. + # + # Returns the added item + addItem: (item, index=@getActiveItemIndex() + 1) -> + return if item in @items + + @items.splice(index, 0, item) + @emit 'item-added', item, index + item + + # Private: + removeItem: (item, destroying) -> + index = @items.indexOf(item) + return if index is -1 + @activateNextItem() if item is @activeItem and @items.length > 1 + @items.splice(index, 1) + @emit 'item-removed', item, index, destroying + @destroy() if @items.length is 0 + + # Public: Moves the given item to the specified index. + moveItem: (item, newIndex) -> + oldIndex = @items.indexOf(item) + @items.splice(oldIndex, 1) + @items.splice(newIndex, 0, item) + @emit 'item-moved', item, newIndex + + # Public: Moves the given item to the given index at another pane. + moveItemToPane: (item, pane, index) -> + pane.addItem(item, index) + @removeItem(item) + + # Public: Destroys the currently active item and make the next item active. + destroyActiveItem: -> + @destroyItem(@activeItem) + false + + # Public: Destroys the given item. If it is the active item, activate the next + # one. If this is the last item, also destroys the pane. + destroyItem: (item) -> + @emit 'before-item-destroyed', item + if @promptToSaveItem(item) + @emit 'item-destroyed', item + @removeItem(item, true) + item.destroy?() + true + else + false + + # Public: Destroys all items and destroys the pane. + destroyItems: -> + @destroyItem(item) for item in @getItems() + + # Public: Destroys all items but the active one. + destroyInactiveItems: -> + @destroyItem(item) for item in @getItems() when item isnt @activeItem + + # Private: Called by model superclass. + destroyed: -> + @container.activateNextPane() if @isActive() + item.destroy?() for item in @items.slice() + + # Public: Prompts the user to save the given item if it can be saved and is + # currently unsaved. + promptToSaveItem: (item) -> + return true unless 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." + buttons: ["Save", "Cancel", "Don't Save"] + + switch chosen + when 0 then @saveItem(item, -> true) + when 1 then false + when 2 then true + + # Public: Saves the active item. + saveActiveItem: -> + @saveItem(@activeItem) + + # Public: Saves the active item at a prompted-for location. + saveActiveItemAs: -> + @saveItemAs(@activeItem) + + # Public: Saves the specified item. + # + # * item: The item to save. + # * nextAction: An optional function which will be called after the item is saved. + saveItem: (item, nextAction) -> + if item.getUri?() + item.save?() + nextAction?() + else + @saveItemAs(item, nextAction) + + # Public: Saves the given item at a prompted-for location. + # + # * item: The item to save. + # * nextAction: An optional function which will be called after the item is saved. + saveItemAs: (item, nextAction) -> + return unless item.saveAs? + + itemPath = item.getPath?() + itemPath = dirname(itemPath) if itemPath + path = atom.showSaveDialogSync(itemPath) + if path + item.saveAs(path) + nextAction?() + + # Public: Saves all items. + saveItems: -> + @saveItem(item) for item in @getItems() + + # Public: Returns the first item that matches the given URI or undefined if + # none exists. + itemForUri: (uri) -> + find @items, (item) -> item.getUri?() is uri + + # Public: Activates the first item that matches the given URI. Returns a + # boolean indicating whether a matching item was found. + activateItemForUri: (uri) -> + if item = @itemForUri(uri) + @activateItem(item) + true + else + false + + # Private: + copyActiveItem: -> + @activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize()) + + # Public: Creates a new pane to the left of the receiver. + # + # * params: + # + items: An optional array of items with which to construct the new pane. + # + # Returns the new {PaneModel}. + splitLeft: (params) -> + @split('horizontal', 'before', params) + + # Public: Creates a new pane to the right of the receiver. + # + # * params: + # + items: An optional array of items with which to construct the new pane. + # + # Returns the new {PaneModel}. + splitRight: (params) -> + @split('horizontal', 'after', params) + + # Public: Creates a new pane above the receiver. + # + # * params: + # + items: An optional array of items with which to construct the new pane. + # + # Returns the new {PaneModel}. + splitUp: (params) -> + @split('vertical', 'before', params) + + # Public: Creates a new pane below the receiver. + # + # * params: + # + items: An optional array of items with which to construct the new pane. + # + # Returns the new {PaneModel}. + splitDown: (params) -> + @split('vertical', 'after', params) + + # Private: + split: (orientation, side, params) -> + if @parent.orientation isnt orientation + @parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this]})) + + newPane = new @constructor(extend({focused: true}, params)) + switch side + when 'before' then @parent.insertChildBefore(this, newPane) + when 'after' then @parent.insertChildAfter(this, newPane) + + newPane.activate() + newPane diff --git a/src/pane-row-view.coffee b/src/pane-row-view.coffee new file mode 100644 index 000000000..8808ce36c --- /dev/null +++ b/src/pane-row-view.coffee @@ -0,0 +1,13 @@ +{$} = require './space-pen-extensions' +_ = require 'underscore-plus' +PaneAxisView = require './pane-axis-view' + +### Internal ### + +module.exports = +class PaneRowView extends PaneAxisView + @content: -> + @div class: 'pane-row' + + className: -> + "PaneRow" diff --git a/src/pane-row.coffee b/src/pane-row.coffee deleted file mode 100644 index 2c9b7774a..000000000 --- a/src/pane-row.coffee +++ /dev/null @@ -1,34 +0,0 @@ -{$} = require './space-pen-extensions' -_ = require 'underscore-plus' -PaneAxis = require './pane-axis' - -### Internal ### - -module.exports = -class PaneRow extends PaneAxis - @content: -> - @div class: 'row' - - className: -> - "PaneRow" - - adjustDimensions: -> - totalUnits = @horizontalGridUnits() - unitsSoFar = 0 - for child in @children() - child = $(child).view() - childUnits = child.horizontalGridUnits() - child.css - width: "#{childUnits / totalUnits * 100}%" - height: '100%' - top: 0 - left: "#{unitsSoFar / totalUnits * 100}%" - - child.adjustDimensions() - unitsSoFar += childUnits - - horizontalGridUnits: -> - _.sum(@horizontalChildUnits()) - - verticalGridUnits: -> - Math.max(@verticalChildUnits()...) diff --git a/src/pane-view.coffee b/src/pane-view.coffee new file mode 100644 index 000000000..fe9da3f31 --- /dev/null +++ b/src/pane-view.coffee @@ -0,0 +1,226 @@ +{$, View} = require './space-pen-extensions' +Serializable = require 'serializable' +Delegator = require 'delegato' + +Pane = require './pane' + +# Public: A container which can contains multiple items to be switched between. +# +# Items can be almost anything however most commonly they're {EditorView}s. +# +# Most packages won't need to use this class, unless you're interested in +# building a package that deals with switching between panes or tiems. +module.exports = +class PaneView extends View + Serializable.includeInto(this) + Delegator.includeInto(this) + + @version: 1 + + @deserialize: (state) -> + new this(Pane.deserialize(state.model)) + + @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', + 'destroyItem', 'destroyItems', 'destroyActiveItem', 'destroyInactiveItems', + 'saveActiveItem', 'saveActiveItemAs', 'saveItem', 'saveItemAs', 'saveItems', + 'itemForUri', 'activateItemForUri', 'promptToSaveItem', 'copyActiveItem', 'isActive', + 'activate', toProperty: 'model' + + previousActiveItem: null + + # Private: + initialize: (args...) -> + if args[0] instanceof Pane + @model = args[0] + else + @model = new Pane(items: args) + @model._view = this + + @onItemAdded(item) for item in @items + @viewsByItem = new WeakMap() + @handleEvents() + + handleEvents: -> + @subscribe @model, 'destroyed', => @remove() + + @subscribe @model.$activeItem, @onActiveItemChanged + @subscribe @model, 'item-added', @onItemAdded + @subscribe @model, 'item-removed', @onItemRemoved + @subscribe @model, 'item-moved', @onItemMoved + @subscribe @model, 'before-item-destroyed', @onBeforeItemDestroyed + @subscribe @model, 'item-destroyed', @onItemDestroyed + @subscribe @model, 'activated', @onActivated + @subscribe @model.$active, @onActiveStatusChanged + + @subscribe this, 'focusin', => @model.focus() + @subscribe this, 'focusout', => @model.blur() + @subscribe this, 'focus', => + @activeView?.focus() + false + + @command 'pane:save-items', => @saveItems() + @command 'pane:show-next-item', => @activateNextItem() + @command 'pane:show-previous-item', => @activatePreviousItem() + + @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', => @splitLeft(@copyActiveItem()) + @command 'pane:split-right', => @splitRight(@copyActiveItem()) + @command 'pane:split-up', => @splitUp(@copyActiveItem()) + @command 'pane:split-down', => @splitDown(@copyActiveItem()) + @command 'pane:close', => @destroyItems() + @command 'pane:close-other-items', => @destroyInactiveItems() + + deserializeParams: (params) -> + params.model = Pane.deserialize(params.model) + params + + serializeParams: -> + model: @model.serialize() + + # Deprecated: Use ::destroyItem + removeItem: (item) -> @destroyItem(item) + + # Deprecated: Use ::activateItem + showItem: (item) -> @activateItem(item) + + # Deprecated: Use ::activateItemForUri + showItemForUri: (item) -> @activateItemForUri(item) + + # Deprecated: Use ::activateItemAtIndex + showItemAtIndex: (index) -> @activateItemAtIndex(index) + + # Deprecated: Use ::activateNextItem + showNextItem: -> @activateNextItem() + + # Deprecated: Use ::activatePreviousItem + showPreviousItem: -> @activatePreviousItem() + + # Private: + afterAttach: (onDom) -> + @focus() if @model.focused and onDom + + return if @attached + @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. + getNextPane: -> + panes = @getContainer()?.getPanes() + return unless panes.length > 1 + nextIndex = (panes.indexOf(this) + 1) % panes.length + panes[nextIndex] + + getActivePaneItem: -> + @activeItem + + onActiveItemChanged: (item) => + @previousActiveItem?.off? 'title-changed', @activeItemTitleChanged + @previousActiveItem = item + + return unless item? + + hasFocus = @hasFocus() + item.on? 'title-changed', @activeItemTitleChanged + view = @viewForItem(item) + @itemViews.children().not(view).hide() + @itemViews.append(view) unless view.parent().is(@itemViews) + view.show() if @attached + view.focus() if hasFocus + + @activeView = view + @trigger 'pane:active-item-changed', [item] + + onItemAdded: (item, index) => + @trigger 'pane:item-added', [item, index] + + onItemRemoved: (item, index, destroyed) => + if item instanceof $ + viewToRemove = item + else if viewToRemove = @viewsByItem.get(item) + @viewsByItem.delete(item) + + if viewToRemove? + viewToRemove.setModel?(null) + if destroyed + viewToRemove.remove() + else + viewToRemove.detach() + + @trigger 'pane:item-removed', [item, index] + + onItemMoved: (item, newIndex) => + @trigger 'pane:item-moved', [item, newIndex] + + onBeforeItemDestroyed: (item) => + @unsubscribe(item) if typeof item.off is 'function' + @trigger 'pane:before-item-destroyed', [item] + + onItemDestroyed: (item) => + @getContainer()?.itemDestroyed(item) + + # Private: + activeItemTitleChanged: => + @trigger 'pane:active-item-title-changed' + + # Private: + viewForItem: (item) -> + if item instanceof $ + item + else if view = @viewsByItem.get(item) + view + else + viewClass = item.getViewClass() + view = new viewClass(item) + @viewsByItem.set(item, view) + view + + # Private: + viewForActiveItem: -> + @viewForItem(@activeItem) + + splitLeft: (items...) -> @model.splitLeft({items})._view + + splitRight: (items...) -> @model.splitRight({items})._view + + splitUp: (items...) -> @model.splitUp({items})._view + + splitDown: (items...) -> @model.splitDown({items})._view + + # Private: + getContainer: -> + @closest('.panes').view() + + beforeRemove: -> + @model.destroy() unless @model.isDestroyed() + + # Private: + remove: (selector, keepData) -> + return super if keepData + @unsubscribe() + super diff --git a/src/pane.coffee b/src/pane.coffee index d976a95f0..746cfb989 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -1,212 +1,193 @@ +{find, compact, extend} = require 'underscore-plus' {dirname} = require 'path' -{$, View} = require './space-pen-extensions' -_ = require 'underscore-plus' +{Model, Sequence} = require 'theorist' Serializable = require 'serializable' +PaneAxis = require './pane-axis' +PaneView = null -PaneRow = require './pane-row' -PaneColumn = require './pane-column' - -# Public: A container which can contains multiple items to be switched between. -# -# Items can be almost anything however most commonly they're {EditorView}s. -# -# Most packages won't need to use this class, unless you're interested in -# building a package that deals with switching between panes or tiems. +# Public: A container for multiple items, one of which is *active* at a given +# time. With the default packages, a tab is displayed for each item and the +# active item's view is displayed. module.exports = -class Pane extends View +class Pane extends Model + atom.deserializers.add(this) Serializable.includeInto(this) - @version: 1 + @properties + container: null + activeItem: null + focused: false - @content: (wrappedView) -> - @div class: 'pane', tabindex: -1, => - @div class: 'item-views', outlet: 'itemViews' - - activeItem: null - items: null - viewsByItem: null # Views without a setModel() method are stored here + # Public: Only one pane is considered *active* at a time. A pane is activated + # when it is focused, and when focus returns to the pane container after + # moving to another element such as a panel, it returns to the active pane. + @behavior 'active', -> + @$container + .switch((container) -> container?.$activePane) + .map((activePane) => activePane is this) + .distinctUntilChanged() # Private: - initialize: (args...) -> - if args[0]?.items # deserializing - {@items, activeItemUri, @focusOnAttach} = args[0] - else - @items = args + constructor: (params) -> + super - @items ?= [] + @items = Sequence.fromArray(params?.items ? []) + @activeItem ?= @items[0] - @handleItemEvents(item) for item in @items + @subscribe @items.onEach (item) => + if typeof item.on is 'function' + @subscribe item, 'destroyed', => @removeItem(item) - @viewsByItem = new WeakMap() + @subscribe @items.onRemoval (item, index) => + @unsubscribe item if typeof item.on is 'function' - unless activeItemUri? and @showItemForUri(activeItemUri) - @showItem(@items[0]) if @items.length > 0 + @activate() if params?.active - @command 'pane:save-items', @saveItems - @command 'pane:show-next-item', @showNextItem - @command 'pane:show-previous-item', @showPreviousItem - - @command 'pane:show-item-1', => @showItemAtIndex(0) - @command 'pane:show-item-2', => @showItemAtIndex(1) - @command 'pane:show-item-3', => @showItemAtIndex(2) - @command 'pane:show-item-4', => @showItemAtIndex(3) - @command 'pane:show-item-5', => @showItemAtIndex(4) - @command 'pane:show-item-6', => @showItemAtIndex(5) - @command 'pane:show-item-7', => @showItemAtIndex(6) - @command 'pane:show-item-8', => @showItemAtIndex(7) - @command 'pane:show-item-9', => @showItemAtIndex(8) - - @command 'pane:split-left', => @splitLeft(@copyActiveItem()) - @command 'pane:split-right', => @splitRight(@copyActiveItem()) - @command 'pane:split-up', => @splitUp(@copyActiveItem()) - @command 'pane:split-down', => @splitDown(@copyActiveItem()) - @command 'pane:close', => @destroyItems() - @command 'pane:close-other-items', => @destroyInactiveItems() - @on 'focus', => @activeView?.focus(); false - @on 'focusin', => @makeActive() + # Private: Called by the Serializable mixin during serialization. + serializeParams: -> + items: compact(@items.map((item) -> item.serialize?())) + activeItemUri: @activeItem?.getUri?() + focused: @focused + active: @active + # Private: Called by the Serializable mixin during deserialization. deserializeParams: (params) -> - params.items = _.compact(params.items.map (itemState) -> atom.deserializers.deserialize(itemState)) + {items, activeItemUri} = params + params.items = compact(items.map (itemState) -> atom.deserializers.deserialize(itemState)) + params.activeItem = find params.items, (item) -> item.getUri?() is activeItemUri params - serializeParams: -> - items: _.compact(@items.map (item) -> item.serialize?()) - focusOnAttach: @is(':has(:focus)') - activeItemUri: @getActivePaneItem()?.getUri?() + # Private: Called by the view layer to construct a view for this model. + getViewClass: -> PaneView ?= require './pane-view' + + isActive: -> @active + + # Private: Called by the view layer to indicate that the pane has gained focus. + focus: -> + @focused = true + @activate() unless @isActive() + + # Private: Called by the view layer to indicate that the pane has lost focus. + blur: -> + @focused = false + true # if this is called from an event handler, don't cancel it + + # Public: Makes this pane the *active* pane, causing it to gain focus + # immediately. + activate: -> + @container?.activePane = this + @emit 'activated' # Private: - afterAttach: (onDom) -> - if @focusOnAttach and onDom - @focusOnAttach = null - @focus() + getPanes: -> [this] - return if @attached - @attached = true - @trigger 'pane:attached', [this] - - # Public: Focus this pane. - makeActive: -> - wasActive = @isActive() - for pane in @getContainer().getPanes() when pane isnt this - pane.makeInactive() - @addClass('active') - @trigger 'pane:became-active' unless wasActive - - # Public: Unfocus this pane. - makeInactive: -> - wasActive = @isActive() - @removeClass('active') - @trigger 'pane:became-inactive' if wasActive - - # Public: Returns whether this pane is currently focused. - isActive: -> - @getContainer()?.getActivePane() == this - - # Public: Returns the next pane, ordered by creation. - getNextPane: -> - panes = @getContainer()?.getPanes() - return unless panes.length > 1 - nextIndex = (panes.indexOf(this) + 1) % panes.length - panes[nextIndex] - - # Public: Returns all contained views. + # Public: getItems: -> - new Array(@items...) - - # Public: Switches to the next contained item. - showNextItem: => - index = @getActiveItemIndex() - if index < @items.length - 1 - @showItemAtIndex(index + 1) - else - @showItemAtIndex(0) - - # Public: Switches to the previous contained item. - showPreviousItem: => - index = @getActiveItemIndex() - if index > 0 - @showItemAtIndex(index - 1) - else - @showItemAtIndex(@items.length - 1) - - getActivePaneItem: -> - @activeItem - - # Public: Returns the index of the currently active item. - getActiveItemIndex: -> - @items.indexOf(@activeItem) - - # Public: Switch to the item associated with the given index. - showItemAtIndex: (index) -> - @showItem(@itemAtIndex(index)) + @items.slice() # Public: Returns the item at the specified index. itemAtIndex: (index) -> @items[index] - # Public: Focuses the given item. - showItem: (item) -> - return if !item? or item is @activeItem + # Public: Makes the next item active. + activateNextItem: -> + index = @getActiveItemIndex() + if index < @items.length - 1 + @activateItemAtIndex(index + 1) + else + @activateItemAtIndex(0) - if @activeItem - @activeItem.off? 'title-changed', @activeItemTitleChanged + # Public: Makes the previous item active. + activatePreviousItem: -> + index = @getActiveItemIndex() + if index > 0 + @activateItemAtIndex(index - 1) + else + @activateItemAtIndex(@items.length - 1) - isFocused = @is(':has(:focus)') - @addItem(item) - item.on? 'title-changed', @activeItemTitleChanged - view = @viewForItem(item) - @itemViews.children().not(view).hide() - @itemViews.append(view) unless view.parent().is(@itemViews) - view.show() if @attached - view.focus() if isFocused - @activeItem = item - @activeView = view - @trigger 'pane:active-item-changed', [item] + # Public: Returns the index of the current active item. + getActiveItemIndex: -> + @items.indexOf(@activeItem) - # Private: - activeItemTitleChanged: => - @trigger 'pane:active-item-title-changed' + # Public: Makes the item at the given index active. + activateItemAtIndex: (index) -> + @activateItem(@itemAtIndex(index)) - # Public: Add an additional item at the specified index. + # Public: Makes the given item active, adding the item if necessary. + activateItem: (item) -> + if item? + @addItem(item) + @activeItem = item + + # Public: Adds the item to the pane. + # + # * item: + # The item to add. It can be a model with an associated view or a view. + # * index: + # An optional index at which to add the item. If omitted, the item is + # added to the end. + # + # Returns the added item addItem: (item, index=@getActiveItemIndex() + 1) -> - return if _.include(@items, item) + return if item in @items @items.splice(index, 0, item) - @trigger 'pane:item-added', [item, index] - @handleItemEvents(item) + @emit 'item-added', item, index item - handleItemEvents: (item) -> - if _.isFunction(item.on) - @subscribe item, 'destroyed', => @destroyItem(item) + # Private: + removeItem: (item, destroying) -> + index = @items.indexOf(item) + return if index is -1 + @activateNextItem() if item is @activeItem and @items.length > 1 + @items.splice(index, 1) + @emit 'item-removed', item, index, destroying + @destroy() if @items.length is 0 - # Public: Remove the currently active item. - destroyActiveItem: => + # Public: Moves the given item to the specified index. + moveItem: (item, newIndex) -> + oldIndex = @items.indexOf(item) + @items.splice(oldIndex, 1) + @items.splice(newIndex, 0, item) + @emit 'item-moved', item, newIndex + + # Public: Moves the given item to the given index at another pane. + moveItemToPane: (item, pane, index) -> + pane.addItem(item, index) + @removeItem(item) + + # Public: Destroys the currently active item and make the next item active. + destroyActiveItem: -> @destroyItem(@activeItem) false - # Public: Remove the specified item. - destroyItem: (item, options) -> - @unsubscribe(item) if _.isFunction(item.off) - @trigger 'pane:before-item-destroyed', [item] - + # Public: Destroys the given item. If it is the active item, activate the next + # one. If this is the last item, also destroys the pane. + destroyItem: (item) -> + @emit 'before-item-destroyed', item if @promptToSaveItem(item) - @getContainer()?.itemDestroyed(item) - @removeItem(item, options) + @emit 'item-destroyed', item + @removeItem(item, true) item.destroy?() true else false - # Public: Remove and delete all items. + # Public: Destroys all items and destroys the pane. destroyItems: -> @destroyItem(item) for item in @getItems() - # Public: Remove and delete all but the currently focused item. + # Public: Destroys all items but the active one. destroyInactiveItems: -> @destroyItem(item) for item in @getItems() when item isnt @activeItem - # Public: Prompt the user to save the given item. + # Private: Called by model superclass. + destroyed: -> + @container.activateNextPane() if @isActive() + item.destroy?() for item in @items.slice() + + # Public: Prompts the user to save the given item if it can be saved and is + # currently unsaved. promptToSaveItem: (item) -> return true unless item.shouldPromptToSave?() @@ -221,15 +202,18 @@ class Pane extends View when 1 then false when 2 then true - # Public: Saves the currently focused item. - saveActiveItem: => + # Public: Saves the active item. + saveActiveItem: -> @saveItem(@activeItem) - # Public: Save and prompt for path for the currently focused item. - saveActiveItemAs: => + # Public: Saves the active item at a prompted-for location. + saveActiveItemAs: -> @saveItemAs(@activeItem) - # Public: Saves the specified item and call the next action when complete. + # Public: Saves the specified item. + # + # * item: The item to save. + # * nextAction: An optional function which will be called after the item is saved. saveItem: (item, nextAction) -> if item.getUri?() item.save?() @@ -237,8 +221,10 @@ class Pane extends View else @saveItemAs(item, nextAction) - # Public: Prompts for path and then saves the specified item. Upon completion - # it also calls the next action. + # Public: Saves the given item at a prompted-for location. + # + # * item: The item to save. + # * nextAction: An optional function which will be called after the item is saved. saveItemAs: (item, nextAction) -> return unless item.saveAs? @@ -249,164 +235,73 @@ class Pane extends View item.saveAs(path) nextAction?() - # Public: Saves all items in this pane. - saveItems: => + # Public: Saves all items. + saveItems: -> @saveItem(item) for item in @getItems() - # Public: - removeItem: (item) -> - index = @items.indexOf(item) - @removeItemAtIndex(index) if index >= 0 - - # Public: Just remove the item at the given index. - removeItemAtIndex: (index) -> - item = @items[index] - @activeItem.off? 'title-changed', @activeItemTitleChanged if item is @activeItem - @showNextItem() if item is @activeItem and @items.length > 1 - _.remove(@items, item) - @cleanupItemView(item) - @trigger 'pane:item-removed', [item, index] - - # Public: Moves the given item to a the new index. - moveItem: (item, newIndex) -> - oldIndex = @items.indexOf(item) - @items.splice(oldIndex, 1) - @items.splice(newIndex, 0, item) - @trigger 'pane:item-moved', [item, newIndex] - - # Public: Moves the given item to another pane. - moveItemToPane: (item, pane, index) -> - @isMovingItem = true - pane.addItem(item, index) - @removeItem(item) - @isMovingItem = false - - # Public: Finds the first item that matches the given uri. + # Public: Returns the first item that matches the given URI or undefined if + # none exists. itemForUri: (uri) -> - _.detect @items, (item) -> item.getUri?() is uri + find @items, (item) -> item.getUri?() is uri - # Public: Focuses the first item that matches the given uri. - showItemForUri: (uri) -> + # Public: Activates the first item that matches the given URI. Returns a + # boolean indicating whether a matching item was found. + activateItemForUri: (uri) -> if item = @itemForUri(uri) - @showItem(item) + @activateItem(item) true else false - # Private: - cleanupItemView: (item) -> - if item instanceof $ - viewToRemove = item - else if viewToRemove = @viewsByItem.get(item) - @viewsByItem.delete(item) - - if @items.length > 0 - if @isMovingItem and item is viewToRemove - viewToRemove?.detach() - else if @isMovingItem and viewToRemove?.setModel - viewToRemove.setModel(null) # dont want to destroy the model, so set to null - viewToRemove.remove() - else - viewToRemove?.remove() - else - if @isMovingItem and item is viewToRemove - viewToRemove?.detach() - else if @isMovingItem and viewToRemove?.setModel - viewToRemove.setModel(null) # dont want to destroy the model, so set to null - - @parent().view().removeChild(this) - - # Private: - viewForItem: (item) -> - if item instanceof $ - item - else if view = @viewsByItem.get(item) - view - else - viewClass = item.getViewClass() - view = new viewClass(item) - @viewsByItem.set(item, view) - view - - # Private: - viewForActiveItem: -> - @viewForItem(@activeItem) - - # Private: - adjustDimensions: -> # do nothing - - # Private: - horizontalGridUnits: -> 1 - - # Private: - verticalGridUnits: -> 1 - - # Public: Creates a new pane above with a copy of the currently focused item. - splitUp: (items...) -> - @split(items, 'column', 'before') - - # Public: Creates a new pane below with a copy of the currently focused item. - splitDown: (items...) -> - @split(items, 'column', 'after') - - # Public: Creates a new pane left with a copy of the currently focused item. - splitLeft: (items...) -> - @split(items, 'row', 'before') - - # Public: Creates a new pane right with a copy of the currently focused item. - splitRight: (items...) -> - @split(items, 'row', 'after') - - # Private: - split: (items, axis, side) -> - PaneContainer = require './pane-container' - - parent = @parent().view() - unless parent.hasClass(axis) - axis = @buildPaneAxis(axis) - if parent instanceof PaneContainer - @detach() - axis.addChild(this) - parent.setRoot(axis) - else - parent.insertChildBefore(this, axis) - axis.addChild(this) - parent = axis - - newPane = new Pane(items...) - - switch side - when 'before' then parent.insertChildBefore(this, newPane) - when 'after' then parent.insertChildAfter(this, newPane) - @getContainer().adjustPaneDimensions() - newPane.makeActive() - newPane.focus() - newPane - - # Private: - buildPaneAxis: (axis) -> - switch axis - when 'row' then new PaneRow() - when 'column' then new PaneColumn() - - # Private: - getContainer: -> - @closest('.panes').view() - # Private: copyActiveItem: -> @activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize()) - # Private: - remove: (selector, keepData) -> - return super if keepData - @parent().view().removeChild(this) + # Public: Creates a new pane to the left of the receiver. + # + # * params: + # + items: An optional array of items with which to construct the new pane. + # + # Returns the new {Pane}. + splitLeft: (params) -> + @split('horizontal', 'before', params) + + # Public: Creates a new pane to the right of the receiver. + # + # * params: + # + items: An optional array of items with which to construct the new pane. + # + # Returns the new {Pane}. + splitRight: (params) -> + @split('horizontal', 'after', params) + + # Public: Creates a new pane above the receiver. + # + # * params: + # + items: An optional array of items with which to construct the new pane. + # + # Returns the new {Pane}. + splitUp: (params) -> + @split('vertical', 'before', params) + + # Public: Creates a new pane below the receiver. + # + # * params: + # + items: An optional array of items with which to construct the new pane. + # + # Returns the new {Pane}. + splitDown: (params) -> + @split('vertical', 'after', params) # Private: - beforeRemove: -> - if @is(':has(:focus)') - @getContainer().focusNextPane() or atom.workspaceView?.focus() - else if @isActive() - @getContainer().makeNextPaneActive() + split: (orientation, side, params) -> + if @parent.orientation isnt orientation + @parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this]})) - item.destroy?() for item in @getItems() + newPane = new @constructor(extend({focused: true}, params)) + switch side + when 'before' then @parent.insertChildBefore(this, newPane) + when 'after' then @parent.insertChildAfter(this, newPane) + + newPane.activate() + newPane diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index bb28a708e..f6a4c3681 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -35,7 +35,7 @@ class TokenizedBuffer extends Model @subscribe @buffer, "changed", (e) => @handleBufferChange(e) @subscribe @buffer, "path-changed", => @bufferPath = @buffer.getPath() - @subscribe @$tabLength.changes.onValue (tabLength) => + @subscribe @$tabLength.changes, (tabLength) => lastRow = @buffer.getLastRow() @tokenizedLines = @buildPlaceholderTokenizedLinesForRows(0, lastRow) @invalidateRow(0) diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index e13dab341..b5bdff230 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -6,10 +6,10 @@ _ = require 'underscore-plus' fs = require 'fs-plus' Serializable = require 'serializable' EditorView = require './editor-view' -Pane = require './pane' -PaneColumn = require './pane-column' -PaneRow = require './pane-row' -PaneContainer = require './pane-container' +PaneView = require './pane-view' +PaneColumnView = require './pane-column-view' +PaneRowView = require './pane-row-view' +PaneContainerView = require './pane-container-view' Editor = require './editor' # Public: The container for the entire Atom application. @@ -39,7 +39,7 @@ Editor = require './editor' module.exports = class WorkspaceView extends View Serializable.includeInto(this) - atom.deserializers.add(this, Pane, PaneRow, PaneColumn, EditorView) + atom.deserializers.add(this, PaneView, PaneRowView, PaneColumnView, EditorView) @version: 2 @@ -60,7 +60,7 @@ class WorkspaceView extends View # Private: initialize: ({panes, @fullScreen}={}) -> - panes ?= new PaneContainer + panes ?= new PaneContainerView @panes.replaceWith(panes) @panes = panes @@ -168,12 +168,12 @@ class WorkspaceView extends View Q(editor ? promise) .then (editor) => if not activePane - activePane = new Pane(editor) + activePane = new PaneView(editor) @panes.setRoot(activePane) @itemOpened(editor) - activePane.showItem(editor) - activePane.focus() if changeFocus + activePane.activateItem(editor) + activePane.activate() if changeFocus @trigger "uri-opened" editor .catch (error) -> @@ -200,15 +200,15 @@ class WorkspaceView extends View else if split == 'left' pane = @getPanes()[0] - pane.showItem(paneItem) + pane.activateItem(paneItem) else paneItem = atom.project.openSync(uri, {initialLine}) - pane = new Pane(paneItem) + pane = new PaneView(paneItem) @panes.setRoot(pane) @itemOpened(paneItem) - pane.focus() if changeFocus + pane.activate() if changeFocus paneItem openSingletonSync: (uri, {changeFocus, initialLine, split}={}) -> @@ -218,8 +218,8 @@ class WorkspaceView extends View if pane paneItem = pane.itemForUri(uri) - pane.showItem(paneItem) - pane.focus() if changeFocus + pane.activateItem(paneItem) + pane.activate() if changeFocus paneItem else @openSync(uri, {changeFocus, initialLine, split}) @@ -290,11 +290,11 @@ class WorkspaceView extends View appendToRight: (element) -> @horizontal.append(element) - # Public: Returns the currently focused {Pane}. + # Public: Returns the currently focused {PaneView}. getActivePane: -> @panes.getActivePane() - # Public: Returns the currently focused item from within the focused {Pane} + # Public: Returns the currently focused item from within the focused {PaneView} getActivePaneItem: -> @panes.getActivePaneItem() @@ -329,15 +329,15 @@ class WorkspaceView extends View saveAll: -> @panes.saveAll() - # Public: Fires a callback on each open {Pane}. + # Public: Fires a callback on each open {PaneView}. eachPane: (callback) -> @panes.eachPane(callback) - # Public: Returns an Array of all open {Pane}s. + # Public: Returns an Array of all open {PaneView}s. getPanes: -> @panes.getPanes() - # Public: Return the id of the given a {Pane} + # Public: Return the id of the given a {PaneView} indexOfPane: (pane) -> @panes.indexOfPane(pane) diff --git a/static/panes.less b/static/panes.less index 33fdbe91a..f2fcb14ab 100644 --- a/static/panes.less +++ b/static/panes.less @@ -3,8 +3,47 @@ // Pane-items are things that go inside a pane. Like the UI-Demo, the // settings-view, the archive-view, the image-view. Etc. Basically a non- // editor resource with a tab. -.pane-item { - overflow: auto; - color: @text-color; - background-color: @pane-item-background-color; +.panes { + display: -webkit-flex; + -webkit-flex: 1; + + .pane-column { + display: -webkit-flex; + -webkit-flex: 1; + -webkit-flex-direction: column; + } + + .pane-row { + display: -webkit-flex; + -webkit-flex: 1; + -webkit-flex-direction: row; + } + + .pane { + position: relative; + display: -webkit-flex; + -webkit-flex: 1; + -webkit-flex-direction: column; + + .item-views { + -webkit-flex: 1; + display: -webkit-flex; + min-height: 0; + min-width: 0; + position: relative; + + .pane-item { + color: @text-color; + background-color: @pane-item-background-color; + } + + > * { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + } + } } diff --git a/static/workspace-view.less b/static/workspace-view.less index ae0afcf56..55fdbd03b 100644 --- a/static/workspace-view.less +++ b/static/workspace-view.less @@ -39,49 +39,3 @@ h6 { -webkit-flex-flow: column; } } - -.panes { - position: relative; - -webkit-flex: 1; - - .column { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - overflow-y: hidden; - } - - .row { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - overflow-x: hidden; - margin: 0; - } - - .pane { - position: absolute; - display: -webkit-flex; - -webkit-flex-flow: column; - top: 0; - bottom: 0; - left: 0; - right: 0; - box-sizing: border-box; - } - - .pane .item-views { - -webkit-flex: 1; - display: -webkit-flex; - min-height: 0; - } - - .pane .item-views > * { - -webkit-flex: 1; - min-width: 0; - } -}