Skip to content

Commit

Permalink
Provide ability to add source directories to the Explorer panel
Browse files Browse the repository at this point in the history
A directory can be added to the Explorer panel via the window menu by
clicking on `File->Open Source Directory...`.
  • Loading branch information
enlight committed Jun 8, 2016
1 parent da0a390 commit 2f48f4a
Show file tree
Hide file tree
Showing 12 changed files with 243 additions and 9 deletions.
1 change: 1 addition & 0 deletions app/src/common/command-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

// Identifiers of all available built-in commands

export const OPEN_SRC_DIR = 'open-src-dir';
export const QUIT_APP = 'quit';
10 changes: 9 additions & 1 deletion app/src/common/command-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@
// TODO: Eventually this all needs to work with users typing in commands, but that's a problem for
// another day.

export interface ICommandArgs {
/**
* The browser window (if any) that invoked the command.
* This argument will always be provided when the command is invoked via the window menu.
*/
browserWindow?: GitHubElectron.BrowserWindow;
}

export interface ICommand {
execute(...args: any[]): void;
execute<T extends ICommandArgs>(args: T): void;
}

export class CommandTable {
Expand Down
15 changes: 15 additions & 0 deletions app/src/common/ipc/source-dir-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) 2016 Vadim Macagon
// MIT License, see LICENSE file for full terms.

export const IPC_CONNECT = 'source-dir-registry:connect';
export const IPC_DISCONNECT = 'source-dir-registry:disconnect';
export const IPC_UPDATE = 'source-dir-registry:update';

export interface IConnectResponse {
dirPaths: string[];
}

export interface IUpdateRequest {
kind: 'add';
dirPaths: string[];
}
7 changes: 7 additions & 0 deletions app/src/main/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { PathPicker } from './platform/path-picker';
import { WindowMenuManager } from './platform/window-menu-manager';
import { CommandTable } from '../common/command-table';
import * as cmds from '../common/command-ids';
import { SourceDirRegistry } from './source-dir-registry';
import { OpenSourceDirCommand } from './commands'

