├── .gitignore
├── .travis.yml
├── LICENSE.md
├── README.md
├── icon.png
├── keymaps
└── expose.cson
├── lib
├── expose-tab-view.coffee
├── expose-view.coffee
├── expose.coffee
└── file-icons.coffee
├── menus
└── expose.cson
├── package.json
├── screenshots
└── preview.png
├── spec
├── expose-spec.coffee
├── expose-tab-view-spec.coffee
├── expose-view-spec.coffee
└── fixtures
│ ├── archive.zip
│ ├── sample1.txt
│ ├── sample2.txt
│ └── sample3.txt
└── styles
├── expose.less
└── themes.less
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 | node_modules
4 | ss.png
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: objective-c
2 |
3 | notifications:
4 | email: false
5 |
6 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh'
7 |
8 | env:
9 | - APM_TEST_PACKAGES="minimap"
10 |
11 | git:
12 | depth: 10
13 |
14 | branches:
15 | only:
16 | - master
17 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Martin Rodalgaard
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Atom Tabs Exposé [](https://travis-ci.org/mrodalgaard/atom-expose)
2 |
3 | > DEPRECATED: This package will no longer be maintained by me since I have switched permanently to VSCode.
4 |
5 | Quick tab overview of open files. Similar to Mac OSX Exposé / Mission Control, Firefox Tab Group, Safari and Chrome Tab Overview, etc.
6 |
7 | * Shows active tab, panes and previews.
8 | * The text editor preview is taken from [Minimap](https://github.com/atom-minimap/minimap) if present, else a suitable file icon is used.
9 | * Shows file icons in tabs if the [file-icon package](https://github.com/DanBrooker/file-icons) is installed.
10 |
11 | 
12 |
13 | ## Keymaps
14 |
15 | Toggle expose with cmd-shift-e on Mac OSX and alt-shift-e on Linux and Windows.
16 |
17 | ESC and Enter closes the expose panel.
18 |
19 | 1 - 9 jumps to tabs.
20 |
21 | ← and → select tabs.
22 |
23 | Just start typing to search among the shown tabs.
24 |
25 | ## Tasks
26 |
27 | - [x] Basic exposé functionality
28 | - [x] Activate and close tabs
29 | - [x] Add tests
30 | - [x] Add detailed unit tests
31 | - [x] Tab preview images with fallback
32 | - [x] Show preview of images
33 | - [x] Tab icons
34 | - [x] Custom icons for more view classes
35 | - [x] Keyboard shortcuts (e.g. numbers)
36 | - [x] Move / sortable tabs
37 | - [x] Stay updated on workspace changes
38 | - [x] Show active tab and navigate with arrows
39 | - [ ] Show all project files options
40 |
41 | > Contributions, bug reports and feature requests are very welcome.
42 |
43 | > _- Martin_
44 |
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrodalgaard/atom-expose/1056113a8fd45ef0cc3fd2419ba6a223f1f71f46/icon.png
--------------------------------------------------------------------------------
/keymaps/expose.cson:
--------------------------------------------------------------------------------
1 | '.platform-darwin atom-workspace atom-text-editor:not([mini]), .platform-darwin atom-workspace':
2 | 'cmd-shift-e': 'expose:toggle'
3 |
4 | '.platform-win32 atom-workspace, .platform-linux atom-workspace':
5 | 'alt-shift-e': 'expose:toggle'
6 |
7 | '.expose-view':
8 | '1': 'expose:activate-1'
9 | '2': 'expose:activate-2'
10 | '3': 'expose:activate-3'
11 | '4': 'expose:activate-4'
12 | '5': 'expose:activate-5'
13 | '6': 'expose:activate-6'
14 | '7': 'expose:activate-7'
15 | '8': 'expose:activate-8'
16 | '9': 'expose:activate-9'
17 | 'tab': 'core:move-right'
18 | 'shift-tab': 'core:move-left'
19 |
--------------------------------------------------------------------------------
/lib/expose-tab-view.coffee:
--------------------------------------------------------------------------------
1 | {View, $$} = require 'atom-space-pen-views'
2 | {CompositeDisposable} = require 'atom'
3 |
4 | FileIcons = require './file-icons'
5 |
6 | module.exports =
7 | class ExposeView extends View
8 | @content: (title, color) ->
9 | @div click: 'activateTab', class: 'expose-tab', =>
10 | @div class: 'tab-header', =>
11 | @div outlet: 'itemTitle', 'data-name': title, title
12 | @div click: 'closeTab', class: 'close-icon icon-x'
13 | @div outlet: 'tabBody', class: 'tab-body', style: "border-color: #{color}"
14 |
15 | constructor: (@item = {}, @color = '#000') ->
16 | @title = @getItemTitle()
17 | super(@title, @color)
18 |
19 | initialize: ->
20 | @disposables = new CompositeDisposable
21 | @handleEvents()
22 | @populateTabBody()
23 | @updateIcon()
24 |
25 | handleEvents: ->
26 | @on 'click', '.icon-sync', @refreshTab
27 |
28 | @disposables.add atom.commands.add @element,
29 | 'expose:close-tab': (e) => @closeTab(e)
30 |
31 | @disposables.add atom.workspace.observeActivePaneItem @toggleActive
32 |
33 | destroy: ->
34 | @remove()
35 | @disposables?.dispose()
36 |
37 | populateTabBody: ->
38 | return if @drawImage()
39 | return if @drawMinimap()
40 | @drawFallback()
41 |
42 | drawFallback: ->
43 | objectClass = @item.constructor.name
44 | iconClass = 'icon-' + @item.getIconName() if @item.getIconName
45 | @tabBody.html $$ ->
46 | @a class: iconClass or switch objectClass
47 | when 'TextEditor' then 'icon-file-code'
48 | when 'ArchiveEditor' then 'icon-file-zip'
49 | when 'ArchiveEditorView' then 'icon-file-zip'
50 | else 'icon-file-text'
51 |
52 | drawImage: ->
53 | return unless @item.constructor.name is 'ImageEditor'
54 | filePath = @item.file.path
55 | @tabBody.html $$ ->
56 | @img src: filePath
57 |
58 | drawMinimap: ->
59 | return unless @item.constructor.name is 'TextEditor'
60 | return unless atom.packages.loadedPackages.minimap
61 |
62 | atom.packages.serviceHub.consume 'minimap', '1.0.0', (minimapAPI) =>
63 | if minimapAPI.standAloneMinimapForEditor?
64 | minimap = minimapAPI.standAloneMinimapForEditor(@item)
65 | minimapElement = atom.views.getView(minimap)
66 | minimapElement.style.cssText = '''
67 | width: 190px;
68 | height: 130px;
69 | left: 10px;
70 | pointer-events: none;
71 | position: absolute;
72 | '''
73 |
74 | minimap.setCharWidth?(2)
75 | minimap.setCharHeight?(4)
76 | minimap.setInterline?(2)
77 |
78 | @tabBody.html minimapElement
79 | else
80 | @tabBody.html $$ ->
81 | @a class: 'icon-sync'
82 |
83 | refreshTab: (event) =>
84 | event.stopPropagation()
85 | event.target.className += ' animate'
86 | atom.workspace.paneForItem(@item).activateItem(@item)
87 | setTimeout (=> @populateTabBody()), 1000
88 |
89 | activateTab: ->
90 | pane = atom.workspace.paneForItem(@item)
91 | pane.activate()
92 | pane.activateItem(@item)
93 |
94 | toggleActive: (item) =>
95 | @toggleClass('active', item is @item)
96 |
97 | isActiveTab: ->
98 | atom.workspace.getActivePaneItem() is @item
99 |
100 | closeTab: (event) ->
101 | event?.stopPropagation()
102 | atom.workspace.paneForItem(@item).destroyItem(@item)
103 | @destroy()
104 |
105 | getItemTitle: ->
106 | return 'untitled' unless title = @item.getTitle?()
107 |
108 | for paneItem in atom.workspace.getPaneItems() when paneItem isnt @item
109 | if paneItem.getTitle() is title and @item.getLongTitle?
110 | title = @item.getLongTitle()
111 | title
112 |
113 | isItemPending: ->
114 | return false unless pane = atom.workspace.paneForItem(@item)
115 | if pane.getPendingItem?
116 | pane.getPendingItem() is @item
117 | else if @item.isPending?
118 | @item.isPending()
119 |
120 | updateIcon: ->
121 | classList = 'title '
122 | classList += 'pending ' if @isItemPending()
123 |
124 | if iconName = @item.getIconName?()
125 | classList += "icon-#{iconName}"
126 | else if path = @item.getPath?()
127 | if iconName = FileIcons.getService().iconClassForPath(path, 'expose')
128 | classList += iconName.join(' ')
129 |
130 | @itemTitle.attr('class', classList)
131 |
--------------------------------------------------------------------------------
/lib/expose-view.coffee:
--------------------------------------------------------------------------------
1 | {CompositeDisposable, TextBuffer} = require 'atom'
2 | {View, TextEditorView} = require 'atom-space-pen-views'
3 | {filter} = require 'fuzzaldrin'
4 | Sortable = require 'sortablejs'
5 |
6 | ExposeTabView = require './expose-tab-view'
7 |
8 | module.exports =
9 | class ExposeView extends View
10 | tabs: []
11 |
12 | @content: (searchBuffer) ->
13 | searchTextEditor = atom.workspace.buildTextEditor(
14 | mini: true
15 | tabLength: 2
16 | softTabs: true
17 | softWrapped: false
18 | buffer: searchBuffer
19 | placeholderText: 'Search tabs'
20 | )
21 |
22 | @div class: 'expose-view', tabindex: -1, =>
23 | @div class: 'expose-top input-block', =>
24 | @div class: 'input-block-item input-block-item--flex', =>
25 | @subview 'searchView', new TextEditorView(editor: searchTextEditor)
26 | @div class: 'input-block-item', =>
27 | @div class: 'btn-group', =>
28 | @button outlet: 'exposeSettings', class: 'btn icon-gear'
29 | @button class: 'btn icon-x'
30 |
31 | @div outlet: 'tabList', class: 'tab-list'
32 |
33 | constructor: () ->
34 | super @searchBuffer = new TextBuffer
35 |
36 | initialize: ->
37 | @disposables = new CompositeDisposable
38 | @handleEvents()
39 | @handleDrag()
40 |
41 | destroy: ->
42 | @remove()
43 | @disposables?.dispose()
44 |
45 | handleEvents: ->
46 | @exposeSettings.on 'click', ->
47 | atom.workspace.open 'atom://config/packages/expose'
48 |
49 | @searchView.on 'click', (event) ->
50 | event.stopPropagation()
51 |
52 | @searchView.getModel().onDidStopChanging =>
53 | @update() if @didIgnoreFirstChange
54 | @didIgnoreFirstChange = true
55 |
56 | # This event gets propagated from most element clicks on top
57 | @on 'click', (event) =>
58 | event.stopPropagation()
59 | @exposeHide()
60 |
61 | @disposables.add atom.commands.add @element,
62 | 'core:confirm': => @handleConfirm()
63 | 'core:cancel': => @exposeHide()
64 | 'core:move-right': => @nextTab()
65 | 'core:move-left': => @nextTab(-1)
66 | 'expose:close': => @exposeHide()
67 | 'expose:activate-1': => @handleNumberKey(1)
68 | 'expose:activate-2': => @handleNumberKey(2)
69 | 'expose:activate-3': => @handleNumberKey(3)
70 | 'expose:activate-4': => @handleNumberKey(4)
71 | 'expose:activate-5': => @handleNumberKey(5)
72 | 'expose:activate-6': => @handleNumberKey(6)
73 | 'expose:activate-7': => @handleNumberKey(7)
74 | 'expose:activate-8': => @handleNumberKey(8)
75 | 'expose:activate-9': => @handleNumberKey(9)
76 |
77 | @on 'keydown', (event) => @handleKeyEvent(event)
78 |
79 | @disposables.add atom.workspace.onDidAddPaneItem => @update()
80 | @disposables.add atom.workspace.onDidDestroyPaneItem => @update()
81 |
82 | handleDrag: ->
83 | Sortable.create(
84 | @tabList.context
85 | ghostClass: 'ghost'
86 | onEnd: (evt) => @moveTab(evt.oldIndex, evt.newIndex)
87 | )
88 |
89 | moveTab: (from, to) ->
90 | return unless fromItem = @tabs[from]?.item
91 | return unless toItem = @tabs[to]?.item
92 |
93 | fromPane = atom.workspace.paneForItem(fromItem)
94 | toPane = atom.workspace.paneForItem(toItem)
95 |
96 | toPaneIndex = 0
97 | for item, i in toPane.getItems()
98 | toPaneIndex = i if item is toItem
99 |
100 | fromPane.moveItemToPane(fromItem, toPane, toPaneIndex)
101 | @update(true)
102 |
103 | didChangeVisible: (@visible) ->
104 | if @visible
105 | @searchBuffer.setText('')
106 | @update()
107 | @focus()
108 | else
109 | atom.workspace.getActivePane().activate()
110 |
111 | getGroupColor: (n) ->
112 | colors = ['#3498db', '#e74c3c', '#2ecc71', '#9b59b6']
113 | colors[n % colors.length]
114 |
115 | update: (force) ->
116 | return unless @visible or force
117 | @removeTabs()
118 |
119 | @tabs = []
120 | for pane, i in @getPanes()
121 | color = @getGroupColor(i)
122 | for item in pane.getItems()
123 | @tabs.push new ExposeTabView(item, color)
124 |
125 | @renderTabs(@tabs = @filterTabs(@tabs))
126 |
127 | getPanes: ->
128 | atom.workspace.getCenter().getPanes()
129 |
130 | filterTabs: (tabs) ->
131 | text = @searchBuffer.getText()
132 | return tabs if text is ''
133 | filter(tabs, text, key: 'title')
134 |
135 | renderTabs: (tabs) ->
136 | for tab in tabs
137 | @tabList.append tab
138 |
139 | removeTabs: ->
140 | @tabList.empty()
141 | for tab in @tabs
142 | tab.destroy()
143 | @tabs = []
144 |
145 | activateTab: (n = 1) ->
146 | n = 1 if n < 1
147 | n = @tabs.length if n > 9 or n > @tabs.length
148 | @tabs[n-1]?.activateTab()
149 | @exposeHide()
150 |
151 | handleConfirm: ->
152 | if @isSearching() then @activateTab() else @exposeHide()
153 |
154 | handleNumberKey: (number) ->
155 | if @isSearching()
156 | @searchView.getModel().insertText(number.toString())
157 | else
158 | @activateTab(number)
159 |
160 | handleKeyEvent: (event) ->
161 | ignoredKeys = ['shift', 'control', 'alt', 'meta']
162 | @searchView.focus() if ignoredKeys.indexOf(event.key.toLowerCase()) is -1
163 |
164 | nextTab: (n = 1) ->
165 | for tabView, i in @tabs
166 | if tabView.isActiveTab()
167 | n = @tabs.length - 1 if i+n < 0
168 | nextTabView.activateTab() if nextTabView = @tabs[(i+n)%@tabs.length]
169 | return @focus()
170 |
171 | exposeHide: ->
172 | @didIgnoreFirstChange = false
173 | for tab in @tabs
174 | tab.destroy()
175 | for panel in atom.workspace.getModalPanels()
176 | panel.hide() if panel.className is 'expose-panel'
177 |
178 | isSearching: -> @searchView.hasClass('is-focused')
179 |
180 | updateFileIcons: ->
181 | for tab in @tabs
182 | tab.updateIcon()
183 |
--------------------------------------------------------------------------------
/lib/expose.coffee:
--------------------------------------------------------------------------------
1 | {CompositeDisposable, Disposable} = require 'atom'
2 |
3 | ExposeView = require './expose-view'
4 | FileIcons = require './file-icons'
5 |
6 | module.exports = Expose =
7 | exposeView: null
8 | modalPanel: null
9 |
10 | activate: ->
11 | @exposeView = new ExposeView
12 | @modalPanel = atom.workspace.addModalPanel(item: @exposeView, visible: false, className: 'expose-panel')
13 |
14 | @disposables = new CompositeDisposable
15 |
16 | @disposables.add @modalPanel.onDidChangeVisible (visible) =>
17 | @exposeView.didChangeVisible(visible)
18 |
19 | @disposables.add atom.commands.add 'atom-workspace',
20 | 'expose:toggle': => @toggle()
21 |
22 | deactivate: ->
23 | @exposeView.destroy()
24 | @modalPanel.destroy()
25 | @disposables.dispose()
26 |
27 | toggle: ->
28 | if @modalPanel.isVisible()
29 | @exposeView.exposeHide()
30 | else
31 | @modalPanel.show()
32 |
33 | consumeFileIcons: (service) ->
34 | FileIcons.setService(service)
35 | @exposeView.updateFileIcons()
36 | new Disposable =>
37 | FileIcons.resetService()
38 | @exposeView.updateFileIcons()
39 |
--------------------------------------------------------------------------------
/lib/file-icons.coffee:
--------------------------------------------------------------------------------
1 | class FileIcons
2 | constructor: ->
3 | @service = new DefaultFileIcons
4 |
5 | getService: ->
6 | @service
7 |
8 | resetService: ->
9 | @service = new DefaultFileIcons
10 |
11 | setService: (@service) ->
12 |
13 | class DefaultFileIcons
14 | iconClassForPath: (filePath) ->
15 | ''
16 |
17 | module.exports = new FileIcons
18 |
--------------------------------------------------------------------------------
/menus/expose.cson:
--------------------------------------------------------------------------------
1 | 'menu': [
2 | {
3 | 'label': 'Packages'
4 | 'submenu': [
5 | 'label': 'expose'
6 | 'submenu': [
7 | {
8 | 'label': 'Toggle'
9 | 'command': 'expose:toggle'
10 | }
11 | ]
12 | ]
13 | }
14 | ]
15 |
16 | 'context-menu':
17 | '.expose-view': [
18 | {label: 'Close Exposé', command: 'expose:close'}
19 | ]
20 | '.expose-tab': [
21 | {label: 'Close Tab', command: 'expose:close-tab'}
22 | ]
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "expose",
3 | "main": "./lib/expose",
4 | "version": "0.15.0",
5 | "description": "Quick tab overview of open files",
6 | "keywords": [
7 | "tab",
8 | "expose",
9 | "overview",
10 | "group"
11 | ],
12 | "activationCommands": {
13 | "atom-workspace": "expose:toggle"
14 | },
15 | "repository": "https://github.com/mrodalgaard/atom-expose",
16 | "license": "MIT",
17 | "engines": {
18 | "atom": ">=0.174.0 <2.0.0"
19 | },
20 | "dependencies": {
21 | "atom-space-pen-views": "^2.0.3",
22 | "fuzzaldrin": "^2.1.0",
23 | "sortablejs": "^1.3.0"
24 | },
25 | "consumedServices": {
26 | "atom.file-icons": {
27 | "versions": {
28 | "1.0.0": "consumeFileIcons"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/screenshots/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrodalgaard/atom-expose/1056113a8fd45ef0cc3fd2419ba6a223f1f71f46/screenshots/preview.png
--------------------------------------------------------------------------------
/spec/expose-spec.coffee:
--------------------------------------------------------------------------------
1 | Expose = require '../lib/expose'
2 |
3 | describe "Expose", ->
4 | [workspaceElement, activationPromise] = []
5 |
6 | beforeEach ->
7 | workspaceElement = atom.views.getView(atom.workspace)
8 | activationPromise = atom.packages.activatePackage('expose')
9 |
10 | describe "when the expose:toggle event is triggered", ->
11 | it "hides and shows the modal panel", ->
12 | expect(workspaceElement.querySelector('.expose-view')).not.toExist()
13 |
14 | atom.commands.dispatch workspaceElement, 'expose:toggle'
15 |
16 | waitsForPromise ->
17 | activationPromise
18 |
19 | runs ->
20 | expect(workspaceElement.querySelector('.expose-view')).toExist()
21 |
22 | exposeModule = atom.packages.loadedPackages['expose'].mainModule
23 | expect(exposeModule.modalPanel.isVisible()).toBe true
24 | atom.commands.dispatch workspaceElement, 'expose:toggle'
25 | expect(exposeModule.modalPanel.isVisible()).toBe false
26 |
27 | it "hides and shows the view", ->
28 | # This test shows you an integration test testing at the view level.
29 |
30 | # Attaching the workspaceElement to the DOM is required to allow the
31 | # `toBeVisible()` matchers to work. Anything testing visibility or focus
32 | # requires that the workspaceElement is on the DOM. Tests that attach the
33 | # workspaceElement to the DOM are generally slower than those off DOM.
34 | jasmine.attachToDOM(workspaceElement)
35 |
36 | expect(workspaceElement.querySelector('.expose-view')).not.toExist()
37 |
38 | atom.commands.dispatch workspaceElement, 'expose:toggle'
39 |
40 | waitsForPromise ->
41 | activationPromise
42 |
43 | runs ->
44 | exposeElement = workspaceElement.querySelector('.expose-view')
45 | expect(exposeElement).toBeVisible()
46 | atom.commands.dispatch workspaceElement, 'expose:toggle'
47 | expect(exposeElement).not.toBeVisible()
48 |
--------------------------------------------------------------------------------
/spec/expose-tab-view-spec.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 |
3 | ExposeTabView = require '../lib/expose-tab-view'
4 |
5 | describe "ExposeTabView", ->
6 | workspaceElement = null
7 |
8 | beforeEach ->
9 | workspaceElement = atom.views.getView(atom.workspace)
10 | atom.project.setPaths [path.join(__dirname, 'fixtures')]
11 |
12 | describe "populateTabBody()", ->
13 | it "can populate empty item", ->
14 | exposeTabView = new ExposeTabView
15 | expect(Object.getOwnPropertyNames(exposeTabView.item)).toHaveLength 0
16 | expect(exposeTabView.find('.title').text()).toBe 'untitled'
17 | expect(exposeTabView.tabBody.find('a')).toHaveLength 1
18 | expect(exposeTabView.tabBody.find('a').attr('class')).toContain 'text'
19 | expect(exposeTabView.isItemPending()).toBe false
20 |
21 | it "populates normal text editor", ->
22 | waitsForPromise ->
23 | atom.workspace.open 'sample1.txt'
24 | runs ->
25 | item = atom.workspace.getActivePaneItem()
26 | exposeTabView = new ExposeTabView(item)
27 |
28 | expect(exposeTabView.item).toBeDefined()
29 | expect(exposeTabView.title).toBe 'sample1.txt'
30 | expect(exposeTabView.tabBody.find('a')).toHaveLength 1
31 | expect(exposeTabView.tabBody.find('a').attr('class')).toContain 'code'
32 |
33 | it "populates image editor", ->
34 | waitsForPromise ->
35 | atom.packages.activatePackage 'image-view'
36 | atom.workspace.open '../../screenshots/preview.png'
37 | runs ->
38 | item = atom.workspace.getActivePaneItem()
39 | exposeTabView = new ExposeTabView(item)
40 |
41 | expect(exposeTabView.item).toBeDefined()
42 | expect(exposeTabView.title).toBe 'preview.png'
43 | expect(exposeTabView.tabBody.find('img')).toHaveLength 1
44 | expect(exposeTabView.tabBody.find('img').attr('src')).toBeDefined()
45 |
46 | it "populates settings view", ->
47 | waitsForPromise ->
48 | jasmine.attachToDOM(workspaceElement)
49 | atom.packages.activatePackage 'settings-view'
50 | runs ->
51 | atom.commands.dispatch workspaceElement, 'settings-view:open'
52 | waitsFor ->
53 | atom.workspace.getActivePaneItem()
54 | runs ->
55 | item = atom.workspace.getActivePaneItem()
56 | exposeTabView = new ExposeTabView(item)
57 |
58 | expect(exposeTabView.title).toBe 'Settings'
59 | expect(exposeTabView.tabBody.find('a')).toHaveLength 1
60 | expect(exposeTabView.tabBody.find('a').attr('class')).toContain 'tools'
61 |
62 | it "populates archive view", ->
63 | waitsForPromise ->
64 | atom.packages.activatePackage 'archive-view'
65 | atom.workspace.open 'archive.zip'
66 | runs ->
67 | item = atom.workspace.getActivePaneItem()
68 | exposeTabView = new ExposeTabView(item)
69 |
70 | expect(exposeTabView.title).toBe 'archive.zip'
71 | expect(exposeTabView.tabBody.find('a')).toHaveLength 1
72 | expect(exposeTabView.tabBody.find('a').attr('class')).toContain 'zip'
73 |
74 | it "populates markdown view", ->
75 | waitsForPromise ->
76 | atom.packages.activatePackage 'markdown-preview'
77 | atom.workspace.open '../../README.md'
78 | runs ->
79 | element = atom.workspace.getActiveTextEditor().getElement()
80 | item = null
81 | atom.commands.dispatch element, 'markdown-preview:toggle'
82 |
83 | waitsFor ->
84 | item = atom.workspace.getPaneItems()[1]
85 | runs ->
86 | exposeTabView = new ExposeTabView(item)
87 | expect(exposeTabView.title).toBe 'Markdown Preview'
88 | expect(exposeTabView.tabBody.find('a')).toHaveLength 1
89 | expect(exposeTabView.tabBody.find('a').attr('class')).toContain 'markdown'
90 |
91 | it "populates text editor with minimap activated", ->
92 | waitsForPromise ->
93 | atom.packages.activatePackage 'minimap'
94 | jasmine.attachToDOM(workspaceElement)
95 | atom.workspace.open 'sample1.txt'
96 | runs ->
97 | item = atom.workspace.getActivePaneItem()
98 | exposeTabView = new ExposeTabView(item)
99 |
100 | waitsFor ->
101 | exposeTabView.tabBody.html()
102 | runs ->
103 | expect(exposeTabView.item).toBeDefined()
104 | expect(exposeTabView.title).toBe 'sample1.txt'
105 | expect(exposeTabView.tabBody.find('atom-text-editor-minimap')).toHaveLength 1
106 |
107 | it "marks pending tabs", ->
108 | waitsForPromise ->
109 | atom.workspace.open('sample1.txt', pending: true)
110 | runs ->
111 | item = atom.workspace.getActivePaneItem()
112 | exposeTabView = new ExposeTabView(item)
113 |
114 | expect(exposeTabView.title).toBe 'sample1.txt'
115 | expect(exposeTabView.isItemPending()).toBe true
116 | expect(exposeTabView.itemTitle.attr('class')).toContain 'pending'
117 |
118 | describe "closeTab()", ->
119 | it "destroys selected tab item", ->
120 | waitsForPromise ->
121 | atom.workspace.open 'sample1.txt'
122 | runs ->
123 | item = atom.workspace.getActivePaneItem()
124 | exposeTabView = new ExposeTabView(item)
125 |
126 | expect(atom.workspace.getTextEditors()).toHaveLength 1
127 | expect(exposeTabView.title).toBe 'sample1.txt'
128 | expect(exposeTabView.disposables.disposed).toBeFalsy()
129 |
130 | exposeTabView.closeTab()
131 |
132 | expect(atom.workspace.getTextEditors()).toHaveLength 0
133 | expect(exposeTabView.disposables.disposed).toBeTruthy()
134 |
135 | describe "activateTab()", ->
136 | it "activates selected tab item", ->
137 | waitsForPromise ->
138 | atom.workspace.open 'sample1.txt'
139 | atom.workspace.open 'sample2.txt'
140 | runs ->
141 | items = atom.workspace.getPaneItems()
142 | activeItem = atom.workspace.getActivePaneItem()
143 | exposeTabView = new ExposeTabView(items[0])
144 |
145 | expect(items).toHaveLength 2
146 | expect(activeItem.getTitle()).toBe 'sample2.txt'
147 | expect(exposeTabView.title).toBe 'sample1.txt'
148 |
149 | exposeTabView.activateTab()
150 | activeItem = atom.workspace.getActivePaneItem()
151 |
152 | expect(activeItem.getTitle()).toBe 'sample1.txt'
153 |
--------------------------------------------------------------------------------
/spec/expose-view-spec.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 |
3 | ExposeView = require '../lib/expose-view'
4 |
5 | describe "ExposeView", ->
6 | exposeView = null
7 |
8 | beforeEach ->
9 | exposeView = new ExposeView
10 | atom.project.setPaths [path.join(__dirname, 'fixtures')]
11 |
12 | describe "update()", ->
13 | beforeEach ->
14 | waitsForPromise ->
15 | atom.workspace.open 'sample1.txt'
16 | waitsForPromise ->
17 | atom.workspace.open 'sample2.txt'
18 |
19 | it "populates list of open tabs", ->
20 | expect(exposeView.tabList.children()).toHaveLength 0
21 | expect(exposeView.tabs).toHaveLength 0
22 | exposeView.update(true)
23 | expect(exposeView.tabList.children()).toHaveLength 2
24 | expect(exposeView.tabs).toHaveLength 2
25 |
26 | it "assigns colors to different panes", ->
27 | atom.workspace.getActivePane().splitRight(copyActiveItem: true)
28 | exposeView.update(true)
29 | expect(atom.workspace.getCenter().getPanes()).toHaveLength 2
30 | expect(exposeView.tabs).toHaveLength 3
31 |
32 | color1 = exposeView.getGroupColor(0)
33 | color2 = exposeView.getGroupColor(1)
34 | expect(exposeView.tabs[1].color).toEqual color1
35 | expect(exposeView.tabs[2].color).toEqual color2
36 | expect(color1).not.toEqual color2
37 |
38 | describe "activateTab(n)", ->
39 | beforeEach ->
40 | waitsForPromise ->
41 | atom.workspace.open 'sample1.txt'
42 | waitsForPromise ->
43 | atom.workspace.open 'sample2.txt'
44 | waitsForPromise ->
45 | atom.workspace.open 'sample3.txt'
46 |
47 | it "activates given tab", ->
48 | exposeView.update(true)
49 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample3.txt'
50 | exposeView.activateTab(2)
51 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample2.txt'
52 | exposeView.activateTab(1)
53 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample1.txt'
54 |
55 | it "handles out of range input", ->
56 | exposeView.update(true)
57 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample3.txt'
58 | exposeView.activateTab(2)
59 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample2.txt'
60 | exposeView.activateTab(9)
61 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample3.txt'
62 | exposeView.activateTab(0)
63 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample1.txt'
64 | exposeView.activateTab()
65 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample1.txt'
66 |
67 | describe "moveTab(from, to)", ->
68 | beforeEach ->
69 | waitsForPromise ->
70 | atom.workspace.open 'sample1.txt'
71 | waitsForPromise ->
72 | atom.workspace.open 'sample2.txt'
73 | waitsForPromise ->
74 | atom.workspace.open 'sample3.txt'
75 |
76 | it "can move tabs", ->
77 | exposeView.update(true)
78 | expect(exposeView.tabs).toHaveLength 3
79 | expect(exposeView.tabs[0].title).toEqual 'sample1.txt'
80 | expect(exposeView.tabs[2].title).toEqual 'sample3.txt'
81 | exposeView.moveTab(2, 0)
82 | expect(exposeView.tabs[0].title).toEqual 'sample3.txt'
83 | expect(exposeView.tabs[2].title).toEqual 'sample2.txt'
84 |
85 | it "can move tabs between panes", ->
86 | item = atom.workspace.getActivePaneItem()
87 | atom.workspace.getActivePane().splitRight(copyActiveItem: true)
88 | item.destroy()
89 | exposeView.update(true)
90 |
91 | color1 = exposeView.getGroupColor(0)
92 | color2 = exposeView.getGroupColor(1)
93 | expect(color1).not.toEqual color2
94 | expect(exposeView.tabs).toHaveLength 3
95 | expect(exposeView.tabs[0].color).toEqual color1
96 | expect(exposeView.tabs[0].title).toEqual 'sample1.txt'
97 | expect(exposeView.tabs[1].title).toEqual 'sample2.txt'
98 | expect(exposeView.tabs[2].title).toEqual 'sample3.txt'
99 | expect(exposeView.tabs[2].color).toEqual color2
100 |
101 | exposeView.moveTab(0, 2)
102 | expect(exposeView.tabs[1].title).toEqual 'sample1.txt'
103 | expect(exposeView.tabs[1].color).toEqual color2
104 | expect(exposeView.tabs[0].title).toEqual 'sample2.txt'
105 | expect(exposeView.tabs[0].color).toEqual color1
106 |
107 | exposeView.moveTab(1, 0)
108 | expect(exposeView.tabs[0].title).toEqual 'sample1.txt'
109 | expect(exposeView.tabs[0].color).toEqual color1
110 |
111 | it "uses long title when there are multiple items with the same name", ->
112 | atom.workspace.getActivePane().splitRight(copyActiveItem: true)
113 | exposeView.update(true)
114 |
115 | expect(exposeView.tabs).toHaveLength 4
116 | expect(exposeView.tabs[0].title).toEqual 'sample1.txt'
117 | expect(exposeView.tabs[2].title).toEqual 'sample3.txt — fixtures'
118 | expect(exposeView.tabs[3].title).toEqual 'sample3.txt — fixtures'
119 |
120 | it "handles missing long title", ->
121 | atom.workspace.getActivePane().splitRight(copyActiveItem: true)
122 | item = atom.workspace.getActivePaneItem()
123 | item.getLongTitle = undefined
124 | exposeView.update(true)
125 |
126 | expect(exposeView.tabs[2].title).toEqual 'sample3.txt — fixtures'
127 | expect(exposeView.tabs[3].title).toEqual 'sample3.txt'
128 |
129 | it "handles invalid input", ->
130 | exposeView.update(true)
131 | exposeView.moveTab()
132 | expect(exposeView.tabs[0].title).toEqual 'sample1.txt'
133 | exposeView.moveTab(9, 9)
134 | expect(exposeView.tabs[2].title).toEqual 'sample3.txt'
135 |
136 | describe "Cycle around in tabs", ->
137 | beforeEach ->
138 | waitsForPromise ->
139 | atom.workspace.open 'sample1.txt'
140 | waitsForPromise ->
141 | atom.workspace.open 'sample2.txt'
142 | waitsForPromise ->
143 | atom.workspace.open 'sample3.txt'
144 |
145 | it "marks active tab", ->
146 | exposeView.visible = true
147 | exposeView.update()
148 |
149 | expect(exposeView.tabs[2].isActiveTab()).toBeTruthy()
150 | expect(exposeView.tabs[0].hasClass('active')).toBeFalsy()
151 | expect(exposeView.tabs[1].hasClass('active')).toBeFalsy()
152 | expect(exposeView.tabs[2].hasClass('active')).toBeTruthy()
153 |
154 | item = atom.workspace.getPaneItems()[0]
155 | atom.workspace.paneForItem(item).activateItem(item)
156 |
157 | expect(exposeView.tabs[0].isActiveTab()).toBeTruthy()
158 | expect(exposeView.tabs[2].hasClass('active')).toBeFalsy()
159 | expect(exposeView.tabs[1].hasClass('active')).toBeFalsy()
160 | expect(exposeView.tabs[0].hasClass('active')).toBeTruthy()
161 |
162 | it "can go to next tab", ->
163 | exposeView.update(true)
164 | exposeView.activateTab(1)
165 |
166 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample1.txt'
167 | exposeView.nextTab()
168 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample2.txt'
169 | exposeView.nextTab()
170 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample3.txt'
171 | exposeView.nextTab()
172 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample1.txt'
173 |
174 | it "can go to previous tab", ->
175 | exposeView.update(true)
176 |
177 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample3.txt'
178 | exposeView.nextTab(-1)
179 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample2.txt'
180 | exposeView.nextTab(-1)
181 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample1.txt'
182 | exposeView.nextTab(-1)
183 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample3.txt'
184 |
185 | describe "Hide expose view", ->
186 | [workspaceElement, activationPromise] = []
187 |
188 | beforeEach ->
189 | workspaceElement = atom.views.getView(atom.workspace)
190 | activationPromise = atom.packages.activatePackage('expose')
191 |
192 | it "closes expose panel", ->
193 | atom.commands.dispatch workspaceElement, 'expose:toggle'
194 |
195 | waitsForPromise ->
196 | activationPromise
197 |
198 | runs ->
199 | exposeModule = atom.packages.loadedPackages['expose'].mainModule
200 | expect(exposeModule.modalPanel.isVisible()).toBe true
201 | exposeView.exposeHide()
202 | expect(exposeModule.modalPanel.isVisible()).toBe false
203 | exposeView.exposeHide()
204 | expect(exposeModule.modalPanel.isVisible()).toBe false
205 |
206 | describe "Stay updated on changes", ->
207 | beforeEach ->
208 | waitsForPromise ->
209 | atom.workspace.open 'sample1.txt'
210 |
211 | it "updates on add/destroy items", ->
212 | exposeView.visible = true
213 | exposeView.update()
214 | expect(exposeView.tabs).toHaveLength 1
215 |
216 | waitsForPromise ->
217 | atom.workspace.open 'sample2.txt'
218 | runs ->
219 | expect(exposeView.tabs).toHaveLength 2
220 | atom.workspace.getActivePaneItem().destroy()
221 | expect(exposeView.tabs).toHaveLength 1
222 |
223 | it "does not update when not visible", ->
224 | exposeView.update(true)
225 | expect(exposeView.tabs).toHaveLength 1
226 | waitsForPromise ->
227 | atom.workspace.open 'sample2.txt'
228 | runs ->
229 | expect(exposeView.tabs).toHaveLength 1
230 |
231 | describe "Filter tabs", ->
232 | beforeEach ->
233 | waitsForPromise ->
234 | atom.workspace.open 'sample1.txt'
235 | atom.workspace.open 'sample2.txt'
236 | atom.workspace.open 'sample3.txt'
237 |
238 | it "filters open tabs", ->
239 | exposeView.didChangeVisible(true)
240 | expect(exposeView.tabs).toHaveLength 3
241 | exposeView.searchView.setText '2.txt'
242 | exposeView.update(true)
243 | expect(exposeView.tabs).toHaveLength 1
244 | expect(exposeView.tabs[0].title).toBe 'sample2.txt'
245 | exposeView.searchBuffer.setText ''
246 | exposeView.update(true)
247 | expect(exposeView.tabs).toHaveLength 3
248 |
--------------------------------------------------------------------------------
/spec/fixtures/archive.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrodalgaard/atom-expose/1056113a8fd45ef0cc3fd2419ba6a223f1f71f46/spec/fixtures/archive.zip
--------------------------------------------------------------------------------
/spec/fixtures/sample1.txt:
--------------------------------------------------------------------------------
1 | sample1
2 |
--------------------------------------------------------------------------------
/spec/fixtures/sample2.txt:
--------------------------------------------------------------------------------
1 | sample2
2 |
--------------------------------------------------------------------------------
/spec/fixtures/sample3.txt:
--------------------------------------------------------------------------------
1 | sample3
2 |
--------------------------------------------------------------------------------
/styles/expose.less:
--------------------------------------------------------------------------------
1 | @import "ui-variables";
2 |
3 | /* Layout */
4 |
5 | atom-panel.modal.expose-panel.from-top {
6 | position: fixed;
7 | left: 0;
8 | margin: auto;
9 | top: 0;
10 | padding-top: 0;
11 | width: 100%;
12 | max-width: 100%;
13 | height: 100%;
14 | overflow: scroll;
15 | background-color: rgba(0, 0, 0, 0.4);
16 | }
17 |
18 | atom-panel.modal .expose-view {
19 | @shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.5);
20 | height: 100%;
21 |
22 | .expose-top {
23 | background-color: @base-background-color;
24 | display: flex;
25 | flex-wrap: wrap;
26 | max-width: 600px;
27 | margin: 0 auto @component-padding auto;
28 | padding: @component-padding / 2 0;
29 |
30 | .input-block-item {
31 | display: flex;
32 | padding: @component-padding / 2 @component-padding;
33 |
34 | .btn-group {
35 | display: flex;
36 | flex: 1 1 220px;
37 | .btn {
38 | display: flex;
39 | }
40 | }
41 |
42 | // Fix for invalid spacing in modal mini text editor
43 | atom-text-editor[mini] {
44 | margin-bottom: 0;
45 | .editor-contents--private {
46 | height: 22px !important;
47 | }
48 | }
49 | }
50 |
51 | .input-block-item--flex {
52 | flex: 100;
53 | position: relative;
54 | atom-text-editor {
55 | width: 100%;
56 | }
57 | }
58 | }
59 |
60 | .tab-list {
61 | display: -webkit-flex;
62 | justify-content: space-around;
63 | flex-flow: row wrap;
64 |
65 | .expose-tab {
66 | width: 210px;
67 | margin: @component-padding;
68 | -webkit-user-drag: element;
69 | border: #333 solid 1px;
70 | background: @base-background-color;
71 |
72 | .tab-header {
73 | height: 32px;
74 | padding: 8px 2px;
75 | text-align: center;
76 | cursor: move;
77 |
78 | .title {
79 | white-space: nowrap;
80 | overflow: hidden;
81 | text-overflow: ellipsis;
82 | margin: 0 15px;
83 |
84 | &.pending {
85 | font-style: italic;
86 | }
87 |
88 | &::before {
89 | margin-right: 5px;
90 | font-size: 1.1em;
91 | }
92 | }
93 |
94 | .close-icon {
95 | float: right;
96 | margin-top: -17px;
97 | cursor: pointer;
98 | transform: scale(0);
99 | transition-duration: 0.08s;
100 | }
101 | }
102 |
103 | .tab-body {
104 | position: relative;
105 | display: flex;
106 | justify-content: center;
107 | align-items: center;
108 | overflow: hidden;
109 | height: 140px;
110 | padding: 5px;
111 | border-top-style: solid;
112 | border-width: 1px;
113 | cursor: pointer;
114 |
115 | img {
116 | max-height: 100%;
117 | }
118 |
119 | a::before {
120 | .icon-resize(64px);
121 | }
122 |
123 | a.animate::before {
124 | -webkit-animation: spin 1s infinite linear;
125 | }
126 | }
127 |
128 | &:hover {
129 | .tab-body {
130 | box-shadow: @shadow;
131 | }
132 |
133 | .close-icon {
134 | transform: scale(1);
135 | transition-duration: 0.12s;
136 | }
137 | }
138 |
139 | &.active .tab-body {
140 | border-style: solid dashed dashed;
141 | padding-bottom: 4px;
142 | // Do not jump when adding a border
143 | atom-text-editor-minimap {
144 | margin-left: -1px;
145 | }
146 | }
147 | }
148 | }
149 |
150 | // Fix stand alone minimap in relative mode (issue #23)
151 | atom-text-editor-minimap[stand-alone="true"] {
152 | width: 190px;
153 | }
154 |
155 | /* Tabs numbers */
156 |
157 | .tab-numbers-style(@content) {
158 | .close-icon {
159 | transform: none;
160 |
161 | &::before {
162 | font-family: Helvetica, Arial, sans-serif;
163 | content: @content;
164 | font-size: 12px;
165 | font-weight: bold;
166 | }
167 | }
168 | }
169 |
170 | .expose-tab:not(:hover) {
171 | &:nth-of-type(1) {
172 | .tab-numbers-style("1");
173 | }
174 | &:nth-of-type(2) {
175 | .tab-numbers-style("2");
176 | }
177 | &:nth-of-type(3) {
178 | .tab-numbers-style("3");
179 | }
180 | &:nth-of-type(4) {
181 | .tab-numbers-style("4");
182 | }
183 | &:nth-of-type(5) {
184 | .tab-numbers-style("5");
185 | }
186 | &:nth-of-type(6) {
187 | .tab-numbers-style("6");
188 | }
189 | &:nth-of-type(7) {
190 | .tab-numbers-style("7");
191 | }
192 | &:nth-of-type(8) {
193 | .tab-numbers-style("8");
194 | }
195 | &:nth-of-type(9) {
196 | .tab-numbers-style("9");
197 | }
198 | }
199 |
200 | /* Helpers */
201 |
202 | .ghost {
203 | opacity: 0.4;
204 | }
205 |
206 | @-webkit-keyframes spin {
207 | from {
208 | transform: rotate(0deg);
209 | }
210 | to {
211 | transform: rotate(360deg);
212 | }
213 | }
214 |
215 | .icon-resize(@size) {
216 | font-size: @size;
217 | height: @size;
218 | width: @size;
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/styles/themes.less:
--------------------------------------------------------------------------------
1 | @import "ui-variables";
2 |
3 | /* Theme adjustments */
4 |
5 | .theme-atom-light-ui {
6 | atom-panel.modal.expose-panel {
7 | background-color: rgba(255, 255, 255, 0.4);
8 | }
9 | }
10 |
11 | .theme-one-light-ui,
12 | .theme-one-dark-ui {
13 | atom-panel.modal.expose-panel {
14 | background-color: inherit;
15 |
16 | &::before {
17 | opacity: 0;
18 | }
19 | }
20 | }
21 |
22 | .theme-atom-material-ui {
23 | atom-panel.modal.overlay.from-top {
24 | padding: 10px;
25 | }
26 |
27 | .expose-view .tab-body,
28 | .expose-view .tab-header {
29 | background-color: @app-background-color;
30 | }
31 | }
32 |
33 | .theme-native-ui {
34 | atom-panel.modal.expose-panel {
35 | top: 0 !important;
36 | }
37 | }
38 |
39 | .theme-nucleus-dark-ui {
40 | atom-panel.modal:before {
41 | background-color: inherit;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------