Revert "Merge pull request #356 from pulsar-edit/machine-decafe-tabs"

This reverts commit 1da0dab087, reversing
changes made to ecb27d5797.
This commit is contained in:
DeeDeeG 2023-02-06 23:34:08 -05:00
parent a6253bea80
commit 9365b1ea91
5 changed files with 855 additions and 1168 deletions

View File

@ -1,7 +1,7 @@
const {CompositeDisposable, Disposable} = require('atom')
const getIconServices = require('./get-icon-services')
const layout = require('./layout')
const TabBarView = require('./tab-bar-view.js')
const TabBarView = require('./tab-bar-view')
const MRUListView = require('./mru-list-view')
const _ = require('underscore-plus')

View File

@ -0,0 +1,558 @@
BrowserWindow = null # Defer require until actually used
{ipcRenderer} = require 'electron'
{CompositeDisposable} = require 'atom'
TabView = require './tab-view'
module.exports =
class TabBarView
constructor: (@pane, @location) ->
@element = document.createElement('ul')
@element.classList.add("list-inline")
@element.classList.add("tab-bar")
@element.classList.add("inset-panel")
@element.setAttribute('is', 'atom-tabs')
@element.setAttribute("tabindex", -1)
@element.setAttribute("location", @location)
@tabs = []
@tabsByElement = new WeakMap
@subscriptions = new CompositeDisposable
@paneElement = @pane.getElement()
@subscriptions.add atom.commands.add @paneElement,
'tabs:keep-pending-tab': => @terminatePendingStates()
'tabs:close-tab': => @closeTab(@getActiveTab())
'tabs:close-other-tabs': => @closeOtherTabs(@getActiveTab())
'tabs:close-tabs-to-right': => @closeTabsToRight(@getActiveTab())
'tabs:close-tabs-to-left': => @closeTabsToLeft(@getActiveTab())
'tabs:close-saved-tabs': => @closeSavedTabs()
'tabs:close-all-tabs': (event) =>
event.stopPropagation()
@closeAllTabs()
'tabs:open-in-new-window': => @openInNewWindow()
addElementCommands = (commands) =>
commandsWithPropagationStopped = {}
Object.keys(commands).forEach (name) ->
commandsWithPropagationStopped[name] = (event) ->
event.stopPropagation()
commands[name]()
@subscriptions.add(atom.commands.add(@element, commandsWithPropagationStopped))
addElementCommands
'tabs:close-tab': => @closeTab()
'tabs:close-other-tabs': => @closeOtherTabs()
'tabs:close-tabs-to-right': => @closeTabsToRight()
'tabs:close-tabs-to-left': => @closeTabsToLeft()
'tabs:close-saved-tabs': => @closeSavedTabs()
'tabs:close-all-tabs': => @closeAllTabs()
'tabs:split-up': => @splitTab('splitUp')
'tabs:split-down': => @splitTab('splitDown')
'tabs:split-left': => @splitTab('splitLeft')
'tabs:split-right': => @splitTab('splitRight')
@element.addEventListener "mouseenter", @onMouseEnter.bind(this)
@element.addEventListener "mouseleave", @onMouseLeave.bind(this)
@element.addEventListener "mousewheel", @onMouseWheel.bind(this)
@element.addEventListener "dragstart", @onDragStart.bind(this)
@element.addEventListener "dragend", @onDragEnd.bind(this)
@element.addEventListener "dragleave", @onDragLeave.bind(this)
@element.addEventListener "dragover", @onDragOver.bind(this)
@element.addEventListener "drop", @onDrop.bind(this)
# Toggle the tab bar when a tab is dragged over the pane with alwaysShowTabBar = false
@paneElement.addEventListener 'dragenter', @onPaneDragEnter.bind(this)
@paneElement.addEventListener 'dragleave', @onPaneDragLeave.bind(this)
@paneContainer = @pane.getContainer()
@addTabForItem(item) for item in @pane.getItems()
@subscriptions.add @pane.onDidDestroy =>
@destroy()
@subscriptions.add @pane.onDidAddItem ({item, index}) =>
@addTabForItem(item, index)
@subscriptions.add @pane.onDidMoveItem ({item, newIndex}) =>
@moveItemTabToIndex(item, newIndex)
@subscriptions.add @pane.onDidRemoveItem ({item}) =>
@removeTabForItem(item)
@subscriptions.add @pane.onDidChangeActiveItem (item) =>
@updateActiveTab()
@subscriptions.add atom.config.observe 'tabs.tabScrolling', (value) => @updateTabScrolling(value)
@subscriptions.add atom.config.observe 'tabs.tabScrollingThreshold', (value) => @updateTabScrollingThreshold(value)
@subscriptions.add atom.config.observe 'tabs.alwaysShowTabBar', => @updateTabBarVisibility()
@updateActiveTab()
@element.addEventListener "mousedown", @onMouseDown.bind(this)
@element.addEventListener "click", @onClick.bind(this)
@element.addEventListener "auxclick", @onClick.bind(this)
@element.addEventListener "dblclick", @onDoubleClick.bind(this)
@onDropOnOtherWindow = @onDropOnOtherWindow.bind(this)
ipcRenderer.on('tab:dropped', @onDropOnOtherWindow)
destroy: ->
ipcRenderer.removeListener('tab:dropped', @onDropOnOtherWindow)
@subscriptions.dispose()
@element.remove()
terminatePendingStates: ->
tab.terminatePendingState?() for tab in @getTabs()
return
addTabForItem: (item, index) ->
tabView = new TabView({
item,
@pane,
@tabs,
didClickCloseIcon: =>
@closeTab(tabView)
return
@location
})
tabView.terminatePendingState() if @isItemMovingBetweenPanes
@tabsByElement.set(tabView.element, tabView)
@insertTabAtIndex(tabView, index)
if atom.config.get('tabs.addNewTabsAtEnd')
@pane.moveItem(item, @pane.getItems().length - 1) unless @isItemMovingBetweenPanes
moveItemTabToIndex: (item, index) ->
tabIndex = @tabs.findIndex((t) -> t.item is item)
if tabIndex isnt -1
tab = @tabs[tabIndex]
tab.element.remove()
@tabs.splice(tabIndex, 1)
@insertTabAtIndex(tab, index)
insertTabAtIndex: (tab, index) ->
followingTab = @tabs[index] if index?
if followingTab
@element.insertBefore(tab.element, followingTab.element)
@tabs.splice(index, 0, tab)
else
@element.appendChild(tab.element)
@tabs.push(tab)
tab.updateTitle()
@updateTabBarVisibility()
removeTabForItem: (item) ->
tabIndex = @tabs.findIndex((t) -> t.item is item)
if tabIndex isnt -1
tab = @tabs[tabIndex]
@tabs.splice(tabIndex, 1)
@tabsByElement.delete(tab)
tab.destroy()
tab.updateTitle() for tab in @getTabs()
@updateTabBarVisibility()
updateTabBarVisibility: ->
# Show tab bar if the setting is true or there is more than one tab
if atom.config.get('tabs.alwaysShowTabBar') or @pane.getItems().length > 1
@element.classList.remove('hidden')
else
@element.classList.add('hidden')
getTabs: ->
@tabs.slice()
tabAtIndex: (index) ->
@tabs[index]
tabForItem: (item) ->
@tabs.find((t) -> t.item is item)
setActiveTab: (tabView) ->
if tabView? and tabView isnt @activeTab
@activeTab?.element.classList.remove('active')
@activeTab = tabView
@activeTab.element.classList.add('active')
@activeTab.element.scrollIntoView(false)
getActiveTab: ->
@tabForItem(@pane.getActiveItem())
updateActiveTab: ->
@setActiveTab(@tabForItem(@pane.getActiveItem()))
closeTab: (tab) ->
tab ?= @rightClickedTab
@pane.destroyItem(tab.item) if tab?
openInNewWindow: (tab) ->
tab ?= @rightClickedTab
item = tab?.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()
return unless itemURI?
@closeTab(tab)
tab.element.style.maxWidth = '' for tab in @getTabs()
pathsToOpen = [atom.project.getPaths(), itemURI].reduce ((a, b) -> a.concat(b)), []
atom.open({pathsToOpen: pathsToOpen, newWindow: true, devMode: atom.devMode, safeMode: atom.safeMode})
splitTab: (fn) ->
if item = @rightClickedTab?.item
if copiedItem = item.copy?()
@pane[fn](items: [copiedItem])
closeOtherTabs: (active) ->
tabs = @getTabs()
active ?= @rightClickedTab
return unless active?
@closeTab tab for tab in tabs when tab isnt active
closeTabsToRight: (active) ->
tabs = @getTabs()
active ?= @rightClickedTab
index = tabs.indexOf(active)
return if index is -1
@closeTab tab for tab, i in tabs when i > index
closeTabsToLeft: (active) ->
tabs = @getTabs()
active ?= @rightClickedTab
index = tabs.indexOf(active)
return if index is -1
@closeTab tab for tab, i in tabs when i < index
closeSavedTabs: ->
for tab in @getTabs()
@closeTab(tab) unless tab.item.isModified?()
closeAllTabs: ->
@closeTab(tab) for tab in @getTabs()
getWindowId: ->
@windowId ?= atom.getCurrentWindow().id
onDragStart: (event) ->
@draggedTab = @tabForElement(event.target)
return unless @draggedTab
@lastDropTargetIndex = null
event.dataTransfer.setData 'atom-tab-event', 'true'
@draggedTab.element.classList.add('is-dragging')
@draggedTab.destroyTooltip()
tabIndex = @tabs.indexOf(@draggedTab)
event.dataTransfer.setData 'sortable-index', tabIndex
paneIndex = @paneContainer.getPanes().indexOf(@pane)
event.dataTransfer.setData 'from-pane-index', paneIndex
event.dataTransfer.setData 'from-pane-id', @pane.id
event.dataTransfer.setData 'from-window-id', @getWindowId()
item = @pane.getItems()[@tabs.indexOf(@draggedTab)]
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() ? ''
if typeof item.getAllowedLocations is 'function'
for location in item.getAllowedLocations()
event.dataTransfer.setData("allowed-location-#{location}", 'true')
else
event.dataTransfer.setData 'allow-all-locations', 'true'
if itemURI?
event.dataTransfer.setData 'text/plain', itemURI
if process.platform is 'darwin' # see #69
itemURI = "file://#{itemURI}" unless @uriHasProtocol(itemURI)
event.dataTransfer.setData 'text/uri-list', itemURI
if item.isModified?() and item.getText?
event.dataTransfer.setData 'has-unsaved-changes', 'true'
event.dataTransfer.setData 'modified-text', item.getText()
uriHasProtocol: (uri) ->
try
require('url').parse(uri).protocol?
catch error
false
onDragLeave: (event) ->
# Do not do anything unless the drag goes outside the tab bar
unless event.currentTarget.contains(event.relatedTarget)
@removePlaceholder()
@lastDropTargetIndex = null
tab.element.style.maxWidth = '' for tab in @getTabs()
onDragEnd: (event) ->
return unless @tabForElement(event.target)
@clearDropTarget()
onDragOver: (event) ->
return unless @isAtomTabEvent(event)
return unless @itemIsAllowed(event, @location)
event.preventDefault()
event.stopPropagation()
newDropTargetIndex = @getDropTargetIndex(event)
return unless newDropTargetIndex?
return if @lastDropTargetIndex is newDropTargetIndex
@lastDropTargetIndex = newDropTargetIndex
@removeDropTargetClasses()
tabs = @getTabs()
placeholder = @getPlaceholder()
return unless placeholder?
if newDropTargetIndex < tabs.length
tab = tabs[newDropTargetIndex]
tab.element.classList.add 'is-drop-target'
tab.element.parentElement.insertBefore(placeholder, tab.element)
else
if tab = tabs[newDropTargetIndex - 1]
tab.element.classList.add 'drop-target-is-after'
if sibling = tab.element.nextSibling
tab.element.parentElement.insertBefore(placeholder, sibling)
else
tab.element.parentElement.appendChild(placeholder)
onDropOnOtherWindow: (event, fromPaneId, fromItemIndex) ->
if @pane.id is fromPaneId
if itemToRemove = @pane.getItems()[fromItemIndex]
@pane.destroyItem(itemToRemove)
@clearDropTarget()
clearDropTarget: ->
@draggedTab?.element.classList.remove('is-dragging')
@draggedTab?.updateTooltip()
@draggedTab = null
@removeDropTargetClasses()
@removePlaceholder()
onDrop: (event) ->
return unless @isAtomTabEvent(event)
event.preventDefault()
fromWindowId = parseInt(event.dataTransfer.getData('from-window-id'))
fromPaneId = parseInt(event.dataTransfer.getData('from-pane-id'))
fromIndex = parseInt(event.dataTransfer.getData('sortable-index'))
fromPaneIndex = parseInt(event.dataTransfer.getData('from-pane-index'))
hasUnsavedChanges = event.dataTransfer.getData('has-unsaved-changes') is 'true'
modifiedText = event.dataTransfer.getData('modified-text')
toIndex = @getDropTargetIndex(event)
toPane = @pane
@clearDropTarget()
return unless @itemIsAllowed(event, @location)
if fromWindowId is @getWindowId()
fromPane = @paneContainer.getPanes()[fromPaneIndex]
if fromPane?.id isnt fromPaneId
# If dragging from a different pane container, we have to be more
# exhaustive in our search.
fromPane = Array.from document.querySelectorAll('atom-pane')
.map (paneEl) -> paneEl.model
.find (pane) -> pane.id is fromPaneId
item = fromPane.getItems()[fromIndex]
@moveItemBetweenPanes(fromPane, fromIndex, toPane, toIndex, item) if item?
else
droppedURI = event.dataTransfer.getData('text/plain')
atom.workspace.open(droppedURI).then (item) =>
# Move the item from the pane it was opened on to the target pane
# where it was dropped onto
activePane = atom.workspace.getActivePane()
activeItemIndex = activePane.getItems().indexOf(item)
@moveItemBetweenPanes(activePane, activeItemIndex, toPane, toIndex, item)
item.setText?(modifiedText) if hasUnsavedChanges
if not isNaN(fromWindowId)
# Let the window where the drag started know that the tab was dropped
browserWindow = @browserWindowForId(fromWindowId)
browserWindow?.webContents.send('tab:dropped', fromPaneId, fromIndex)
atom.focus()
# Show the tab bar when a tab is being dragged in this pane when alwaysShowTabBar = false
onPaneDragEnter: (event) ->
return unless @isAtomTabEvent(event)
return unless @itemIsAllowed(event, @location)
return if @pane.getItems().length > 1 or atom.config.get('tabs.alwaysShowTabBar')
if @paneElement.contains(event.relatedTarget)
@element.classList.remove('hidden')
# Hide the tab bar when the dragged tab leaves this pane when alwaysShowTabBar = false
onPaneDragLeave: (event) ->
return unless @isAtomTabEvent(event)
return unless @itemIsAllowed(event, @location)
return if @pane.getItems().length > 1 or atom.config.get('tabs.alwaysShowTabBar')
unless @paneElement.contains(event.relatedTarget)
@element.classList.add('hidden')
onMouseWheel: (event) ->
return if event.shiftKey or not @tabScrolling
@wheelDelta ?= 0
@wheelDelta += event.wheelDeltaY
if @wheelDelta <= -@tabScrollingThreshold
@wheelDelta = 0
@pane.activateNextItem()
else if @wheelDelta >= @tabScrollingThreshold
@wheelDelta = 0
@pane.activatePreviousItem()
onMouseDown: (event) ->
@pane.activate() unless @pane.isDestroyed()
tab = @tabForElement(event.target)
return unless tab
if event.button is 2 or (event.button is 0 and event.ctrlKey is true)
@rightClickedTab = tab
event.preventDefault()
else if event.button is 1
# This prevents Chromium from activating "scroll mode" when
# middle-clicking while some tabs are off-screen.
event.preventDefault()
onClick: (event) ->
tab = @tabForElement(event.target)
return unless tab
event.preventDefault()
if event.button is 2 or (event.button is 0 and event.ctrlKey is true)
# Bail out early when receiving this event, because we have already
# handled it in the mousedown handler.
return
else if event.button is 0 and not event.target.classList.contains('close-icon')
@pane.activateItem(tab.item)
else if event.button is 1
@pane.destroyItem(tab.item)
onDoubleClick: (event) ->
if tab = @tabForElement(event.target)
tab.item.terminatePendingState?()
else if event.target is @element
atom.commands.dispatch(@element, 'application:new-file')
event.preventDefault()
updateTabScrollingThreshold: (value) ->
@tabScrollingThreshold = value
updateTabScrolling: (value) ->
if value is 'platform'
@tabScrolling = (process.platform is 'linux')
else
@tabScrolling = value
browserWindowForId: (id) ->
BrowserWindow ?= require('electron').remote.BrowserWindow
BrowserWindow.fromId id
moveItemBetweenPanes: (fromPane, fromIndex, toPane, toIndex, item) ->
try
if toPane is fromPane
toIndex-- if fromIndex < toIndex
toPane.moveItem(item, toIndex)
else
@isItemMovingBetweenPanes = true
fromPane.moveItemToPane(item, toPane, toIndex--)
toPane.activateItem(item)
toPane.activate()
finally
@isItemMovingBetweenPanes = false
removeDropTargetClasses: ->
workspaceElement = atom.workspace.getElement()
for dropTarget in workspaceElement.querySelectorAll('.tab-bar .is-drop-target')
dropTarget.classList.remove('is-drop-target')
for dropTarget in workspaceElement.querySelectorAll('.tab-bar .drop-target-is-after')
dropTarget.classList.remove('drop-target-is-after')
getDropTargetIndex: (event) ->
target = event.target
return if @isPlaceholder(target)
tabs = @getTabs()
tab = @tabForElement(target)
tab ?= tabs[tabs.length - 1]
return 0 unless tab?
{left, width} = tab.element.getBoundingClientRect()
elementCenter = left + width / 2
elementIndex = tabs.indexOf(tab)
if event.pageX < elementCenter
elementIndex
else
elementIndex + 1
getPlaceholder: ->
return @placeholderEl if @placeholderEl?
@placeholderEl = document.createElement("li")
@placeholderEl.classList.add("placeholder")
@placeholderEl
removePlaceholder: ->
@placeholderEl?.remove()
@placeholderEl = null
isPlaceholder: (element) ->
element.classList.contains('placeholder')
onMouseEnter: ->
for tab in @getTabs()
{width} = tab.element.getBoundingClientRect()
tab.element.style.maxWidth = width.toFixed(2) + 'px'
return
onMouseLeave: ->
tab.element.style.maxWidth = '' for tab in @getTabs()
return
tabForElement: (element) ->
currentElement = element
while currentElement?
if tab = @tabsByElement.get(currentElement)
return tab
else
currentElement = currentElement.parentElement
isAtomTabEvent: (event) ->
for item in event.dataTransfer.items
if item.type is 'atom-tab-event'
return true
return false
itemIsAllowed: (event, location) ->
for item in event.dataTransfer.items
if item.type is 'allow-all-locations' or item.type is "allowed-location-#{location}"
return true
return false