export interface IApplicationArgs {
/** Path to the root directory of the application. */
Expand All @@ -24,12 +26,14 @@ export class Application {
private _pathPicker: PathPicker;
private _windowMenuManager: WindowMenuManager;
private _commands: CommandTable;
private _sourceDirRegistry: SourceDirRegistry;

run(args: IApplicationArgs): void {
this._devTools = new DevTools();
const uriPathResolver = new UriPathResolver(args.rootPath);
this._appProtocolHandler = new AppProtocolHandler(uriPathResolver);
this._pathPicker = new PathPicker();
this._sourceDirRegistry = new SourceDirRegistry();
this._commands = new CommandTable();
this.registerCommands();
this._windowMenuManager = new WindowMenuManager(this._commands);
Expand All @@ -44,6 +48,9 @@ export class Application {
}

registerCommands(): void {
this._commands.add(cmds.OPEN_SRC_DIR, new OpenSourceDirCommand({
pathPicker: this._pathPicker, sourceDirRegistry: this._sourceDirRegistry
}));
this._commands.add(cmds.QUIT_APP, { execute: () => app.quit() });
}
}
4 changes: 4 additions & 0 deletions app/src/main/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) 2016 Vadim Macagon
// MIT License, see LICENSE file for full terms.

export { OpenSourceDirCommand } from './open-src-dir';
50 changes: 50 additions & 0 deletions app/src/main/commands/open-src-dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2016 Vadim Macagon
// MIT License, see LICENSE file for full terms.

import { ICommand, ICommandArgs } from '../../common/command-table';
import { PathPicker } from '../platform/path-picker';
import { SourceDirRegistry } from '../source-dir-registry';

/**
* Opens a source directory and displays it in the `Explorer` panel.
*
* If a directory path is not supplied the user will be prompted for one.
*/
export class OpenSourceDirCommand implements ICommand {
private _pathPicker: PathPicker;
private _sourceDirRegistry: SourceDirRegistry;

constructor(params: OpenSourceDirCommand.IConstructorParams) {
this._pathPicker = params.pathPicker;
this._sourceDirRegistry = params.sourceDirRegistry;
}

/**
* If the directory path is not supplied a native directory selection dialog will be displayed.
*
* @return A promise that will be resolved when the command is done.
*/
async execute({ dirPath, browserWindow }: OpenSourceDirCommand.IArgs): Promise<void> {
if (!dirPath) {
dirPath = await this._pathPicker.promptForPath({
title: 'Open Source Directory', pathKind: 'dir', parentWindow: browserWindow
});
if (dirPath === null) {
return;
}
}
this._sourceDirRegistry.add(dirPath);
}
}

export namespace OpenSourceDirCommand {
export interface IConstructorParams {
pathPicker: PathPicker;
sourceDirRegistry: SourceDirRegistry;
}

export interface IArgs extends ICommandArgs {
/** Absolute path to a source directory. */
dirPath?: string;
}
}
6 changes: 3 additions & 3 deletions app/src/main/platform/window-menu-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { ipcMain, BrowserWindow, Menu } from 'electron';
import * as ipc from '../../common/window-menu-ipc';
import { CommandTable } from '../../common/command-table';
import { CommandTable, ICommandArgs } from '../../common/command-table';

/**
* Works in conjuction with [[WindowMenu]] to allow the `BrowserWindow` menu to be manipulated
Expand Down Expand Up @@ -99,7 +99,7 @@ export class WindowMenuManager {
if (cmdId) {
const cmd = this.commands.findCommandById(cmdId);
if (cmd) {
cmd.execute();
cmd.execute({ browserWindow });
}
} else {
const request: ipc.IActionRequest = { id: menuItem.id };
Expand All @@ -114,7 +114,7 @@ export class WindowMenuManager {
if (cmdId) {
const cmd = this.commands.findCommandById(cmdId);
if (cmd) {
cmd.execute(menuItem.checked);
cmd.execute({ value: menuItem.checked, browserWindow });
}
} else {
const request: ipc.IActionRequest = {
Expand Down
51 changes: 51 additions & 0 deletions app/src/main/source-dir-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) 2016 Vadim Macagon
// MIT License, see LICENSE file for full terms.

import { ipcMain } from 'electron';
import * as ipc from '../common/ipc/source-dir-registry';

/**
* Maintains a list of all currently open source directories.
*
* The list of source directories can be accessed in the renderer process via
* [[RendererSourceDirRegistry]]. Any changes made to the list of source directories in
* [[SourceDirRegistry]] will be propagated to all [[RendererSourceDirRegistry]] instances.
*/
export class SourceDirRegistry {
/** Absolute paths to source directories. */
private _dirPaths: string[] = [];
/** Hosts of all remote [[RendererSourceDirRegistry]] instances. */
private _subscribers = new Set<GitHubElectron.WebContents>();

constructor() {
this._onConnectRequest = this._onConnectRequest.bind(this);
ipcMain.on(ipc.IPC_CONNECT, this._onConnectRequest);
}

dispose(): void {
ipcMain.removeListener(ipc.IPC_CONNECT, this._onConnectRequest);
this._subscribers.clear();
}

add(...sourceDirPaths: string[]): void {
this._dirPaths.push(...sourceDirPaths);
const request: ipc.IUpdateRequest = { kind: 'add', dirPaths: sourceDirPaths };
this._subscribers.forEach(subscriber => subscriber.send(ipc.IPC_UPDATE, request));
}

private _onConnectRequest(event: GitHubElectron.IMainIPCEvent): void {
const webContents = event.sender;

if (this._subscribers.has(webContents)) {
throw new Error(`Subscriber is already connected.`);
}
this._subscribers.add(webContents);
ipcMain.once(ipc.IPC_DISCONNECT, (event: GitHubElectron.IMainIPCEvent) => {
this._subscribers.delete(event.sender);
});
const response: ipc.IConnectResponse = {
dirPaths: this._dirPaths
};
webContents.send(ipc.IPC_CONNECT, response);
}
}
35 changes: 32 additions & 3 deletions app/src/renderer/components/directory-tree/directory-tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import * as path from 'path';
import * as mobx from 'mobx';
import { PanelModel, IPanelItem } from '../layout/panel-model';

function isArrayChange(change: mobx.IArrayChange<any> | mobx.IArraySplice<any>): change is mobx.IArrayChange<any> {
return change.type === 'update';
}

function isArraySplice(change: mobx.IArrayChange<any> | mobx.IArraySplice<any>): change is mobx.IArraySplice<any> {
return change.type === 'splice';
}

function isObservableArray<T>(array: T[]): array is mobx.IObservableArray<T> {
return mobx.isObservableArray(array);
}

// FIXME: Watch directories in the tree for changes, and update the children of any expanded items
export class DirectoryTreeModel implements IPanelItem {
id: string;
Expand All @@ -20,18 +32,31 @@ export class DirectoryTreeModel implements IPanelItem {
private _itemIndices = new Map<DirectoryTreeItemModel, /*index:*/number>();

constructor({
id, displayRoot = true, indentPerLevel = 25
id, dirPaths, displayRoot = true, indentPerLevel = 25
}: DirectoryTreeModel.IConstructorParams) {
this.id = id;
this._isRootExcluded = !displayRoot;
this._indentPerLevel = indentPerLevel;
this._root = new DirectoryTreeItemModel({ id: 'root', fullPath: null, level: 0, isExpandable: true, isExpanded: true });
this._root = new DirectoryTreeItemModel({
id: 'root', fullPath: null, level: 0, isExpandable: true, isExpanded: true
});
if (this._isRootExcluded) {
this.items = [];
} else {
this.items = [this._root];
this._itemIndices.set(this._root, 0);
}
if (isObservableArray(dirPaths)) {
dirPaths.observe(this._onDirsDidChange.bind(this), true);
}
}

@mobx.action
private _onDirsDidChange(change: mobx.IArrayChange<string> | mobx.IArraySplice<string>): void {
if (isArraySplice(change)) {
change.added.forEach(dirPath => this.addDirectory(dirPath));
change.removed.forEach(dirPath => this.removeDirectory(dirPath));
}
}

onDidAttachToPanel(panel: PanelModel): void {
Expand All @@ -43,7 +68,10 @@ export class DirectoryTreeModel implements IPanelItem {
if (!dirStat.isDirectory()) {
throw new Error(`"${absolutePath}" is not a directory.`);
}
const item = new DirectoryTreeItemModel({ id: 'test', fullPath: absolutePath, level: 1, parent: this._root, isExpandable: true });
const item = new DirectoryTreeItemModel({
id: `${this._root.children ? this._root.children.length : 0}`,
fullPath: absolutePath, level: 1, parent: this._root, isExpandable: true
});

// FIXME: can't just call getLastSubTreeItem() since the order of siblings will depend on the sort order
this._addItems(getLastSubTreeItem(item.parent), [item]);
Expand Down Expand Up @@ -148,6 +176,7 @@ export class DirectoryTreeModel implements IPanelItem {
export namespace DirectoryTreeModel {
export interface IConstructorParams {
id: string;
dirPaths: string[];
/** Set to `false` to prevent the root item from being rendered, defaults to `true`. */
displayRoot?: boolean;
/** Number of pixels to indent each level of the tree by when the tree is rendered. */
Expand Down
5 changes: 4 additions & 1 deletion app/src/renderer/init-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import * as AppWindowConfig from '../common/app-window-config';
window.onload = (e: Event) => {
const config = AppWindowConfig.decodeFromUriComponent(window.location.hash.substr(1));
RendererContext.create(config)
.then(rendererContext => rendererContext.showWindow())
.then(rendererContext => {
window.onunload = () => rendererContext.dispose();
rendererContext.showWindow();
})
.catch((error) => {
showError(error);
});
Expand Down
12 changes: 11 additions & 1 deletion app/src/renderer/renderer-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
import { PathPickerProxy } from './platform/path-picker-proxy';
import { WindowMenu } from './platform/window-menu';
import * as cmds from '../common/command-ids';
import { RendererSourceDirRegistry } from './source-dir-registry';

export const enum Cursor {
HorizontalResize,
Expand All @@ -50,6 +51,7 @@ export class RendererContext {

private windowMenu: WindowMenu;
private devTools: RendererDevTools;
private _sourceDirRegistry: RendererSourceDirRegistry;

/** Create the renderer context for the current process. */
static async create(config: IAppWindowConfig): Promise<RendererContext> {
Expand All @@ -63,6 +65,12 @@ export class RendererContext {
this.devTools = new RendererDevTools();
}

dispose(): void {
if (this._sourceDirRegistry) {
this._sourceDirRegistry.dispose();
}
}

async initialize(): Promise<void> {
const uriPathResolver = new UriPathResolver(this.rootPath);
const elementManifestLoader = new ElementManifestLoader(uriPathResolver);
Expand Down Expand Up @@ -94,6 +102,7 @@ export class RendererContext {
);

this.windowMenu = this.createWindowMenu();
this._sourceDirRegistry = new RendererSourceDirRegistry();

const userDataDir = electron.remote.app.getPath('userData');
const debugConfigsPath = path.join(userDataDir, 'HydragonDebugConfigs.json');
Expand All @@ -115,7 +124,7 @@ export class RendererContext {
const mainPageSet = new PageSetModel({ id: 'main-page-set', height: '100%' });
const pageTree = new PageTreeModel({ id: 'page-tree', height: '100%' });
const debugToolbar = new DebugToolbarModel({ id: 'debug-toolbar', debugConfigManager, debugConfigPresenter });
const dirTree = new DirectoryTreeModel({ id: 'explorer', displayRoot: false });
const dirTree = new DirectoryTreeModel({ id: 'explorer', displayRoot: false, dirPaths: this._sourceDirRegistry.dirPaths });

workspaceModel.createDefaultLayout({ mainPageSet, pageTree, debugToolbar, dirTree });

Expand Down Expand Up @@ -235,6 +244,7 @@ export class RendererContext {
}

const fileMenu = menu.subMenu('&File');
fileMenu.item('Open Source Directory...', { action: cmds.OPEN_SRC_DIR });
if (process.platform !== 'darwin') {
fileMenu.separator();
fileMenu.item('E&xit', { action: cmds.QUIT_APP });
Expand Down
Loading

0 comments on commit 2f48f4a

Please sign in to comment.