diff --git a/app/commands/commandBindings/index.js b/app/commands/commandBindings/index.js index e7534a3d..9d7c9a88 100644 --- a/app/commands/commandBindings/index.js +++ b/app/commands/commandBindings/index.js @@ -2,10 +2,12 @@ import git from './git' import file from './file' import misc from './misc' import editor from './editor' +import tab from './tab' export default { ...git, ...file, ...misc, - ...editor + ...editor, + ...tab } diff --git a/app/commands/commandBindings/tab.js b/app/commands/commandBindings/tab.js new file mode 100644 index 00000000..f6268a2d --- /dev/null +++ b/app/commands/commandBindings/tab.js @@ -0,0 +1,40 @@ +/* @flow weak */ +import store, { dispatch as $d } from '../../store' +import * as Tab from '../../components/Tab/actions' +import * as PaneActions from '../../components/Pane/actions' + +export default { + 'tab:close': c => { + $d(Tab.removeTab(c.context.id)) + }, + + 'tab:close_other': c => { + $d(Tab.removeOtherTab(c.context.id)) + }, + + 'tab:close_all': c => { + $d(Tab.removeAllTab(c.context.id)) + }, + + 'tab:split_v': c => { + const panes = store.getState().PaneState.panes + const pane = Object.values(panes).find((pane) => ( + pane.content && pane.content.type === 'tabGroup' && pane.content.id === c.context.tabGroupId + )) + $d(PaneActions.splitTo(pane.id, 'bottom')) + .then(newPaneId => { + $d(Tab.moveTabToPane(c.context.id, newPaneId)) + }) + }, + + 'tab:split_h': c => { + const panes = store.getState().PaneState.panes + const pane = Object.values(panes).find((pane) => ( + pane.content && pane.content.type === 'tabGroup' && pane.content.id === c.context.tabGroupId + )) + $d(PaneActions.splitTo(pane.id, 'right')) + .then(newPaneId => { + $d(Tab.moveTabToPane(c.context.id, newPaneId)) + }) + }, +} diff --git a/app/components/ContextMenu/index.jsx b/app/components/ContextMenu/index.jsx index 8c4c209e..ce50ee99 100644 --- a/app/components/ContextMenu/index.jsx +++ b/app/components/ContextMenu/index.jsx @@ -3,7 +3,7 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import cx from 'classnames' import Menu from '../Menu' -import {setContext} from '../../commands' +import {setContext} from '../../commands/dispatchCommand' const ContextMenu = (props) => { const {items, context, isActive, pos, deactivate} = props diff --git a/app/components/Tab/TabBar.jsx b/app/components/Tab/TabBar.jsx index 704d8120..d1ed91eb 100644 --- a/app/components/Tab/TabBar.jsx +++ b/app/components/Tab/TabBar.jsx @@ -5,7 +5,33 @@ import { dragStart } from '../DragAndDrop/actions'; import Menu from '../Menu' import * as TabActions from './actions'; import * as PaneActions from '../Pane/actions'; - +import ContextMenu from '../ContextMenu' + +const dividItem = { name: '-' } +const items = [ + { + name: 'Close', + icon: '', + command: 'tab:close' + }, { + name: 'Close Others', + icon: '', + command: 'tab:close_other' + }, { + name: 'Close All', + icon: '', + command: 'tab:close_all' + }, dividItem, + { + name: 'Vertical Split', + icon: '', + command: 'tab:split_v' + }, { + name: 'Horizontal Split', + icon: '', + command: 'tab:split_h' + } +] class _TabBar extends Component { constructor (props) { @@ -57,7 +83,7 @@ class _TabBar extends Component { } render () { - const { tabIds, tabGroupId, isRootPane, addTab, closePane, isDraggedOver } = this.props + const { tabIds, tabGroupId, isRootPane, addTab, closePane, isDraggedOver, contextMenu, closeContextMenu } = this.props return (
- + ) } @@ -87,16 +119,19 @@ const TabBar = connect((state, { tabIds, tabGroupId, containingPaneId }) => ({ isDraggedOver: state.DragAndDrop.meta ? state.DragAndDrop.meta.tabBarTargetId === `tab_bar_${tabGroupId}` : false, - isRootPane: state.PaneState.rootPaneId === containingPaneId + isRootPane: state.PaneState.rootPaneId === containingPaneId, + contextMenu: state.TabState.contextMenu }), (dispatch, { tabGroupId, containingPaneId }) => ({ activateTab: (tabId) => dispatch(TabActions.activateTab(tabId)), addTab: () => dispatch(TabActions.createTabInGroup(tabGroupId)), - closePane: () => dispatch(PaneActions.closePane(containingPaneId)) + closePane: () => dispatch(PaneActions.closePane(containingPaneId)), + openContextMenu: (e, node) => dispatch(TabActions.openContextMenu(e, node)), + closeContextMenu: () => dispatch(TabActions.closeContextMenu()) }) )(_TabBar) -const _TabLabel = ({tab, isActive, isDraggedOver, removeTab, activateTab, dragStart}) => { +const _TabLabel = ({tab, isActive, isDraggedOver, removeTab, activateTab, dragStart, openContextMenu}) => { const possibleStatus = { 'modified': '*', 'warning': '!', @@ -115,6 +150,7 @@ const _TabLabel = ({tab, isActive, isDraggedOver, removeTab, activateTab, dragSt onClick={e => activateTab(tab.id)} draggable='true' onDragStart={e => dragStart({sourceType: 'TAB', sourceId: tab.id})} + onContextMenu={e => openContextMenu(e, tab)} > {isDraggedOver ?
: null} {tab.icon ?
: null} @@ -133,6 +169,8 @@ _TabLabel.propTypes = { removeTab: PropTypes.func, activateTab: PropTypes.func, dragStart: PropTypes.func, + openContextMenu: PropTypes.func, + closeContextMenu: PropTypes.func } const TabLabel = connect((state, { tabId }) => { @@ -146,6 +184,8 @@ const TabLabel = connect((state, { tabId }) => { removeTab: (tabId) => dispatch(TabActions.removeTab(tabId)), activateTab: (tabId) => dispatch(TabActions.activateTab(tabId)), dragStart: (dragEventObj) => dispatch(dragStart(dragEventObj)), + openContextMenu: (e, node) => dispatch(TabActions.openContextMenu(e, node)), + closeContextMenu: () => dispatch(TabActions.closeContextMenu()) }) )(_TabLabel) diff --git a/app/components/Tab/actions.js b/app/components/Tab/actions.js index f99acd8f..e857807c 100644 --- a/app/components/Tab/actions.js +++ b/app/components/Tab/actions.js @@ -12,6 +12,12 @@ export const createTabInGroup = createAction(TAB_CREATE_IN_GROUP, (groupId, tab) export const TAB_REMOVE = 'TAB_REMOVE' export const removeTab = createAction(TAB_REMOVE, tabId => tabId) +export const TAB_REMOVE_OTHER = 'TAB_REMOVE_OTHER' +export const removeOtherTab = createAction(TAB_REMOVE_OTHER, tabId => tabId) + +export const TAB_REMOVE_ALL = 'TAB_REMOVE_ALL' +export const removeAllTab = createAction(TAB_REMOVE_ALL, tabId => tabId) + export const TAB_ACTIVATE = 'TAB_ACTIVATE' export const activateTab = createAction(TAB_ACTIVATE, tabId => tabId) @@ -62,3 +68,18 @@ export const TAB_INSERT_AT = 'TAB_INSERT_AT' export const insertTabAt = createAction(TAB_INSERT_AT, (tabId, beforeTabId) => ({tabId, beforeTabId}) ) + +export const TAB_CONTEXT_MENU_OPEN = 'TAB_CONTEXT_MENU_OPEN' +export const openContextMenu = createAction(TAB_CONTEXT_MENU_OPEN, (e, node) => { + e.stopPropagation() + e.preventDefault() + + return { + isActive: true, + pos: { x: e.clientX, y: e.clientY }, + contextNode: node, + } +}) + +export const TAB_CONTEXT_MENU_CLOSE = 'TAB_CONTEXT_MENU_CLOSE' +export const closeContextMenu = createAction(TAB_CONTEXT_MENU_CLOSE) diff --git a/app/components/Tab/reducer.js b/app/components/Tab/reducer.js index 6953cd4a..ef1bf442 100644 --- a/app/components/Tab/reducer.js +++ b/app/components/Tab/reducer.js @@ -14,7 +14,11 @@ import { TAB_UPDATE_BY_PATH, TAB_MOVE_TO_GROUP, TAB_MOVE_TO_PANE, - TAB_INSERT_AT + TAB_INSERT_AT, + TAB_CONTEXT_MENU_OPEN, + TAB_CONTEXT_MENU_CLOSE, + TAB_REMOVE_OTHER, + TAB_REMOVE_ALL } from './actions' import { @@ -57,7 +61,12 @@ import { const defaultState = { tabGroups: {}, tabs: {}, - activeTabGroupId: '' + activeTabGroupId: '', + contextMenu: { + isActive: false, + pos: { x: 0, y: 0 }, + contextNode: null, + } } const Tab = Model({ @@ -113,6 +122,36 @@ const _removeTab = (state, tab) => { }) } +const _removeOtherTab = (state, tab) => { + let nextState = state + nextState = _activateTab(state, tab) + const tabGroup = state.tabGroups[tab.tabGroupId] + const tabIdsToBeDeleted = _.without(tabGroup.tabIds, tab.id) + const nextTabs = _.omit(state.tabs, tabIdsToBeDeleted) + return update(nextState, { + tabGroups: { + [tab.tabGroupId]: { + tabIds: { $set: [tab.id] } + } + }, + tabs: { $set: nextTabs } + }) +} + +const _removeAllTab = (state, tab) => { + const tabGroup = state.tabGroups[tab.tabGroupId] + const tabIdsToBeDeleted = tabGroup.tabIds + const nextTabs = _.omit(state.tabs, tabIdsToBeDeleted) + return update(state, { + tabGroups: { + [tab.tabGroupId]: { + tabIds: { $set: [] } + } + }, + tabs: { $set: nextTabs } + }) +} + const _moveTabToGroup = (state, tab, tabGroup, insertIndex = tabGroup.tabIds.length) => { // 1. remove it from original group let nextState = _removeTab(state, tab) @@ -193,6 +232,16 @@ const TabReducer = handleActions({ return _removeTab(state, tab) }, + [TAB_REMOVE_OTHER]: (state, action) => { + const tab = state.tabs[action.payload] + return _removeOtherTab(state, tab) + }, + + [TAB_REMOVE_ALL]: (state, action) => { + const tab = state.tabs[action.payload] + return _removeAllTab(state, tab) + }, + [TAB_ACTIVATE]: (state, action) => { const tab = state.tabs[action.payload] let nextState = _activateTab(state, tab) @@ -261,7 +310,21 @@ const TabReducer = handleActions({ const { targetTabGroupId, sourceTabGroupId } = action.payload if (!targetTabGroupId) return update(state, {tabGroups: {$delete: sourceTabGroupId}}) return _mergeTabGroups(state, targetTabGroupId, sourceTabGroupId) - } + }, + + [TAB_CONTEXT_MENU_OPEN]: (state, action) => ( + update(state, { + contextMenu: { $merge: action.payload } + }) + ), + + [TAB_CONTEXT_MENU_CLOSE]: (state, action) => ( + update(state, { + contextMenu: { $merge: { + isActive: false + } } + }) + ), }, defaultState)