View File

@ -1,766 +0,0 @@
let BrowserWindow = null; // Defer require until actually used
const {ipcRenderer} = require('electron');
const {CompositeDisposable} = require('atom');
const TabView = require('./tab-view.js');
class TabBarView {
constructor(pane, location) {
this.pane = pane;
this.location = location;
this.element = document.createElement("ul");
this.element.classList.add("list-inline");
this.element.classList.add("tab-bar");
this.element.classList.add("inset-panel");
this.element.setAttribute("is", "atom-tabs");
this.element.setAttribute("tabindex", -1);
this.element.setAttribute("location", this.location);
this.tabs = [];
this.tabsByElement = new WeakMap;
this.subscriptions = new CompositeDisposable;
this.paneElement = this.pane.getElement();
this.subscriptions.add(atom.commands.add(this.paneElement, {
"tabs:keep-pending-tab": () => this.terminatePendingStates(),
"tabs:close-tab": () => this.closeTab(this.getActiveTab()),
"tabs:close-other-tabs": () => this.closeOtherTabs(this.getActiveTab()),
"tabs:close-tabs-to-right": () => this.closeTabsToRight(this.getActiveTab()),
"tabs:close-tabs-to-left": () => this.closeTabsToLeft(this.getActiveTab()),
"tabs:close-saved-tabs": () => this.closeSavedTabs(),
"tabs:close-all-tabs": event => {
event.stopPropagation();
return this.closeAllTabs();
},
"tabs:open-in-new-window": () => this.openInNewWindow()
}));
const addElementCommands = commands => {
const commandsWithPropagationStopped = {};
Object.keys(commands).forEach(name => commandsWithPropagationStopped[name] = function(event) {
event.stopPropagation();
return commands[name]();
});
return this.subscriptions.add(atom.commands.add(this.element, commandsWithPropagationStopped));
};
addElementCommands({
"tabs:close-tab": () => this.closeTab(),
"tabs:close-other-tabs": () => this.closeOtherTabs(),
"tabs:close-tabs-to-right": () => this.closeTabsToRight(),
"tabs:close-tabs-to-left": () => this.closeTabsToLeft(),
"tabs:close-saved-tabs": () => this.closeSavedTabs(),
"tabs:close-all-tabs": () => this.closeAllTabs(),
"tabs:split-up": () => this.splitTab("splitUp"),
"tabs:split-down": () => this.splitTab("splitDown"),
"tabs:split-left": () => this.splitTab("splitLeft"),
"tabs:split-right": () => this.splitTab("splitRight")
});
this.element.addEventListener("mouseenter", this.onMouseEnter.bind(this));
this.element.addEventListener("mouseleave", this.onMouseLeave.bind(this));
this.element.addEventListener("mousewheel", this.onMouseWheel.bind(this));
this.element.addEventListener("dragstart", this.onDragStart.bind(this));
this.element.addEventListener("dragend", this.onDragEnd.bind(this));
this.element.addEventListener("dragleave", this.onDragLeave.bind(this));
this.element.addEventListener("dragover", this.onDragOver.bind(this));
this.element.addEventListener("drop", this.onDrop.bind(this));
// Toggle the tab bar when a tab is dragged over the pane with alwaysShowTabBar = false
this.paneElement.addEventListener('dragenter', this.onPaneDragEnter.bind(this));
this.paneElement.addEventListener('dragleave', this.onPaneDragLeave.bind(this));
this.paneContainer = this.pane.getContainer();
for (let item of Array.from(this.pane.getItems())) { this.addTabForItem(item); }
this.subscriptions.add(this.pane.onDidDestroy(() => {
return this.destroy();
}));
this.subscriptions.add(this.pane.onDidAddItem(({item, index}) => {
return this.addTabForItem(item, index);
}));
this.subscriptions.add(this.pane.onDidMoveItem(({item, newIndex}) => {
return this.moveItemTabToIndex(item, newIndex);
}));
this.subscriptions.add(this.pane.onDidRemoveItem(({item}) => {
return this.removeTabForItem(item);
}));
this.subscriptions.add(this.pane.onDidChangeActiveItem(item => {
return this.updateActiveTab();
}));
this.subscriptions.add(atom.config.observe('tabs.tabScrolling', value => this.updateTabScrolling(value)));
this.subscriptions.add(atom.config.observe('tabs.tabScrollingThreshold', value => this.updateTabScrollingThreshold(value)));
this.subscriptions.add(atom.config.observe('tabs.alwaysShowTabBar', () => this.updateTabBarVisibility()));
this.updateActiveTab();
this.element.addEventListener("mousedown", this.onMouseDown.bind(this));
this.element.addEventListener("click", this.onClick.bind(this));
this.element.addEventListener("auxclick", this.onClick.bind(this));
this.element.addEventListener("dblclick", this.onDoubleClick.bind(this));
this.onDropOnOtherWindow = this.onDropOnOtherWindow.bind(this);
ipcRenderer.on('tab:dropped', this.onDropOnOtherWindow);
}
destroy() {
ipcRenderer.removeListener('tab:dropped', this.onDropOnOtherWindow);
this.subscriptions.dispose();
return this.element.remove();
}
terminatePendingStates() {
for (let tab of this.getTabs()) {
if (typeof tab.terminatePendingState === "function") {
tab.terminatePendingState();
}
}
}
addTabForItem(item, index) {
let tabView = new TabView({
item,
pane: this.pane,
tabs: this.tabs,
didClickCloseIcon: () => {
this.closeTab(tabView);
},
location: this.location
});
if (this.isItemMovingBetweenPanes) {
tabView.terminatePendingState();
}
this.tabsByElement.set(tabView.element, tabView);
this.insertTabAtIndex(tabView, index);
if (atom.config.get('tabs.addNewTabsAtEnd')) {
if (!this.isItemMovingBetweenPanes) {
return this.pane.moveItem(item, this.pane.getItems().length - 1);
}
}
}
moveItemTabToIndex(item, index) {
const tabIndex = this.tabs.findIndex(t => t.item === item);
if (tabIndex !== -1) {
const tab = this.tabs[tabIndex];
tab.element.remove();
this.tabs.splice(tabIndex, 1);
return this.insertTabAtIndex(tab, index);
}
}
insertTabAtIndex(tab, index) {
let followingTab;
if (index != null) {
followingTab = this.tabs[index];
}
if (followingTab) {
this.element.insertBefore(tab.element, followingTab.element);
this.tabs.splice(index, 0, tab);
} else {
this.element.appendChild(tab.element);
this.tabs.push(tab);
}
tab.updateTitle();
return this.updateTabBarVisibility();
}
removeTabForItem(item) {
let tab;
const tabIndex = this.tabs.findIndex(t => t.item === item);
if (tabIndex !== -1) {
tab = this.tabs[tabIndex];
this.tabs.splice(tabIndex, 1);
this.tabsByElement.delete(tab);
tab.destroy();
}
for (tab of this.getTabs()) {
tab.updateTitle();
}
return this.updateTabBarVisibility();
}
updateTabBarVisibility() {
// Show tab bar if the setting is true or there is more than one tab
if (atom.config.get('tabs.alwaysShowTabBar') || (this.pane.getItems().length > 1)) {
return this.element.classList.remove('hidden');
} else {
return this.element.classList.add('hidden');
}
}
getTabs() {
return this.tabs.slice();
}
tabAtIndex(index) {
return this.tabs[index];
}
tabForItem(item) {
return this.tabs.find(t => t.item === item);
}
setActiveTab(tabView) {
if ((tabView != null) && (tabView !== this.activeTab)) {
if (this.activeTab != null) {
this.activeTab.element.classList.remove('active');
}
this.activeTab = tabView;
this.activeTab.element.classList.add('active');
return this.activeTab.element.scrollIntoView(false);
}
}
getActiveTab() {
return this.tabForItem(this.pane.getActiveItem());
}
updateActiveTab() {
return this.setActiveTab(this.tabForItem(this.pane.getActiveItem()));
}
closeTab(tab) {
if (tab == null) { tab = this.rightClickedTab; }
if (tab != null) { return this.pane.destroyItem(tab.item); }
}
openInNewWindow(tab) {
let itemURI;
if (tab == null) { tab = this.rightClickedTab; }
const item = tab != null ? tab.item : undefined;
if (item == null) { return; }
if (typeof item.getURI === 'function') {
itemURI = item.getURI();
} else if (typeof item.getPath === 'function') {
itemURI = item.getPath();
} else if (typeof item.getUri === 'function') {
itemURI = item.getUri();
}
if (itemURI == null) { return; }
this.closeTab(tab);
for (tab of this.getTabs()) { tab.element.style.maxWidth = ''; }
const pathsToOpen = [atom.project.getPaths(), itemURI].reduce(((a, b) => a.concat(b)), []);
return atom.open({pathsToOpen, newWindow: true, devMode: atom.devMode, safeMode: atom.safeMode});
}
splitTab(fn) {
let item;
if (item = this.rightClickedTab != null ? this.rightClickedTab.item : undefined) {
let copiedItem;
if (copiedItem = typeof item.copy === 'function' ? item.copy() : undefined) {
return this.pane[fn]({items: [copiedItem]});
}
}
}
closeOtherTabs(active) {
const tabs = this.getTabs();
if (active == null) { active = this.rightClickedTab; }
if (active == null) { return; }
return tabs.filter((tab) => tab !== active).map((tab) => this.closeTab(tab));
}
closeTabsToRight(active) {
const tabs = this.getTabs();
if (active == null) { active = this.rightClickedTab; }
const index = tabs.indexOf(active);
if (index === -1) { return; }
return (() => {
const result = [];
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
if (i > index) {
result.push(this.closeTab(tab));
}
}
return result;
})();
}
closeTabsToLeft(active) {
const tabs = this.getTabs();
if (active == null) { active = this.rightClickedTab; }
const index = tabs.indexOf(active);
if (index === -1) { return; }
return (() => {
const result = [];
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
if (i < index) {
result.push(this.closeTab(tab));
}
}
return result;
})();
}
closeSavedTabs() {
return (() => {
const result = [];
for (let tab of this.getTabs()) {
if (!(typeof tab.item.isModified === 'function' ? tab.item.isModified() : undefined)) { result.push(this.closeTab(tab)); } else {
result.push(undefined);
}
}
return result;
})();
}
closeAllTabs() {
return this.getTabs().map((tab) => this.closeTab(tab));
}
getWindowId() {
return this.windowId != null ? this.windowId : (this.windowId = atom.getCurrentWindow().id);
}
onDragStart(event) {
let itemURI;
this.draggedTab = this.tabForElement(event.target);
if (!this.draggedTab) { return; }
this.lastDropTargetIndex = null;
event.dataTransfer.setData('atom-tab-event', 'true');
this.draggedTab.element.classList.add('is-dragging');
this.draggedTab.destroyTooltip();
const tabIndex = this.tabs.indexOf(this.draggedTab);
event.dataTransfer.setData('sortable-index', tabIndex);
const paneIndex = this.paneContainer.getPanes().indexOf(this.pane);
event.dataTransfer.setData('from-pane-index', paneIndex);
event.dataTransfer.setData('from-pane-id', this.pane.id);
event.dataTransfer.setData('from-window-id', this.getWindowId());
const item = this.pane.getItems()[this.tabs.indexOf(this.draggedTab)];
if (item == null) { return; }
if (typeof item.getURI === 'function') {
let left;
itemURI = (left = item.getURI()) != null ? left : '';
} else if (typeof item.getPath === 'function') {
let left1;
itemURI = (left1 = item.getPath()) != null ? left1 : '';
} else if (typeof item.getUri === 'function') {
let left2;
itemURI = (left2 = item.getUri()) != null ? left2 : '';
}
if (typeof item.getAllowedLocations === 'function') {
for (let location of item.getAllowedLocations()) {
event.dataTransfer.setData(`allowed-location-${location}`, 'true');
}
} else {
event.dataTransfer.setData('allow-all-locations', 'true');
}
if (itemURI != null) {
event.dataTransfer.setData('text/plain', itemURI);
if (process.platform === 'darwin') { // see #69
if (!this.uriHasProtocol(itemURI)) { itemURI = `file://${itemURI}`; }
event.dataTransfer.setData('text/uri-list', itemURI);
}
if ((typeof item.isModified === 'function' ? item.isModified() : undefined) && (item.getText != null)) {
event.dataTransfer.setData('has-unsaved-changes', 'true');
return event.dataTransfer.setData('modified-text', item.getText());
}
}
}
uriHasProtocol(uri) {
try {
return (require('url').parse(uri).protocol != null);
} catch (error) {
return false;
}
}
onDragLeave(event) {
// Do not do anything unless the drag goes outside the tab bar
if (!event.currentTarget.contains(event.relatedTarget)) {
this.removePlaceholder();
this.lastDropTargetIndex = null;
return this.getTabs().map((tab) => (tab.element.style.maxWidth = ''));
}
}
onDragEnd(event) {
if (!this.tabForElement(event.target)) { return; }
return this.clearDropTarget();
}
onDragOver(event) {
let tab;
if (!this.isAtomTabEvent(event)) { return; }
if (!this.itemIsAllowed(event, this.location)) { return; }
event.preventDefault();
event.stopPropagation();
const newDropTargetIndex = this.getDropTargetIndex(event);
if (newDropTargetIndex == null) { return; }
if (this.lastDropTargetIndex === newDropTargetIndex) { return; }
this.lastDropTargetIndex = newDropTargetIndex;
this.removeDropTargetClasses();
const tabs = this.getTabs();
const placeholder = this.getPlaceholder();
if (placeholder == null) { return; }
if (newDropTargetIndex < tabs.length) {
tab = tabs[newDropTargetIndex];
tab.element.classList.add('is-drop-target');
return tab.element.parentElement.insertBefore(placeholder, tab.element);
} else {
if (tab = tabs[newDropTargetIndex - 1]) {
let sibling;
tab.element.classList.add('drop-target-is-after');
if ((sibling = tab.element.nextSibling)) {
return tab.element.parentElement.insertBefore(placeholder, sibling);
} else {
return tab.element.parentElement.appendChild(placeholder);
}
}
}
}
onDropOnOtherWindow(event, fromPaneId, fromItemIndex) {
if (this.pane.id === fromPaneId) {
let itemToRemove;
if (itemToRemove = this.pane.getItems()[fromItemIndex]) {
this.pane.destroyItem(itemToRemove);
}
}
return this.clearDropTarget();
}
clearDropTarget() {
if (this.draggedTab != null) {
this.draggedTab.element.classList.remove('is-dragging');
}
if (this.draggedTab != null) {
this.draggedTab.updateTooltip();
}
this.draggedTab = null;
this.removeDropTargetClasses();
return this.removePlaceholder();
}
onDrop(event) {
if (!this.isAtomTabEvent(event)) {
return;
}
event.preventDefault();
const fromWindowId = parseInt(event.dataTransfer.getData('from-window-id'));
const fromPaneId = parseInt(event.dataTransfer.getData('from-pane-id'));
const fromIndex = parseInt(event.dataTransfer.getData('sortable-index'));
const fromPaneIndex = parseInt(event.dataTransfer.getData('from-pane-index'));
const hasUnsavedChanges = event.dataTransfer.getData('has-unsaved-changes') === 'true';
const modifiedText = event.dataTransfer.getData('modified-text');
const toIndex = this.getDropTargetIndex(event);
const toPane = this.pane;
this.clearDropTarget();
if (!this.itemIsAllowed(event, this.location)) { return; }
if (fromWindowId === this.getWindowId()) {
let fromPane = this.paneContainer.getPanes()[fromPaneIndex];
if ((fromPane != null ? fromPane.id : undefined) !== fromPaneId) {
// If dragging from a different pane container, we have to be more
// exhaustive in our search.
fromPane = Array.from(document.querySelectorAll('atom-pane'))
.map(paneEl => paneEl.model)
.find(pane => pane.id === fromPaneId);
}
const item = fromPane.getItems()[fromIndex];
if (item != null) { return this.moveItemBetweenPanes(fromPane, fromIndex, toPane, toIndex, item); }
} else {
const droppedURI = event.dataTransfer.getData('text/plain');
atom.workspace.open(droppedURI).then(item => {
// Move the item from the pane it was opened on to the target pane
// where it was dropped onto
const activePane = atom.workspace.getActivePane();
const activeItemIndex = activePane.getItems().indexOf(item);
this.moveItemBetweenPanes(activePane, activeItemIndex, toPane, toIndex, item);
if (hasUnsavedChanges) { if (typeof item.setText === 'function') {
item.setText(modifiedText);
} }
if (!isNaN(fromWindowId)) {
// Let the window where the drag started know that the tab was dropped
const browserWindow = this.browserWindowForId(fromWindowId);
return (browserWindow != null ? browserWindow.webContents.send('tab:dropped', fromPaneId, fromIndex) : undefined);
}
});
return atom.focus();
}
}
// Show the tab bar when a tab is being dragged in this pane when alwaysShowTabBar = false
onPaneDragEnter(event) {
if (!this.isAtomTabEvent(event)) {
return;
}
if (!this.itemIsAllowed(event, this.location)) {
return;
}
if ((this.pane.getItems().length > 1) || atom.config.get('tabs.alwaysShowTabBar')) {
return;
}
if (this.paneElement.contains(event.relatedTarget)) {
return this.element.classList.remove('hidden');
}
}
// Hide the tab bar when the dragged tab leaves this pane when alwaysShowTabBar = false
onPaneDragLeave(event) {
if (!this.isAtomTabEvent(event)) {
return;
}
if (!this.itemIsAllowed(event, this.location)) {
return;
}
if ((this.pane.getItems().length > 1) || atom.config.get('tabs.alwaysShowTabBar')) {
return;
}
if (!this.paneElement.contains(event.relatedTarget)) {
return this.element.classList.add('hidden');
}
}
onMouseWheel(event) {
if (event.shiftKey || !this.tabScrolling) { return; }
if (this.wheelDelta == null) { this.wheelDelta = 0; }
this.wheelDelta += event.wheelDeltaY;
if (this.wheelDelta <= -this.tabScrollingThreshold) {
this.wheelDelta = 0;
return this.pane.activateNextItem();
} else if (this.wheelDelta >= this.tabScrollingThreshold) {
this.wheelDelta = 0;
return this.pane.activatePreviousItem();
}
}
onMouseDown(event) {
if (!this.pane.isDestroyed()) { this.pane.activate(); }
const tab = this.tabForElement(event.target);
if (!tab) { return; }
if ((event.button === 2) || ((event.button === 0) && (event.ctrlKey === true))) {
if (this.rightClickedTab) {
this.rightClickedTab.element.classList.remove('right-clicked');
}
this.rightClickedTab = tab;
this.rightClickedTab.element.classList.add('right-clicked');
return event.preventDefault();
} else if (event.button === 1) {
// This prevents Chromium from activating "scroll mode" when
// middle-clicking while some tabs are off-screen.
return event.preventDefault();
}
}
onClick(event) {
const tab = this.tabForElement(event.target);
if (!tab) {
return;
}
event.preventDefault();
if ((event.button === 2) || ((event.button === 0) && (event.ctrlKey === true))) {
// Bail out early when receiving this event, because we have already
// handled it in the mousedown handler.
return;
} else if ((event.button === 0) && !event.target.classList.contains('close-icon')) {
return this.pane.activateItem(tab.item);
} else if (event.button === 1) {
return this.pane.destroyItem(tab.item);
}
}
onDoubleClick(event) {
let tab = this.tabForElement(event.target);
if (tab) {
return (typeof tab.item.terminatePendingState === 'function' ? tab.item.terminatePendingState() : undefined);
} else if (event.target === this.element) {
atom.commands.dispatch(this.element, 'application:new-file');
return event.preventDefault();
}
}
updateTabScrollingThreshold(value) {
return this.tabScrollingThreshold = value;
}
updateTabScrolling(value) {
if (value === 'platform') {
return this.tabScrolling = (process.platform === 'linux');
} else {
return this.tabScrolling = value;
}
}
browserWindowForId(id) {
if (BrowserWindow == null) { ({
BrowserWindow
} = require('electron').remote); }
return BrowserWindow.fromId(id);
}
moveItemBetweenPanes(fromPane, fromIndex, toPane, toIndex, item) {
try {
if (toPane === fromPane) {
if (fromIndex < toIndex) { toIndex--; }
toPane.moveItem(item, toIndex);
} else {
this.isItemMovingBetweenPanes = true;
fromPane.moveItemToPane(item, toPane, toIndex--);
}
toPane.activateItem(item);
return toPane.activate();
} finally {
this.isItemMovingBetweenPanes = false;
}
}
removeDropTargetClasses() {
let dropTarget;
const workspaceElement = atom.workspace.getElement();
for (dropTarget of workspaceElement.querySelectorAll('.tab-bar .is-drop-target')) {
dropTarget.classList.remove('is-drop-target');
}
return (() => {
const result = [];
for (dropTarget of workspaceElement.querySelectorAll('.tab-bar .drop-target-is-after')) {
result.push(dropTarget.classList.remove('drop-target-is-after'));
}
return result;
})();
}
getDropTargetIndex(event) {
const { target } = event;
if (this.isPlaceholder(target)) {
return;
}
const tabs = this.getTabs();
let tab = this.tabForElement(target);
if (tab == null) {
tab = tabs[tabs.length - 1];
}
if (tab == null) {
return 0;
}
const { left, width } = tab.element.getBoundingClientRect();
const elementCenter = left + (width / 2);
const elementIndex = tabs.indexOf(tab);
if (event.pageX < elementCenter) {
return elementIndex;
} else {
return elementIndex + 1;
}
}
getPlaceholder() {
if (this.placeholderEl != null) {
return this.placeholderEl;
}
this.placeholderEl = document.createElement("li");
this.placeholderEl.classList.add("placeholder");
return this.placeholderEl;
}
removePlaceholder() {
if (this.placeholderEl != null) {
this.placeholderEl.remove();
}
return this.placeholderEl = null;
}
isPlaceholder(element) {
return element.classList.contains('placeholder');
}
onMouseEnter() {
for (let tab of this.getTabs()) {
const { width } = tab.element.getBoundingClientRect();
tab.element.style.maxWidth = width.toFixed(2) + 'px';
}
}
onMouseLeave() {
for (let tab of this.getTabs()) {
tab.element.style.maxWidth = '';
}
}
tabForElement(element) {
let currentElement = element;
while (currentElement != null) {
if (this.tabsByElement.get(currentElement)) {
return this.tabsByElement.get(currentElement);
} else {
currentElement = currentElement.parentElement;
}
}
}
isAtomTabEvent(event) {
for (let item of event.dataTransfer.items) {
if (item.type === 'atom-tab-event') {
return true;
}
}
return false;
}
itemIsAllowed(event, location) {
for (let item of event.dataTransfer.items) {
if ((item.type === 'allow-all-locations') || (item.type === `allowed-location-${location}`)) {
return true;
}
}
return false;
}
}
module.exports = TabBarView;

