Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File dialog Enhancements #8748

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
# Change Log

## v1.12.0

- [filesystem] add text input and navigate up icon to file dialog [#8748](https://github.com/eclipse-theia/theia/pull/8748)

<a name="breaking_changes_1.12.0">[Breaking Changes:](#breaking_changes_1.12.0)</a>

- [filesystem] `FileDialog` and `LocationListRenderer` now require `FileService` to be passed into constructor for text-based file dialog navigation in browser [#8748](https://github.com/eclipse-theia/theia/pull/8748)

## v1.11.0 - 2/25/2021

- [api-samples] added example to echo the currently supported vscode API version [#8191](https://github.com/eclipse-theia/theia/pull/8191)
Expand Down Expand Up @@ -40,7 +49,6 @@
- [task] updated logic to activate corresponding terminal when using the `show running tasks` action [#9016](https://github.com/eclipse-theia/theia/pull/9016)
- [vsx-registry] added API compatibility handling when installing extensions through the 'extensions-view' [#8191](https://github.com/eclipse-theia/theia/pull/8191)


<a name="breaking_changes_1.11.0">[Breaking Changes:](#breaking_changes_1.11.0)</a>

- [core] updated `SearchBox.input` field type from `HTMLInputElement` to `HTMLSpanElement` [#9005](https://github.com/eclipse-theia/theia/pull/9005)
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/browser/style/tabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
--theia-private-horizontal-tab-scrollbar-rail-height: 7px;
--theia-private-horizontal-tab-scrollbar-height: 5px;
--theia-tabbar-toolbar-z-index: 1001;
--theia-toolbar-active-transform-scale: 1.272019649;
}

/*-----------------------------------------------------------------------------
Expand Down Expand Up @@ -352,7 +353,7 @@ body.theia-editor-highlightModifiedTabs
}

.p-TabBar-toolbar .item.enabled.active {
transform: scale(1.272019649);
transform: scale(var(--theia-toolbar-active-transform-scale));
}

.p-TabBar-toolbar .item.toggled {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,9 @@ export class FileDialogModel extends FileTreeModel {
private isFileStatNodeSelectable(node: FileStatNode): boolean {
return !(!node.fileStat.isDirectory && this._disableFileSelection);
}

canNavigateUpward(): boolean {
const treeRoot = this.tree.root;
return FileStatNode.is(treeRoot) && !treeRoot.uri.path.isRoot;
}
}
75 changes: 65 additions & 10 deletions packages/filesystem/src/browser/file-dialog/file-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { FileDialogWidget } from './file-dialog-widget';
import { FileDialogTreeFiltersRenderer, FileDialogTreeFilters } from './file-dialog-tree-filters-renderer';
import URI from '@theia/core/lib/common/uri';
import { Panel } from '@phosphor/widgets';
import { FileService } from '../file-service';

export const OpenFileDialogFactory = Symbol('OpenFileDialogFactory');
export interface OpenFileDialogFactory {
Expand All @@ -43,6 +44,7 @@ export const NAVIGATION_PANEL_CLASS = 'theia-NavigationPanel';
export const NAVIGATION_BACK_CLASS = 'theia-NavigationBack';
export const NAVIGATION_FORWARD_CLASS = 'theia-NavigationForward';
export const NAVIGATION_HOME_CLASS = 'theia-NavigationHome';
export const NAVIGATION_UP_CLASS = 'theia-NavigationUp';
export const NAVIGATION_LOCATION_LIST_PANEL_CLASS = 'theia-LocationListPanel';

export const FILTERS_PANEL_CLASS = 'theia-FiltersPanel';
Expand All @@ -54,6 +56,7 @@ export const FILENAME_LABEL_CLASS = 'theia-FileNameLabel';
export const FILENAME_TEXTFIELD_CLASS = 'theia-FileNameTextField';

export const CONTROL_PANEL_CLASS = 'theia-ControlPanel';
export const TOOLBAR_ITEM_TRANSFORM_TIMEOUT = 100;

export class FileDialogProps extends DialogProps {

Expand Down Expand Up @@ -116,13 +119,15 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
protected readonly back: HTMLSpanElement;
protected readonly forward: HTMLSpanElement;
protected readonly home: HTMLSpanElement;
protected readonly up: HTMLSpanElement;
protected readonly locationListRenderer: LocationListRenderer;
protected readonly treeFiltersRenderer: FileDialogTreeFiltersRenderer | undefined;
protected readonly treePanel: Panel;

constructor(
@inject(FileDialogProps) readonly props: FileDialogProps,
@inject(FileDialogWidget) readonly widget: FileDialogWidget
@inject(FileDialogWidget) readonly widget: FileDialogWidget,
@inject(FileService) readonly fileService: FileService
) {
super(props);
this.treePanel = new Panel();
Expand All @@ -145,8 +150,13 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
navigationPanel.appendChild(this.home = createIconButton('fa', 'fa-home'));
this.home.classList.add(NAVIGATION_HOME_CLASS);
this.home.title = 'Go To Initial Location';
navigationPanel.appendChild(this.up = createIconButton('fa', 'fa-level-up'));
this.up.classList.add(NAVIGATION_UP_CLASS);
this.up.title = 'Navigate Up One Directory';

this.locationListRenderer = this.createLocationListRenderer();
const locationListRendererHost = document.createElement('div');
this.locationListRenderer = this.createLocationListRenderer(locationListRendererHost);
this.toDispose.push(this.locationListRenderer);
this.locationListRenderer.host.classList.add(NAVIGATION_LOCATION_LIST_PANEL_CLASS);
navigationPanel.appendChild(this.locationListRenderer.host);

Expand All @@ -157,8 +167,8 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
return this.widget.model;
}

protected createLocationListRenderer(): LocationListRenderer {
return new LocationListRenderer(this.model);
protected createLocationListRenderer(host?: HTMLElement): LocationListRenderer {
return new LocationListRenderer(this.model, this.fileService, host);
}

protected createFileTreeFiltersRenderer(): FileDialogTreeFiltersRenderer | undefined {
Expand All @@ -176,6 +186,7 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
setEnabled(this.home, !!this.model.initialLocation
&& !!this.model.location
&& this.model.initialLocation.toString() !== this.model.location.toString());
setEnabled(this.up, this.model.canNavigateUpward());
this.locationListRenderer.render();

if (this.treeFiltersRenderer) {
Expand All @@ -185,6 +196,28 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
this.widget.update();
}

protected handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement || this.targetIsDirectoryInput(event.target) || this.targetIsInputToggle(event.target)) {
return false;
}
this.accept();
}

protected handleEscape(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement || this.targetIsDirectoryInput(event.target)) {
return false;
}
this.close();
}

protected targetIsDirectoryInput(target: EventTarget | null): boolean {
return target instanceof HTMLInputElement && target.classList.contains(LocationListRenderer.Styles.LOCATION_TEXT_INPUT_CLASS);
}

protected targetIsInputToggle(target: EventTarget | null): boolean {
return target instanceof HTMLSpanElement && target.classList.contains(LocationListRenderer.Styles.LOCATION_INPUT_TOGGLE_CLASS);
}

protected appendFiltersPanel(): void {
if (this.treeFiltersRenderer) {
const filtersPanel = document.createElement('div');
Expand Down Expand Up @@ -216,16 +249,36 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
this.appendCloseButton('Cancel');
this.appendAcceptButton(this.getAcceptButtonLabel());

this.addKeyListener(this.back, Key.ENTER, () => this.model.navigateBackward(), 'click');
this.addKeyListener(this.forward, Key.ENTER, () => this.model.navigateForward(), 'click');
this.addKeyListener(this.back, Key.ENTER, () => {
this.addTransformEffectToIcon(this.back);
this.model.navigateBackward();
}, 'click');

this.addKeyListener(this.forward, Key.ENTER, () => {
this.addTransformEffectToIcon(this.forward);
this.model.navigateForward();
}, 'click');
this.addKeyListener(this.home, Key.ENTER, () => {
this.addTransformEffectToIcon(this.home);
if (this.model.initialLocation) {
this.model.location = this.model.initialLocation;
}
}, 'click');
this.addKeyListener(this.up, Key.ENTER, () => {
this.addTransformEffectToIcon(this.up);
if (this.model.location) {
this.model.location = this.model.location.parent;
}
}, 'click');
super.onAfterAttach(msg);
}

protected addTransformEffectToIcon(element: HTMLSpanElement): void {
const icon = element.getElementsByTagName('i')[0];
icon.classList.add('active');
setTimeout(() => icon.classList.remove('active'), TOOLBAR_ITEM_TRANSFORM_TIMEOUT);
}

protected abstract getAcceptButtonLabel(): string;

protected onActivateRequest(msg: Message): void {
Expand All @@ -239,9 +292,10 @@ export class OpenFileDialog extends FileDialog<MaybeArray<FileStatNode>> {

constructor(
@inject(OpenFileDialogProps) readonly props: OpenFileDialogProps,
@inject(FileDialogWidget) readonly widget: FileDialogWidget
@inject(FileDialogWidget) readonly widget: FileDialogWidget,
@inject(FileService) readonly fileService: FileService
) {
super(props, widget);
super(props, widget, fileService);
if (props.canSelectFiles !== undefined) {
this.widget.disableFileSelection = !props.canSelectFiles;
}
Expand Down Expand Up @@ -288,9 +342,10 @@ export class SaveFileDialog extends FileDialog<URI | undefined> {

constructor(
@inject(SaveFileDialogProps) readonly props: SaveFileDialogProps,
@inject(FileDialogWidget) readonly widget: FileDialogWidget
@inject(FileDialogWidget) readonly widget: FileDialogWidget,
@inject(FileService) readonly fileService: FileService
) {
super(props, widget);
super(props, widget, fileService);
widget.addClass(SAVE_DIALOG_CLASS);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/filesystem/src/browser/file-tree/file-tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export class FileTreeModel extends TreeModelImpl implements LocationService {
const node = DirNode.createRoot(fileStat);
this.navigateTo(node);
}
}).catch(() => {
// no-op, allow failures for file dialog text input
});
} else {
this.navigateTo(undefined);
Expand Down
Loading