pane-container.js
1 const { find } = require('underscore-plus'); 2 const { Emitter, CompositeDisposable } = require('event-kit'); 3 const Pane = require('./pane'); 4 const ItemRegistry = require('./item-registry'); 5 const PaneContainerElement = require('./pane-container-element'); 6 7 const SERIALIZATION_VERSION = 1; 8 const STOPPED_CHANGING_ACTIVE_PANE_ITEM_DELAY = 100; 9 10 module.exports = class PaneContainer { 11 constructor(params) { 12 let applicationDelegate, deserializerManager, notificationManager; 13 ({ 14 config: this.config, 15 applicationDelegate, 16 notificationManager, 17 deserializerManager, 18 viewRegistry: this.viewRegistry, 19 location: this.location 20 } = params); 21 this.emitter = new Emitter(); 22 this.subscriptions = new CompositeDisposable(); 23 this.itemRegistry = new ItemRegistry(); 24 this.alive = true; 25 this.stoppedChangingActivePaneItemTimeout = null; 26 27 this.setRoot( 28 new Pane({ 29 container: this, 30 config: this.config, 31 applicationDelegate, 32 notificationManager, 33 deserializerManager, 34 viewRegistry: this.viewRegistry 35 }) 36 ); 37 this.didActivatePane(this.getRoot()); 38 } 39 40 getLocation() { 41 return this.location; 42 } 43 44 getElement() { 45 return this.element != null 46 ? this.element 47 : (this.element = new PaneContainerElement().initialize(this, { 48 views: this.viewRegistry 49 })); 50 } 51 52 destroy() { 53 this.alive = false; 54 for (let pane of this.getRoot().getPanes()) { 55 pane.destroy(); 56 } 57 this.cancelStoppedChangingActivePaneItemTimeout(); 58 this.subscriptions.dispose(); 59 this.emitter.dispose(); 60 } 61 62 isAlive() { 63 return this.alive; 64 } 65 66 isDestroyed() { 67 return !this.isAlive(); 68 } 69 70 serialize(params) { 71 return { 72 deserializer: 'PaneContainer', 73 version: SERIALIZATION_VERSION, 74 root: this.root ? this.root.serialize() : null, 75 activePaneId: this.activePane.id 76 }; 77 } 78 79 deserialize(state, deserializerManager) { 80 if (state.version !== SERIALIZATION_VERSION) return; 81 this.itemRegistry = new ItemRegistry(); 82 this.setRoot(deserializerManager.deserialize(state.root)); 83 this.activePane = 84 find(this.getRoot().getPanes(), pane => pane.id === state.activePaneId) || 85 this.getPanes()[0]; 86 if (this.config.get('core.destroyEmptyPanes')) this.destroyEmptyPanes(); 87 } 88 89 onDidChangeRoot(fn) { 90 return this.emitter.on('did-change-root', fn); 91 } 92 93 observeRoot(fn) { 94 fn(this.getRoot()); 95 return this.onDidChangeRoot(fn); 96 } 97 98 onDidAddPane(fn) { 99 return this.emitter.on('did-add-pane', fn); 100 } 101 102 observePanes(fn) { 103 for (let pane of this.getPanes()) { 104 fn(pane); 105 } 106 return this.onDidAddPane(({ pane }) => fn(pane)); 107 } 108 109 onDidDestroyPane(fn) { 110 return this.emitter.on('did-destroy-pane', fn); 111 } 112 113 onWillDestroyPane(fn) { 114 return this.emitter.on('will-destroy-pane', fn); 115 } 116 117 onDidChangeActivePane(fn) { 118 return this.emitter.on('did-change-active-pane', fn); 119 } 120 121 onDidActivatePane(fn) { 122 return this.emitter.on('did-activate-pane', fn); 123 } 124 125 observeActivePane(fn) { 126 fn(this.getActivePane()); 127 return this.onDidChangeActivePane(fn); 128 } 129 130 onDidAddPaneItem(fn) { 131 return this.emitter.on('did-add-pane-item', fn); 132 } 133 134 observePaneItems(fn) { 135 for (let item of this.getPaneItems()) { 136 fn(item); 137 } 138 return this.onDidAddPaneItem(({ item }) => fn(item)); 139 } 140 141 onDidChangeActivePaneItem(fn) { 142 return this.emitter.on('did-change-active-pane-item', fn); 143 } 144 145 onDidStopChangingActivePaneItem(fn) { 146 return this.emitter.on('did-stop-changing-active-pane-item', fn); 147 } 148 149 observeActivePaneItem(fn) { 150 fn(this.getActivePaneItem()); 151 return this.onDidChangeActivePaneItem(fn); 152 } 153 154 onWillDestroyPaneItem(fn) { 155 return this.emitter.on('will-destroy-pane-item', fn); 156 } 157 158 onDidDestroyPaneItem(fn) { 159 return this.emitter.on('did-destroy-pane-item', fn); 160 } 161 162 getRoot() { 163 return this.root; 164 } 165 166 setRoot(root) { 167 this.root = root; 168 this.root.setParent(this); 169 this.root.setContainer(this); 170 this.emitter.emit('did-change-root', this.root); 171 if (this.getActivePane() == null && this.root instanceof Pane) { 172 this.didActivatePane(this.root); 173 } 174 } 175 176 replaceChild(oldChild, newChild) { 177 if (oldChild !== this.root) { 178 throw new Error('Replacing non-existent child'); 179 } 180 this.setRoot(newChild); 181 } 182 183 getPanes() { 184 if (this.alive) { 185 return this.getRoot().getPanes(); 186 } else { 187 return []; 188 } 189 } 190 191 getPaneItems() { 192 return this.getRoot().getItems(); 193 } 194 195 getActivePane() { 196 return this.activePane; 197 } 198 199 getActivePaneItem() { 200 return this.getActivePane().getActiveItem(); 201 } 202 203 paneForURI(uri) { 204 return find(this.getPanes(), pane => pane.itemForURI(uri) != null); 205 } 206 207 paneForItem(item) { 208 return find(this.getPanes(), pane => pane.getItems().includes(item)); 209 } 210 211 saveAll() { 212 for (let pane of this.getPanes()) { 213 pane.saveItems(); 214 } 215 } 216 217 confirmClose(options) { 218 const promises = []; 219 for (const pane of this.getPanes()) { 220 for (const item of pane.getItems()) { 221 promises.push(pane.promptToSaveItem(item, options)); 222 } 223 } 224 return Promise.all(promises).then(results => !results.includes(false)); 225 } 226 227 activateNextPane() { 228 const panes = this.getPanes(); 229 if (panes.length > 1) { 230 const currentIndex = panes.indexOf(this.activePane); 231 const nextIndex = (currentIndex + 1) % panes.length; 232 panes[nextIndex].activate(); 233 return true; 234 } else { 235 return false; 236 } 237 } 238 239 activatePreviousPane() { 240 const panes = this.getPanes(); 241 if (panes.length > 1) { 242 const currentIndex = panes.indexOf(this.activePane); 243 let previousIndex = currentIndex - 1; 244 if (previousIndex < 0) { 245 previousIndex = panes.length - 1; 246 } 247 panes[previousIndex].activate(); 248 return true; 249 } else { 250 return false; 251 } 252 } 253 254 moveActiveItemToPane(destPane) { 255 const item = this.activePane.getActiveItem(); 256 257 if (!destPane.isItemAllowed(item)) { 258 return; 259 } 260 261 this.activePane.moveItemToPane(item, destPane); 262 destPane.setActiveItem(item); 263 } 264 265 copyActiveItemToPane(destPane) { 266 const item = this.activePane.copyActiveItem(); 267 268 if (item && destPane.isItemAllowed(item)) { 269 destPane.activateItem(item); 270 } 271 } 272 273 destroyEmptyPanes() { 274 for (let pane of this.getPanes()) { 275 if (pane.items.length === 0) { 276 pane.destroy(); 277 } 278 } 279 } 280 281 didAddPane(event) { 282 this.emitter.emit('did-add-pane', event); 283 const items = event.pane.getItems(); 284 for (let i = 0, length = items.length; i < length; i++) { 285 const item = items[i]; 286 this.didAddPaneItem(item, event.pane, i); 287 } 288 } 289 290 willDestroyPane(event) { 291 this.emitter.emit('will-destroy-pane', event); 292 } 293 294 didDestroyPane(event) { 295 this.emitter.emit('did-destroy-pane', event); 296 } 297 298 didActivatePane(activePane) { 299 if (activePane !== this.activePane) { 300 if (!this.getPanes().includes(activePane)) { 301 throw new Error( 302 'Setting active pane that is not present in pane container' 303 ); 304 } 305 306 this.activePane = activePane; 307 this.emitter.emit('did-change-active-pane', this.activePane); 308 this.didChangeActiveItemOnPane( 309 this.activePane, 310 this.activePane.getActiveItem() 311 ); 312 } 313 this.emitter.emit('did-activate-pane', this.activePane); 314 return this.activePane; 315 } 316 317 didAddPaneItem(item, pane, index) { 318 this.itemRegistry.addItem(item); 319 this.emitter.emit('did-add-pane-item', { item, pane, index }); 320 } 321 322 willDestroyPaneItem(event) { 323 return this.emitter.emitAsync('will-destroy-pane-item', event); 324 } 325 326 didDestroyPaneItem(event) { 327 this.itemRegistry.removeItem(event.item); 328 this.emitter.emit('did-destroy-pane-item', event); 329 } 330 331 didChangeActiveItemOnPane(pane, activeItem) { 332 if (pane === this.getActivePane()) { 333 this.emitter.emit('did-change-active-pane-item', activeItem); 334 335 this.cancelStoppedChangingActivePaneItemTimeout(); 336 // `setTimeout()` isn't available during the snapshotting phase, but that's okay. 337 if (!global.isGeneratingSnapshot) { 338 this.stoppedChangingActivePaneItemTimeout = setTimeout(() => { 339 this.stoppedChangingActivePaneItemTimeout = null; 340 this.emitter.emit('did-stop-changing-active-pane-item', activeItem); 341 }, STOPPED_CHANGING_ACTIVE_PANE_ITEM_DELAY); 342 } 343 } 344 } 345 346 cancelStoppedChangingActivePaneItemTimeout() { 347 if (this.stoppedChangingActivePaneItemTimeout != null) { 348 clearTimeout(this.stoppedChangingActivePaneItemTimeout); 349 } 350 } 351 };