View File

@ -0,0 +1,296 @@
path = require 'path'
{Disposable, CompositeDisposable} = require 'atom'
getIconServices = require './get-icon-services'
layout = require './layout'
module.exports =
class TabView
constructor: ({@item, @pane, didClickCloseIcon, @tabs, location}) ->
if typeof @item.getPath is 'function'
@path = @item.getPath()
@element = document.createElement('li')
@element.setAttribute('is', 'tabs-tab')
if ['TextEditor', 'TestView'].indexOf(@item.constructor.name) > -1
@element.classList.add('texteditor')
@element.classList.add('tab', 'sortable')
@itemTitle = document.createElement('div')
@itemTitle.classList.add('title')
@element.appendChild(@itemTitle)
if location is 'center' or not @item.isPermanentDockItem?()
closeIcon = document.createElement('div')
closeIcon.classList.add('close-icon')
closeIcon.onclick = didClickCloseIcon
@element.appendChild(closeIcon)
@subscriptions = new CompositeDisposable()
@handleEvents()
@updateDataAttributes()
@updateTitle()
@updateIcon()
@updateModifiedStatus()
@setupTooltip()
if @isItemPending()
@itemTitle.classList.add('temp')
@element.classList.add('pending-tab')
@element.ondrag = (e) -> layout.drag e
@element.ondragend = (e) -> layout.end e
@element.pane = @pane
@element.item = @item
@element.itemTitle = @itemTitle
@element.path = @path
handleEvents: ->
titleChangedHandler = =>
@updateTitle()
@subscriptions.add @pane.onDidDestroy => @destroy()
@subscriptions.add @pane.onItemDidTerminatePendingState (item) =>
@clearPending() if item is @item
if typeof @item.onDidChangeTitle is 'function'
onDidChangeTitleDisposable = @item.onDidChangeTitle(titleChangedHandler)
if Disposable.isDisposable(onDidChangeTitleDisposable)
@subscriptions.add(onDidChangeTitleDisposable)
else
console.warn "::onDidChangeTitle does not return a valid Disposable!", @item
else if typeof @item.on is 'function'
#TODO Remove once old events are no longer supported
@item.on('title-changed', titleChangedHandler)
@subscriptions.add dispose: =>
@item.off?('title-changed', titleChangedHandler)
pathChangedHandler = (@path) =>
@updateDataAttributes()
@updateTitle()
@updateTooltip()
@updateIcon()
if typeof @item.onDidChangePath is 'function'
onDidChangePathDisposable = @item.onDidChangePath(pathChangedHandler)
if Disposable.isDisposable(onDidChangePathDisposable)
@subscriptions.add(onDidChangePathDisposable)
else
console.warn "::onDidChangePath does not return a valid Disposable!", @item
else if typeof @item.on is 'function'
#TODO Remove once old events are no longer supported
@item.on('path-changed', pathChangedHandler)
@subscriptions.add dispose: =>
@item.off?('path-changed', pathChangedHandler)
iconChangedHandler = =>
@updateIcon()
@subscriptions.add getIconServices().onDidChange => @updateIcon()
if typeof @item.onDidChangeIcon is 'function'
onDidChangeIconDisposable = @item.onDidChangeIcon? =>
@updateIcon()
if Disposable.isDisposable(onDidChangeIconDisposable)
@subscriptions.add(onDidChangeIconDisposable)
else
console.warn "::onDidChangeIcon does not return a valid Disposable!", @item
else if typeof @item.on is 'function'
#TODO Remove once old events are no longer supported
@item.on('icon-changed', iconChangedHandler)
@subscriptions.add dispose: =>
@item.off?('icon-changed', iconChangedHandler)
modifiedHandler = =>
@updateModifiedStatus()
if typeof @item.onDidChangeModified is 'function'
onDidChangeModifiedDisposable = @item.onDidChangeModified(modifiedHandler)
if Disposable.isDisposable(onDidChangeModifiedDisposable)
@subscriptions.add(onDidChangeModifiedDisposable)
else
console.warn "::onDidChangeModified does not return a valid Disposable!", @item
else if typeof @item.on is 'function'
#TODO Remove once old events are no longer supported
@item.on('modified-status-changed', modifiedHandler)
@subscriptions.add dispose: =>
@item.off?('modified-status-changed', modifiedHandler)
if typeof @item.onDidSave is 'function'
onDidSaveDisposable = @item.onDidSave (event) =>
@terminatePendingState()
if event.path isnt @path
@path = event.path
@setupVcsStatus() if atom.config.get 'tabs.enableVcsColoring'
if Disposable.isDisposable(onDidSaveDisposable)
@subscriptions.add(onDidSaveDisposable)
else
console.warn "::onDidSave does not return a valid Disposable!", @item
@subscriptions.add atom.config.observe 'tabs.showIcons', =>
@updateIconVisibility()
@subscriptions.add atom.config.observe 'tabs.enableVcsColoring', (isEnabled) =>
if isEnabled and @path? then @setupVcsStatus() else @unsetVcsStatus()
setupTooltip: ->
# Defer creating the tooltip until the tab is moused over
onMouseEnter = =>
@mouseEnterSubscription.dispose()
@hasBeenMousedOver = true
@updateTooltip()
# Trigger again so the tooltip shows
@element.dispatchEvent(new CustomEvent('mouseenter', bubbles: true))
@mouseEnterSubscription = dispose: =>
@element.removeEventListener('mouseenter', onMouseEnter)
@mouseEnterSubscription = null
@element.addEventListener('mouseenter', onMouseEnter)
updateTooltip: ->
return unless @hasBeenMousedOver
@destroyTooltip()
if @path
@tooltip = atom.tooltips.add @element,
title: @path
html: false
delay:
show: 1000
hide: 100
placement: 'bottom'
destroyTooltip: ->
return unless @hasBeenMousedOver
@tooltip?.dispose()
destroy: ->
@subscriptions?.dispose()
@mouseEnterSubscription?.dispose()
@repoSubscriptions?.dispose()
@destroyTooltip()
@element.remove()
updateDataAttributes: ->
if @path
@itemTitle.dataset.name = path.basename(@path)
@itemTitle.dataset.path = @path
else
delete @itemTitle.dataset.name
delete @itemTitle.dataset.path
if itemClass = @item.constructor?.name
@element.dataset.type = itemClass
else
delete @element.dataset.type
updateTitle: ({updateSiblings, useLongTitle}={}) ->
return if @updatingTitle
@updatingTitle = true
if updateSiblings is false
title = @item.getTitle()
title = @item.getLongTitle?() ? title if useLongTitle
@itemTitle.textContent = title
else
title = @item.getTitle()
useLongTitle = false
for tab in @tabs when tab isnt this
if tab.item.getTitle() is title
tab.updateTitle(updateSiblings: false, useLongTitle: true)
useLongTitle = true
title = @item.getLongTitle?() ? title if useLongTitle
@itemTitle.textContent = title
@updatingTitle = false
updateIcon: ->
getIconServices().updateTabIcon(this)
isItemPending: ->
if @pane.getPendingItem?
@pane.getPendingItem() is @item
else if @item.isPending?
@item.isPending()
terminatePendingState: ->
if @pane.clearPendingItem?
@pane.clearPendingItem() if @pane.getPendingItem() is @item
else if @item.terminatePendingState?
@item.terminatePendingState()
clearPending: ->
@itemTitle.classList.remove('temp')
@element.classList.remove('pending-tab')
updateIconVisibility: ->
if atom.config.get 'tabs.showIcons'
@itemTitle.classList.remove('hide-icon')
else
@itemTitle.classList.add('hide-icon')
updateModifiedStatus: ->
if @item.isModified?()
@element.classList.add('modified') unless @isModified
@isModified = true
else
@element.classList.remove('modified') if @isModified
@isModified = false
setupVcsStatus: ->
return unless @path?
@repoForPath(@path).then (repo) =>
@subscribeToRepo(repo)
@updateVcsStatus(repo)
# Subscribe to the project's repo for changes to the VCS status of the file.
subscribeToRepo: (repo) ->
return unless repo?
# Remove previous repo subscriptions.
@repoSubscriptions?.dispose()
@repoSubscriptions = new CompositeDisposable()
@repoSubscriptions.add repo.onDidChangeStatus (event) =>
@updateVcsStatus(repo, event.pathStatus) if event.path is @path
@repoSubscriptions.add repo.onDidChangeStatuses =>
@updateVcsStatus(repo)
repoForPath: ->
for dir in atom.project.getDirectories()
return atom.project.repositoryForDirectory(dir) if dir.contains @path
Promise.resolve(null)
# Update the VCS status property of this tab using the repo.
updateVcsStatus: (repo, status) ->
return unless repo?
newStatus = null
if repo.isPathIgnored(@path)
newStatus = 'ignored'
else
status = repo.getCachedPathStatus(@path) unless status?
if repo.isStatusModified(status)
newStatus = 'modified'
else if repo.isStatusNew(status)
newStatus = 'added'
if newStatus isnt @status
@status = newStatus
@updateVcsColoring()
updateVcsColoring: ->
@itemTitle.classList.remove('status-ignored', 'status-modified', 'status-added')
if @status and atom.config.get 'tabs.enableVcsColoring'
@itemTitle.classList.add("status-#{@status}")
unsetVcsStatus: ->
@repoSubscriptions?.dispose()
delete @status
@updateVcsColoring()

