/******************************************************************************** * Copyright (C) 2017 TypeFox and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * This Source Code may also be made available under the following Secondary * Licenses when the conditions for such availability set forth in the Eclipse * Public License v. 2.0 are satisfied: GNU General Public License, version 2 * with the GNU Classpath Exception which is available at * https://www.gnu.org/software/classpath/license.html. * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { inject, injectable, postConstruct } from 'inversify'; import { Emitter, Event, WaitUntilEvent } from '@gedit/utils'; import { DisposableCollection } from '@gedit/utils'; import { CancellationToken } from '@gedit/utils'; import { Logger } from '@gedit/utils'; import { SelectionProvider, } from '@gedit/application-common'; import { CompositeTreeNode, Tree, TreeNode } from './tree'; import { SelectableTreeNode, TreeSelection, TreeSelectionService } from './tree-selection'; import { ExpandableTreeNode, TreeExpansionService } from './tree-expansion'; import { TreeNavigationService } from './tree-navigation'; import { BottomUpTreeIterator, Iterators, TopDownTreeIterator, TreeIterator } from './tree-iterator'; import { TreeSearch } from './tree-search'; /** * The tree model. */ export const TreeModel = Symbol('TreeModel'); export interface TreeModel extends Tree, TreeSelectionService, TreeExpansionService { /** * Event when a node should be opened. */ readonly onOpenNode: Event>; /** * Expands the given node. If the `node` argument is `undefined`, then expands the currently selected tree node. * If multiple tree nodes are selected, expands the most recently selected tree node. */ expandNode(node?: Readonly): Promise | undefined>; /** * Collapses the given node. If the `node` argument is `undefined`, then collapses the currently selected tree node. * If multiple tree nodes are selected, collapses the most recently selected tree node. */ collapseNode(node?: Readonly): Promise; /** * Collapses recursively. If the `node` argument is `undefined`, then collapses the currently selected tree node. * If multiple tree nodes are selected, collapses the most recently selected tree node. */ collapseAll(node?: Readonly): Promise; /** * Toggles the expansion state of the given node. If not give, then it toggles the expansion state of the currently selected node. * If multiple nodes are selected, then the most recently selected tree node's expansion state will be toggled. */ toggleNodeExpansion(node?: Readonly): Promise; /** * Opens the given node or the currently selected on if the argument is `undefined`. * If multiple nodes are selected, open the most recently selected node. */ openNode(node?: Readonly | undefined): void; /** * Selects the parent node relatively to the selected taking into account node expansion. */ selectParent(): void; /** * Navigates to the given node if it is defined. This method accepts both the tree node and its ID as an argument. * Navigation sets a node as a root node and expand it. Resolves to the node if the navigation was successful. Otherwise, * resolves to `undefined`. */ navigateTo(nodeOrId: Readonly | string | undefined): Promise; /** * Tests whether it is possible to navigate forward. */ canNavigateForward(): boolean; /** * Tests whether it is possible to navigate backward. */ canNavigateBackward(): boolean; /** * Navigates forward. */ navigateForward(): Promise; /** * Navigates backward. */ navigateBackward(): Promise; /** * Selects the previous node relatively to the currently selected one. This method takes the expansion state of the tree into consideration. */ selectPrevNode(type?: TreeSelection.SelectionType): void; /** * Returns the previous selectable tree node. */ getPrevSelectableNode(node?: TreeNode): SelectableTreeNode | undefined; /** * Selects the next node relatively to the currently selected one. This method takes the expansion state of the tree into consideration. */ selectNextNode(type?: TreeSelection.SelectionType): void; /** * Returns the next selectable tree node. */ getNextSelectableNode(node?: TreeNode): SelectableTreeNode | undefined; /** * Selects the given tree node. Has no effect when the node does not exist in the tree. Discards any previous selection state. */ selectNode(node: Readonly): void; /** * Selects the given node if it was not yet selected, or unselects it if it was. Keeps the previous selection state and updates it * with the current toggle selection. */ toggleNode(node: Readonly): void; /** * Selects a range of tree nodes. The target of the selection range is the argument, the from tree node is the previous selected node. * If no node was selected previously, invoking this method does nothing. */ selectRange(node: Readonly): void; } @injectable() export class TreeModelImpl implements TreeModel, SelectionProvider>> { @inject(Logger) protected readonly logger: Logger; @inject(Tree) protected readonly tree: Tree; @inject(TreeSelectionService) protected readonly selectionService: TreeSelectionService; @inject(TreeExpansionService) protected readonly expansionService: TreeExpansionService; @inject(TreeNavigationService) protected readonly navigationService: TreeNavigationService; @inject(TreeSearch) protected readonly treeSearch: TreeSearch; protected readonly onChangedEmitter = new Emitter(); protected readonly onOpenNodeEmitter = new Emitter(); protected readonly toDispose = new DisposableCollection(); get root(): TreeNode | undefined { return this.tree.root; } set root(root: TreeNode | undefined) { this.tree.root = root; } get onChanged(): Event { return this.onChangedEmitter.event; } get onOpenNode(): Event { return this.onOpenNodeEmitter.event; } get onNodeRefreshed(): Event & WaitUntilEvent> { return this.tree.onNodeRefreshed; } // tslint:disable-next-line:typedef get selectedNodes() { return this.selectionService.selectedNodes; } // tslint:disable-next-line:typedef get onSelectionChanged() { return this.selectionService.onSelectionChanged; } get onExpansionChanged(): Event> { return this.expansionService.onExpansionChanged; } get onDidChangeBusy(): Event { return this.tree.onDidChangeBusy; } dispose(): void { this.toDispose.dispose(); } getNode(id: string | undefined): TreeNode | undefined { return this.tree.getNode(id); } validateNode(node: TreeNode | undefined): TreeNode | undefined { return this.tree.validateNode(node); } async refresh(parent?: Readonly): Promise { if (parent) { return this.tree.refresh(parent); } return this.tree.refresh(); } async expandNode(raw?: Readonly): Promise { for (const node of raw ? [raw] : this.selectedNodes) { if (ExpandableTreeNode.is(node)) { return this.expansionService.expandNode(node); } } return undefined; } async collapseNode(raw?: Readonly): Promise { for (const node of raw ? [raw] : this.selectedNodes) { if (ExpandableTreeNode.is(node)) { return this.expansionService.collapseNode(node); } } return false; } async collapseAll(raw?: Readonly): Promise { const node = raw || this.selectedNodes[0]; if (SelectableTreeNode.is(node)) { this.selectNode(node); } if (CompositeTreeNode.is(node)) { return this.expansionService.collapseAll(node); } return false; } async toggleNodeExpansion(raw?: Readonly): Promise { for (const node of raw ? [raw] : this.selectedNodes) { if (ExpandableTreeNode.is(node)) { await this.expansionService.toggleNodeExpansion(node); return; } } } selectPrevNode(type: TreeSelection.SelectionType = TreeSelection.SelectionType.DEFAULT): void { const node = this.getPrevSelectableNode(); if (node) { this.addSelection({node, type}); } } getPrevSelectableNode(node: TreeNode = this.selectedNodes[0]): SelectableTreeNode | undefined { const iterator = this.createBackwardIterator(node); return iterator && this.doGetNextNode(iterator); } selectNextNode(type: TreeSelection.SelectionType = TreeSelection.SelectionType.DEFAULT): void { const node = this.getNextSelectableNode(); if (node) { this.addSelection({node, type}); } } getNextSelectableNode(node: TreeNode = this.selectedNodes[0]): SelectableTreeNode | undefined { const iterator = this.createIterator(node); return iterator && this.doGetNextNode(iterator); } openNode(raw?: TreeNode | undefined): void { const node = raw || this.selectedNodes[0]; if (node) { this.doOpenNode(node); this.onOpenNodeEmitter.fire(node); } } selectParent(): void { if (this.selectedNodes.length === 1) { const node = this.selectedNodes[0]; const parent = SelectableTreeNode.getVisibleParent(node); if (parent) { this.selectNode(parent); } } } async navigateTo(nodeOrId: TreeNode | string | undefined): Promise { if (nodeOrId) { const node = typeof nodeOrId === 'string' ? this.getNode(nodeOrId) : nodeOrId; if (node) { this.navigationService.push(node); await this.doNavigate(node); return node; } } return undefined; } canNavigateForward(): boolean { return !!this.navigationService.next; } canNavigateBackward(): boolean { return !!this.navigationService.prev; } async navigateForward(): Promise { const node = this.navigationService.advance(); if (node) { await this.doNavigate(node); } } async navigateBackward(): Promise { const node = this.navigationService.retreat(); if (node) { await this.doNavigate(node); } } addSelection(selectionOrTreeNode: TreeSelection | Readonly): void { this.selectionService.addSelection(selectionOrTreeNode); } selectNode(node: Readonly): void { this.addSelection(node); } toggleNode(node: Readonly): void { this.addSelection({node, type: TreeSelection.SelectionType.TOGGLE}); } selectRange(node: Readonly): void { this.addSelection({node, type: TreeSelection.SelectionType.RANGE}); } storeState(): TreeModelImpl.State { return { selection: this.selectionService.storeState() }; } restoreState(state: TreeModelImpl.State): void { if (state.selection) { this.selectionService.restoreState(state.selection); } } markAsBusy(node: Readonly, ms: number, token: CancellationToken): Promise { return this.tree.markAsBusy(node, ms, token); } @postConstruct() protected init(): void { this.toDispose.push(this.tree); this.toDispose.push(this.tree.onChanged(() => this.fireChanged())); this.toDispose.push(this.selectionService); this.toDispose.push(this.expansionService); this.toDispose.push(this.expansionService.onExpansionChanged(node => { this.fireChanged(); this.handleExpansion(node); })); this.toDispose.push(this.onOpenNodeEmitter); this.toDispose.push(this.onChangedEmitter); this.toDispose.push(this.treeSearch); } protected handleExpansion(node: Readonly): void { this.selectIfAncestorOfSelected(node); } /** * Select the given node if it is the ancestor of a selected node. */ protected selectIfAncestorOfSelected(node: Readonly): void { if (!node.expanded && [...this.selectedNodes].some(selectedNode => CompositeTreeNode.isAncestor(node, selectedNode))) { if (SelectableTreeNode.isVisible(node)) { this.selectNode(node); } } } protected fireChanged(): void { this.onChangedEmitter.fire(undefined); } protected doGetNextNode(iterator: TreeIterator): SelectableTreeNode | undefined { // Skip the first item. // TODO: clean this up, and skip the first item in a different way without loading everything. iterator.next(); let result = iterator.next(); while (!result.done && !SelectableTreeNode.isVisible(result.value)) { result = iterator.next(); } const node = result.value; if (SelectableTreeNode.isVisible(node)) { return node; } return undefined; } protected createBackwardIterator(node: TreeNode | undefined): TreeIterator | undefined { const {filteredNodes} = this.treeSearch; if (filteredNodes.length === 0) { return node ? new BottomUpTreeIterator(node!, {pruneCollapsed: true}) : undefined; } if (node && filteredNodes.indexOf(node) === -1) { return undefined; } return Iterators.cycle(filteredNodes.slice().reverse(), node); } protected createIterator(node: TreeNode | undefined): TreeIterator | undefined { const {filteredNodes} = this.treeSearch; if (filteredNodes.length === 0) { return node ? new TopDownTreeIterator(node!, {pruneCollapsed: true}) : undefined; } if (node && filteredNodes.indexOf(node) === -1) { return undefined; } return Iterators.cycle(filteredNodes, node); } protected doOpenNode(node: TreeNode): void { if (ExpandableTreeNode.is(node)) { this.toggleNodeExpansion(node); } } protected async doNavigate(node: TreeNode): Promise { this.tree.root = node; if (ExpandableTreeNode.is(node)) { await this.expandNode(node); } if (SelectableTreeNode.is(node)) { this.selectNode(node); } } get size(): number { return this.tree.size; } } export namespace TreeModelImpl { export interface State { selection: object } }