diff --git a/lib/tab-bar-view.coffee b/lib/tab-bar-view.coffee index 95e25007..40291ca7 100644 --- a/lib/tab-bar-view.coffee +++ b/lib/tab-bar-view.coffee @@ -41,6 +41,7 @@ class TabBarView extends View 'tabs:split-down': => @splitTab('splitDown') 'tabs:split-left': => @splitTab('splitLeft') 'tabs:split-right': => @splitTab('splitRight') + 'tabs:open-in-new-window': => @onOpenInNewWindow() @on 'dragstart', '.sortable', @onDragStart @on 'dragend', '.sortable', @onDragEnd @@ -101,9 +102,12 @@ class TabBarView extends View false RendererIpc.on('tab:dropped', @onDropOnOtherWindow) + RendererIpc.on('tab:new-window-opened', @onNewWindowOpened) unsubscribe: -> RendererIpc.removeListener('tab:dropped', @onDropOnOtherWindow) + RendererIpc.removeListener('tab:new-window-opened', @onNewWindowOpened) + @subscriptions.dispose() handleTreeViewEvents: -> @@ -251,12 +255,7 @@ class TabBarView extends View item = @pane.getItems()[element.index()] return unless item? - if typeof item.getURI is 'function' - itemURI = item.getURI() ? '' - else if typeof item.getPath is 'function' - itemURI = item.getPath() ? '' - else if typeof item.getUri is 'function' - itemURI = item.getUri() ? '' + itemURI = @getItemURI item if itemURI? event.originalEvent.dataTransfer.setData 'text/plain', itemURI @@ -269,6 +268,82 @@ class TabBarView extends View event.originalEvent.dataTransfer.setData 'has-unsaved-changes', 'true' event.originalEvent.dataTransfer.setData 'modified-text', item.getText() + getItemURI: (item) -> + return unless item? + if typeof item.getURI is 'function' + itemURI = item.getURI() ? '' + else if typeof item.getPath is 'function' + itemURI = item.getPath() ? '' + else if typeof item.getUri is 'function' + itemURI = item.getUri() ? '' + + onNewWindowOpened: (title, openURI, hasUnsavedChanges, modifiedText, scrollTop, fromWindowId) => + #remove any panes created by opening the window + for item in @pane.getItems() + @pane.destroyItem(item) + + # open the content and reset state based on previous state + atom.workspace.open(openURI).then (item) -> + item.setText?(modifiedText) if hasUnsavedChanges + item.setScrollTop?(scrollTop) + + atom.focus() + + browserWindow = @browserWindowForId(fromWindowId) + browserWindow?.webContents.send('tab:item-moved-to-window') + + onOpenInNewWindow: (active) => + tabs = @getTabs() + active ?= @children('.right-clicked')[0] + @openTabInNewWindow(active, window.screenX + 20, window.screenY + 20) + + openTabInNewWindow: (tab, windowX=0, windowY=0) => + item = @pane.getItems()[$(tab).index()] + itemURI = @getItemURI(item) + return unless itemURI? + + # open and then find the new window + atom.commands.dispatch(@element, 'application:new-window') + BrowserWindow ?= require('remote').require('browser-window') + windows = BrowserWindow.getAllWindows() + newWindow = windows[windows.length - 1] + + # move the tab to the new window + newWindow.webContents.once 'did-finish-load', => + @moveAndSizeNewWindow(newWindow, windowX, windowY) + itemScrollTop = item.getScrollTop?() ? 0 + hasUnsavedChanges = item.isModified?() ? false + itemText = if hasUnsavedChanges then item.getText() else "" + + #tell the new window to open this item and pass the current item state + newWindow.send('tab:new-window-opened', + item.getTitle(), itemURI, hasUnsavedChanges, + itemText, itemScrollTop, @getWindowId()) + + #listen for open success, so old tab can be removed + RendererIpc.on('tab:item-moved-to-window', => @onTabMovedToWindow(item)) + + onTabMovedToWindow: (item) -> + # clear changes so moved item can be closed without a warning + item.getBuffer?().reload() + @pane.destroyItem(item) + RendererIpc.removeListener('tab:item-moved-to-window', @onTabMovedToWindow) + + moveAndSizeNewWindow: (newWindow, windowX=0, windowY=0) -> + WINDOW_MIN_WIDTH_HEIGHT = 300 + windowWidth = Math.min(window.innerWidth, window.screen.availWidth - windowX) + windowHeight = Math.min(window.innerHeight, window.screen.availHeight - windowY) + if windowWidth < WINDOW_MIN_WIDTH_HEIGHT + windowWidth = WINDOW_MIN_WIDTH_HEIGHT + windowX = window.screen.availWidth - WINDOW_MIN_WIDTH_HEIGHT + + if windowHeight < WINDOW_MIN_WIDTH_HEIGHT + windowHeight = WINDOW_MIN_WIDTH_HEIGHT + windowY = window.screen.availHeight - WINDOW_MIN_WIDTH_HEIGHT + + newWindow.setPosition(windowX, windowY) + newWindow.setSize(windowWidth, windowHeight) + uriHasProtocol: (uri) -> try require('url').parse(uri).protocol? @@ -279,6 +354,12 @@ class TabBarView extends View @removePlaceholder() onDragEnd: (event) => + {dataTransfer, screenX, screenY} = event.originalEvent + + #if the drop target doesn't handle the drop then this is a new window + if dataTransfer.dropEffect is "none" + @openTabInNewWindow(event.target, screenX, screenY) + @clearDropTarget() onDragOver: (event) => diff --git a/menus/tabs.cson b/menus/tabs.cson index 7bc02c13..36c53c46 100644 --- a/menus/tabs.cson +++ b/menus/tabs.cson @@ -8,6 +8,10 @@ {type: 'separator'} + {label: 'Open In New Window', command: 'tabs:open-in-new-window'} + + {type: 'separator'} + {label: 'Split Up', command: 'tabs:split-up'} {label: 'Split Down', command: 'tabs:split-down'} {label: 'Split Left', command: 'tabs:split-left'} diff --git a/spec/tabs-spec.coffee b/spec/tabs-spec.coffee index 132b11c8..576b7957 100644 --- a/spec/tabs-spec.coffee +++ b/spec/tabs-spec.coffee @@ -1,3 +1,4 @@ +BrowserWindow = null {$, View} = require 'atom-space-pen-views' _ = require 'underscore-plus' path = require 'path' @@ -429,6 +430,23 @@ describe "TabBarView", -> expect(pane.getItems().length).toBe 1 expect(pane.getItems()[0]).toBe item1 + describe "when tabs:open-in-new-window is fired", -> + it "calls the open new window command with the selected tab", -> + spyOn(tabBar, "onOpenInNewWindow").andCallThrough() + spyOn(tabBar, "openTabInNewWindow").andCallThrough() + spyOn(atom.workspace, "open").andCallThrough() + + triggerMouseDownEvent(tabBar.tabForItem(editor1), which: 3) + atom.commands.dispatch(tabBar.element, 'tabs:open-in-new-window') + + waitsFor -> + atom.workspace.open() + + runs -> + expect(tabBar.onOpenInNewWindow).toHaveBeenCalled() + expect(tabBar.openTabInNewWindow).toHaveBeenCalled() + expect(atom.workspace.open).toHaveBeenCalled() + describe "when tabs:split-up is fired", -> it "splits the selected tab up", -> triggerMouseDownEvent(tabBar.tabForItem(item2), which: 3) @@ -521,6 +539,15 @@ describe "TabBarView", -> expect(pane.getItems()[0]).toBe item1 describe "dragging and dropping tabs", -> + describe "when getting dragged tab's URI", -> + it "getItemURI returns the tab location information", -> + + itemWithNoURI = tabBar.getItemURI(item1) + expect(itemWithNoURI).not.toBeDefined() + + itemWithURI = tabBar.getItemURI(editor1) + expect(itemWithURI).toBe editor1.getURI() + describe "when a tab is dragged within the same pane", -> describe "when it is dropped on tab that's later in the list", -> it "moves the tab and its item, shows the tab's item, and focuses the pane", -> @@ -673,6 +700,24 @@ describe "TabBarView", -> if process.platform is 'darwin' expect(dragStartEvent.originalEvent.dataTransfer.getData("text/uri-list")).toEqual "file://#{editor1.getPath()}" + it "should open a new window if the target doesn't handle the file information", -> + [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(1), tabBar.tabAtIndex(0)) + spyOn(tabBar, "openTabInNewWindow").andCallThrough() + spyOn(atom.workspace, "open").andCallThrough() + + tabBar.onDragStart(dragStartEvent) + dropEvent.originalEvent.dataTransfer.dropEffect = "none" + dropEvent.originalEvent.screenX = 10 + dropEvent.originalEvent.screenY = 20 + tabBar.onDragEnd(dropEvent) + + waitsFor -> + atom.workspace.open() + + runs -> + expect(tabBar.openTabInNewWindow).toHaveBeenCalledWith(dropEvent.target, 10, 20) + expect(atom.workspace.open).toHaveBeenCalled() + describe "when a tab is dragged to another Atom window", -> it "closes the tab in the first window and opens the tab in the second window", -> [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(1), tabBar.tabAtIndex(0))