View File

@ -1,401 +0,0 @@
const path = require('path');
const {Disposable, CompositeDisposable} = require('atom');
const getIconServices = require('./get-icon-services');
const layout = require('./layout');
class TabView {
constructor({item, pane, didClickCloseIcon, tabs, location}) {
this.item = item;
this.pane = pane;
this.tabs = tabs;
if (typeof this.item.getPath === 'function') {
this.path = this.item.getPath();
}
this.element = document.createElement('li');
this.element.setAttribute('is', 'tabs-tab');
if (['TextEditor', 'TestView'].indexOf(this.item.constructor.name) > -1) {
this.element.classList.add('texteditor');
}
this.element.classList.add('tab', 'sortable');
this.itemTitle = document.createElement('div');
this.itemTitle.classList.add('title');
this.element.appendChild(this.itemTitle);
if ((location === 'center') || !(typeof this.item.isPermanentDockItem === 'function' ? this.item.isPermanentDockItem() : undefined)) {
const closeIcon = document.createElement('div');
closeIcon.classList.add('close-icon');
closeIcon.onclick = didClickCloseIcon;
this.element.appendChild(closeIcon);
}
this.subscriptions = new CompositeDisposable();
this.handleEvents();
this.updateDataAttributes();
this.updateTitle();
this.updateIcon();
this.updateModifiedStatus();
this.setupTooltip();
if (this.isItemPending()) {
this.itemTitle.classList.add('temp');
this.element.classList.add('pending-tab');
}
this.element.ondrag = e => layout.drag(e);
this.element.ondragend = e => layout.end(e);
this.element.pane = this.pane;
this.element.item = this.item;
this.element.itemTitle = this.itemTitle;
this.element.path = this.path;
}
handleEvents() {
const titleChangedHandler = () => {
return this.updateTitle();
};
this.subscriptions.add(this.pane.onDidDestroy(() => this.destroy()));
this.subscriptions.add(this.pane.onItemDidTerminatePendingState(item => {
if (item === this.item) { return this.clearPending(); }
})
);
if (typeof this.item.onDidChangeTitle === 'function') {
const onDidChangeTitleDisposable = this.item.onDidChangeTitle(titleChangedHandler);
if (Disposable.isDisposable(onDidChangeTitleDisposable)) {
this.subscriptions.add(onDidChangeTitleDisposable);
} else {
console.warn("::onDidChangeTitle does not return a valid Disposable!", this.item);
}
} else if (typeof this.item.on === 'function') {
//TODO Remove once old events are no longer supported
this.item.on('title-changed', titleChangedHandler);
this.subscriptions.add({dispose: () => {
return (typeof this.item.off === 'function' ? this.item.off('title-changed', titleChangedHandler) : undefined);
}
});
}
const pathChangedHandler = path1 => {
this.path = path1;
this.updateDataAttributes();
this.updateTitle();
this.updateTooltip();
return this.updateIcon();
};
if (typeof this.item.onDidChangePath === 'function') {
const onDidChangePathDisposable = this.item.onDidChangePath(pathChangedHandler);
if (Disposable.isDisposable(onDidChangePathDisposable)) {
this.subscriptions.add(onDidChangePathDisposable);
} else {
console.warn("::onDidChangePath does not return a valid Disposable!", this.item);
}
} else if (typeof this.item.on === 'function') {
//TODO Remove once old events are no longer supported
this.item.on('path-changed', pathChangedHandler);
this.subscriptions.add({dispose: () => {
return (typeof this.item.off === 'function' ? this.item.off('path-changed', pathChangedHandler) : undefined);
}
});
}
const iconChangedHandler = () => {
return this.updateIcon();
};
this.subscriptions.add(getIconServices().onDidChange(() => this.updateIcon()));
if (typeof this.item.onDidChangeIcon === 'function') {
const onDidChangeIconDisposable = typeof this.item.onDidChangeIcon === 'function' ? this.item.onDidChangeIcon(() => {
return this.updateIcon();
}) : undefined;
if (Disposable.isDisposable(onDidChangeIconDisposable)) {
this.subscriptions.add(onDidChangeIconDisposable);
} else {
console.warn("::onDidChangeIcon does not return a valid Disposable!", this.item);
}
} else if (typeof this.item.on === 'function') {
//TODO Remove once old events are no longer supported
this.item.on('icon-changed', iconChangedHandler);
this.subscriptions.add({dispose: () => {
return (typeof this.item.off === 'function' ? this.item.off('icon-changed', iconChangedHandler) : undefined);
}
});
}
const modifiedHandler = () => {
return this.updateModifiedStatus();
};
if (typeof this.item.onDidChangeModified === 'function') {
const onDidChangeModifiedDisposable = this.item.onDidChangeModified(modifiedHandler);
if (Disposable.isDisposable(onDidChangeModifiedDisposable)) {
this.subscriptions.add(onDidChangeModifiedDisposable);
} else {
console.warn("::onDidChangeModified does not return a valid Disposable!", this.item);
}
} else if (typeof this.item.on === 'function') {
//TODO Remove once old events are no longer supported
this.item.on('modified-status-changed', modifiedHandler);
this.subscriptions.add({dispose: () => {
return (typeof this.item.off === 'function' ? this.item.off('modified-status-changed', modifiedHandler) : undefined);
}
});
}
if (typeof this.item.onDidSave === 'function') {
const onDidSaveDisposable = this.item.onDidSave(event => {
this.terminatePendingState();
if (event.path !== this.path) {
this.path = event.path;
if (atom.config.get('tabs.enableVcsColoring')) { return this.setupVcsStatus(); }
}
});
if (Disposable.isDisposable(onDidSaveDisposable)) {
this.subscriptions.add(onDidSaveDisposable);
} else {
console.warn("::onDidSave does not return a valid Disposable!", this.item);
}
}
this.subscriptions.add(atom.config.observe('tabs.showIcons', () => {
return this.updateIconVisibility();
})
);
return this.subscriptions.add(atom.config.observe('tabs.enableVcsColoring', isEnabled => {
if (isEnabled && (this.path != null)) { return this.setupVcsStatus(); } else { return this.unsetVcsStatus(); }
})
);
}
setupTooltip() {
// Defer creating the tooltip until the tab is moused over
const onMouseEnter = () => {
this.mouseEnterSubscription.dispose();
this.hasBeenMousedOver = true;
this.updateTooltip();
// Trigger again so the tooltip shows
return this.element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: true}));
};
this.mouseEnterSubscription = { dispose: () => {
this.element.removeEventListener('mouseenter', onMouseEnter);
return this.mouseEnterSubscription = null;
}
};
return this.element.addEventListener('mouseenter', onMouseEnter);
}
updateTooltip() {
if (!this.hasBeenMousedOver) { return; }
this.destroyTooltip();
if (this.path) {
return this.tooltip = atom.tooltips.add(this.element, {
title: this.path,
html: false,
delay: {
show: 1000,
hide: 100
},
placement: 'bottom'
}
);
}
}
destroyTooltip() {
if (!this.hasBeenMousedOver) { return; }
return (this.tooltip != null ? this.tooltip.dispose() : undefined);
}
destroy() {
if (this.subscriptions != null) {
this.subscriptions.dispose();
}
if (this.mouseEnterSubscription != null) {
this.mouseEnterSubscription.dispose();
}
if (this.repoSubscriptions != null) {
this.repoSubscriptions.dispose();
}
this.destroyTooltip();
return this.element.remove();
}
updateDataAttributes() {
let itemClass;
if (this.path) {
this.itemTitle.dataset.name = path.basename(this.path);
this.itemTitle.dataset.path = this.path;
} else {
delete this.itemTitle.dataset.name;
delete this.itemTitle.dataset.path;
}
if ((itemClass = this.item.constructor != null ? this.item.constructor.name : undefined)) {
return this.element.dataset.type = itemClass;
} else {
return delete this.element.dataset.type;
}
}
updateTitle(param) {
let title;
if (param == null) { param = {}; }
let {updateSiblings, useLongTitle} = param;
if (this.updatingTitle) { return; }
this.updatingTitle = true;
if (updateSiblings === false) {
title = this.item.getTitle();
if (useLongTitle) { let left;
title = (left = (typeof this.item.getLongTitle === 'function' ? this.item.getLongTitle() : undefined)) != null ? left : title; }
this.itemTitle.textContent = title;
} else {
title = this.item.getTitle();
useLongTitle = false;
for (let tab of this.tabs) {
if (tab !== this) {
if (tab.item.getTitle() === title) {
tab.updateTitle({updateSiblings: false, useLongTitle: true});
useLongTitle = true;
}
}
}
if (useLongTitle) { let left1;
title = (left1 = (typeof this.item.getLongTitle === 'function' ? this.item.getLongTitle() : undefined)) != null ? left1 : title; }
this.itemTitle.textContent = title;
}
return this.updatingTitle = false;
}
updateIcon() {
return getIconServices().updateTabIcon(this);
}
isItemPending() {
if (this.pane.getPendingItem != null) {
return this.pane.getPendingItem() === this.item;
} else if (this.item.isPending != null) {
return this.item.isPending();
}
}
terminatePendingState() {
if (this.pane.clearPendingItem != null) {
if (this.pane.getPendingItem() === this.item) { return this.pane.clearPendingItem(); }
} else if (this.item.terminatePendingState != null) {
return this.item.terminatePendingState();
}
}
clearPending() {
this.itemTitle.classList.remove('temp');
return this.element.classList.remove('pending-tab');
}
updateIconVisibility() {
if (atom.config.get('tabs.showIcons')) {
return this.itemTitle.classList.remove('hide-icon');
} else {
return this.itemTitle.classList.add('hide-icon');
}
}
updateModifiedStatus() {
if (typeof this.item.isModified === 'function' ? this.item.isModified() : undefined) {
if (!this.isModified) { this.element.classList.add('modified'); }
return this.isModified = true;
} else {
if (this.isModified) { this.element.classList.remove('modified'); }
return this.isModified = false;
}
}
setupVcsStatus() {
if (this.path == null) { return; }
return this.repoForPath(this.path).then(repo => {
this.subscribeToRepo(repo);
return this.updateVcsStatus(repo);
});
}
// Subscribe to the project's repo for changes to the VCS status of the file.
subscribeToRepo(repo) {
if (repo == null) { return; }
// Remove previous repo subscriptions.
if (this.repoSubscriptions != null) {
this.repoSubscriptions.dispose();
}
this.repoSubscriptions = new CompositeDisposable();
this.repoSubscriptions.add(repo.onDidChangeStatus(event => {
if (event.path === this.path) { return this.updateVcsStatus(repo, event.pathStatus); }
})
);
return this.repoSubscriptions.add(repo.onDidChangeStatuses(() => {
return this.updateVcsStatus(repo);
})
);
}
repoForPath() {
for (let dir of atom.project.getDirectories()) {
if (dir.contains(this.path)) { return atom.project.repositoryForDirectory(dir); }
}
return Promise.resolve(null);
}
// Update the VCS status property of this tab using the repo.
updateVcsStatus(repo, status) {
if (repo == null) { return; }
let newStatus = null;
if (repo.isPathIgnored(this.path)) {
newStatus = 'ignored';
} else {
if (status == null) { status = repo.getCachedPathStatus(this.path); }
if (repo.isStatusModified(status)) {
newStatus = 'modified';
} else if (repo.isStatusNew(status)) {
newStatus = 'added';
}
}
if (newStatus !== this.status) {
this.status = newStatus;
return this.updateVcsColoring();
}
}
updateVcsColoring() {
this.itemTitle.classList.remove('status-ignored', 'status-modified', 'status-added');
if (this.status && atom.config.get('tabs.enableVcsColoring')) {
return this.itemTitle.classList.add(`status-${this.status}`);
}
}
unsetVcsStatus() {
if (this.repoSubscriptions != null) {
this.repoSubscriptions.dispose();
}
delete this.status;
return this.updateVcsColoring();
}
}
module.exports = TabView;