Skip to content
This repository has been archived by the owner on Dec 15, 2022. It is now read-only.

Add ability to create a new window by dragging a tab out or using right click menu on tab #210

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
93 changes: 87 additions & 6 deletions lib/tab-bar-view.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: ->
Expand Down Expand Up @@ -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
Expand All @@ -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?
Expand All @@ -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) =>
Expand Down
4 changes: 4 additions & 0 deletions menus/tabs.cson
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
Expand Down
45 changes: 45 additions & 0 deletions spec/tabs-spec.coffee
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
BrowserWindow = null
{$, View} = require 'atom-space-pen-views'
_ = require 'underscore-plus'
path = require 'path'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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", ->
Expand Down Expand Up @@ -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))
Expand Down