Skip to content

Commit

Permalink
Add sash option for widget resize (#10441)
Browse files Browse the repository at this point in the history
- Apply focus to a widget handle when you hover over it
- 'sash.hoverDelay' preference controls the hover feedback delay in milliseconds
- 'sash.size' preference controls the feedback area size in pixels for the dragging area
- Refactor the code for creating and manipulating dynamic stylesheets

Signed-off-by: Kaiyue Pan <kaiyue.pan@ericsson.com>
  • Loading branch information
kaiyue0329 authored Dec 15, 2021
1 parent 0836c7a commit 73346de
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 88 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [core, editor, editor-preview] additional commands added to tabbar context menu for editor widgets. [#10394](https://github.com/eclipse-theia/theia/pull/10394)
- [preferences] Updated `AbstractResourcePreferenceProvider` to handle multiple preference settings in the same tick and handle open preference files. It will save the file exactly once, and prompt the user if the file is dirty when a programmatic setting is attempted. [#7775](https://github.com/eclipse-theia/theia/pull/7775)
- [core] `WindowService` and `ElectronMainApplication` updated to allow for asynchronous pre-exit code in Electron. [#10379](https://github.com/eclipse-theia/theia/pull/10379)
- [core] added sash option for widget resize [#10441](https://github.com/eclipse-theia/theia/pull/10441)

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

Expand All @@ -28,6 +29,7 @@
- [plugin] renamed `WebviewThemeData.activeTheme` to `activeThemeType` [#10493](https://github.com/eclipse-theia/theia/pull/10493)
- [plugin] removed the application prop `resolveSystemPlugins`, builtin plugins should now be resolved at build time [#10353](https://github.com/eclipse-theia/theia/pull/10353)
- [core/shared] removed `vscode-languageserver-types`; use `vscode-languageserver-protocol` instead. [#10500](https://github.com/eclipse-theia/theia/pull/10500)
- [editor] moved the utilities for creating and manipulating dynamic stylesheets from `editor-decoration-style.ts` to `decoration-style.ts` in `core`. Each namespace now has its indepednent style sheet. Only one rule should exist for a given selector in the provided stylesheet. [#10441](https://github.com/eclipse-theia/theia/pull/10441)

## v1.20.0 - 11/25/2021

Expand Down
22 changes: 21 additions & 1 deletion packages/core/src/browser/common-frontend-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { CurrentWidgetCommandAdapter } from './shell/current-widget-command-adap
import { ConfirmDialog, confirmExit, Dialog } from './dialogs';
import { WindowService } from './window/window-service';
import { FrontendApplicationConfigProvider } from './frontend-application-config-provider';
import { DecorationStyle } from './decoration-style';

export namespace CommonMenus {

Expand Down Expand Up @@ -308,6 +309,8 @@ export const RECENT_COMMANDS_STORAGE_KEY = 'commands';
@injectable()
export class CommonFrontendContribution implements FrontendApplicationContribution, MenuContribution, CommandContribution, KeybindingContribution, ColorContribution {

protected commonDecorationsStyleSheet: CSSStyleSheet = DecorationStyle.createStyleSheet('coreCommonDecorationsStyle');

constructor(
@inject(ApplicationShell) protected readonly shell: ApplicationShell,
@inject(SelectionService) protected readonly selectionService: SelectionService,
Expand Down Expand Up @@ -378,6 +381,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
this.updateStyles();
this.updateThemeFromPreference('workbench.colorTheme');
this.updateThemeFromPreference('workbench.iconTheme');
this.preferences.ready.then(() => this.setSashProperties());
this.preferences.onPreferenceChanged(e => this.handlePreferenceChange(e, app));
this.themeService.onDidColorThemeChange(() => this.updateThemePreference('workbench.colorTheme'));
this.iconThemes.onDidChangeCurrent(() => this.updateThemePreference('workbench.iconTheme'));
Expand Down Expand Up @@ -466,9 +470,24 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
}
break;
}
case 'workbench.sash.hoverDelay':
case 'workbench.sash.size': {
this.setSashProperties();
break;
}
}
}

protected setSashProperties(): void {
const sashRule = `:root {
--theia-sash-hoverDelay: ${this.preferences['workbench.sash.hoverDelay']}ms;
--theia-sash-width: ${this.preferences['workbench.sash.size']}px;
}`;

DecorationStyle.deleteStyleRule(':root', this.commonDecorationsStyleSheet);
this.commonDecorationsStyleSheet.insertRule(sashRule);
}

onStart(): void {
this.storageService.getData<{ recent: Command[] }>(RECENT_COMMANDS_STORAGE_KEY, { recent: [] })
.then(tasks => this.commandRegistry.recent = tasks.recent);
Expand Down Expand Up @@ -1100,7 +1119,8 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
// if not yet contributed by Monaco, check runtime css variables to learn
{ id: 'selection.background', defaults: { dark: '#217daf', light: '#c0dbf1' }, description: 'Overall border color for focused elements. This color is only used if not overridden by a component.' },
{ id: 'icon.foreground', defaults: { dark: '#C5C5C5', light: '#424242', hc: '#FFFFFF' }, description: 'The default color for icons in the workbench.' },

{ id: 'sash.hoverBorder', defaults: { dark: Color.transparent('focusBorder', 0.99), light: Color.transparent('focusBorder', 0.99), hc: Color.transparent('focusBorder', 0.99) }, description: 'The hover border color for draggable sashes.' },
{ id: 'sash.activeBorder', defaults: { dark: 'focusBorder', light: 'focusBorder', hc: 'focusBorder' }, description: 'The active border color for draggable sashes.' },
// Window border colors should be aligned with https://code.visualstudio.com/api/references/theme-color#window-border
{
id: 'window.activeBorder', defaults: {
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/browser/core-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,20 @@ export const corePreferenceSchema: PreferenceSchema = {
default: isOSX ? 1500 : 500,
description: nls.localizeByDefault('Controls the delay in milliseconds after which the hover is shown.')
},
'workbench.sash.hoverDelay': {
type: 'number',
default: 300,
minimum: 0,
maximum: 2000,
description: nls.localizeByDefault('Controls the hover feedback delay in milliseconds of the dragging area in between views/editors.')
},
'workbench.sash.size': {
type: 'number',
default: 4,
minimum: 1,
maximum: 20,
description: nls.localizeByDefault('Controls the feedback area size in pixels of the dragging area in between views/editors. Set it to a larger value if needed.')
},
}
};

Expand All @@ -154,6 +168,8 @@ export interface CoreConfiguration {
'workbench.statusBar.visible': boolean;
'workbench.tree.renderIndentGuides': 'onHover' | 'none' | 'always';
'workbench.hover.delay': number;
'workbench.sash.hoverDelay': number;
'workbench.sash.size': number;
}

export const CorePreferenceContribution = Symbol('CorePreferenceContribution');
Expand Down
61 changes: 61 additions & 0 deletions packages/core/src/browser/decoration-style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/********************************************************************************
* Copyright (C) 2021 Ericsson 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
********************************************************************************/

export namespace DecorationStyle {

export function createStyleSheet(styleId: string, container: HTMLElement = document.getElementsByTagName('head')[0]): CSSStyleSheet {
const style = document.createElement('style');
style.id = styleId;
style.type = 'text/css';
style.media = 'screen';
style.appendChild(document.createTextNode('')); // trick for webkit
container.appendChild(style);
return <CSSStyleSheet>style.sheet;
}

function getRuleIndex(selector: string, styleSheet: CSSStyleSheet): number {
return Array.from(styleSheet.cssRules || styleSheet.rules).findIndex(rule => rule.type === CSSRule.STYLE_RULE && (<CSSStyleRule>rule).selectorText === selector);
}

export function getOrCreateStyleRule(selector: string, styleSheet: CSSStyleSheet): CSSStyleRule {
let index = getRuleIndex(selector, styleSheet);
if (index === -1) {
// The given selector does not exist in the provided independent style sheet, rule index = 0
index = styleSheet.insertRule(selector + '{}', 0);
}

const rules = styleSheet.cssRules || styleSheet.rules;
const rule = rules[index];
if (rule && rule.type === CSSRule.STYLE_RULE) {
return rule as CSSStyleRule;
}
styleSheet.deleteRule(index);
throw new Error('This function is only for CSS style rules. Other types of CSS rules are not allowed.');
}

export function deleteStyleRule(selector: string, styleSheet: CSSStyleSheet): void {
if (!styleSheet) {
return;
}

// In general, only one rule exists for a given selector in the provided independent style sheet
const index = getRuleIndex(selector, styleSheet);
if (index !== -1) {
styleSheet.deleteRule(index);
}
}
}

1 change: 1 addition & 0 deletions packages/core/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ export * from './view-container';
export * from './breadcrumbs';
export * from './tooltip-service';
export * from './markdown-renderer';
export * from './decoration-style';
7 changes: 5 additions & 2 deletions packages/core/src/browser/shell/application-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -973,8 +973,11 @@ export class ApplicationShell extends Widget {
if (panel instanceof TheiaDockPanel) {
panel.markAsCurrent(newValue.title);
}
// Set the z-index so elements with `position: fixed` contained in the active widget are displayed correctly
this.setZIndex(newValue.node, '1');
// Add checks to ensure that the 'sash' for left panel is displayed correctly
if (newValue.node.className === 'p-Widget theia-view-container p-DockPanel-widget') {
// Set the z-index so elements with `position: fixed` contained in the active widget are displayed correctly
this.setZIndex(newValue.node, '1');
}

// activate another widget if an active widget will be closed
const onCloseRequest = newValue['onCloseRequest'];
Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/browser/style/dockpanel.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,36 @@

.p-DockPanel-handle[data-orientation='vertical'] {
min-height: var(--theia-border-width);
z-index: 3;
}

.p-DockPanel-handle[data-orientation='horizontal'] {
min-width: var(--theia-border-width);
}

.p-DockPanel-handle[data-orientation='horizontal']::after {
min-width: var(--theia-sash-width);
transform: translateX(0%);
left: calc(-1*var(--theia-sash-width)/2);
}

.p-DockPanel-handle[data-orientation='vertical']::after {
min-height: var(--theia-sash-width);
width: 100%;
transform: translateY(0%);
top: calc(-1*var(--theia-sash-width)/2);
}

.p-DockPanel-handle:hover::after {
background-color: var(--theia-sash-hoverBorder);
transition-delay: var(--theia-sash-hoverDelay);
}

.p-DockPanel-handle:active::after {
background-color: var(--theia-sash-activeBorder);
transition-delay: 0s !important;
}

.p-DockPanel-overlay {
background: var(--theia-editorGroup-dropBackground);
border: var(--theia-border-width) dashed var(--theia-contrastActiveBorder);
Expand Down
31 changes: 28 additions & 3 deletions packages/core/src/browser/style/view-container.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,41 @@
min-height: var(--theia-content-line-height);
}

.theia-view-container > .p-SplitPanel > .p-SplitPanel-handle:after {
background-color: var(--theia-sideBarSectionHeader-border);
.theia-view-container > .p-SplitPanel > .p-SplitPanel-handle::after {
min-height: 2px;
min-width: 2px
min-width: 2px;
}

.theia-view-container > .p-SplitPanel > .p-SplitPanel-handle {
background-color: var(--theia-sideBarSectionHeader-border);
}

.p-SplitPanel > .p-SplitPanel-handle:hover::after {
background-color: var(--theia-sash-hoverBorder);
transition-delay: var(--theia-sash-hoverDelay);
}

.p-SplitPanel > .p-SplitPanel-handle:active::after {
background-color: var(--theia-sash-activeBorder);
transition-delay: 0s !important;
}

.p-SplitPanel[data-orientation='horizontal'] > .p-SplitPanel-handle::after {
min-width: var(--theia-sash-width);
}

.p-SplitPanel[data-orientation='vertical'] > .p-SplitPanel-handle::after {
min-height: var(--theia-sash-width);
}

.p-SplitPanel[data-orientation='vertical'] > .p-SplitPanel-handle.sash-hidden {
visibility: hidden;
}

.p-SplitPanel[data-orientation='vertical'] > .p-SplitPanel-handle.sash-hidden::after {
min-height: 0px;
}

.theia-view-container .part {
height: 100%;
}
Expand Down
30 changes: 30 additions & 0 deletions packages/core/src/browser/view-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ export interface ViewContainerTitleOptions {
closeable?: boolean;
}

enum SashType {
TopSash = 'top-sash',
BottomSash = 'bottom-sash'
}

@injectable()
export class ViewContainerIdentifier {
id: string;
Expand Down Expand Up @@ -994,6 +999,11 @@ export class ViewContainerPart extends BaseWidget {
}
this._collapsed = collapsed;
this.node.classList.toggle('collapsed', collapsed);

// Update the sashes for the active `horizontal` container when the container is collapsed/expanded
this.updateSashState(SashType.TopSash, this.node.previousElementSibling, collapsed);
this.updateSashState(SashType.BottomSash, this.node.nextElementSibling, collapsed);

if (collapsed && this.wrapped.node.contains(document.activeElement)) {
this.header.focus();
}
Expand Down Expand Up @@ -1230,6 +1240,26 @@ export class ViewContainerPart extends BaseWidget {
}
}

protected updateSashState(sashType: SashType, sashElement: Element | null, activeContainerCollapsed: boolean): void {
if (sashElement && sashElement.className.includes('p-SplitPanel-handle')) {
// Hide the sash when the active `horizontal` container in the left panel is collapsed
sashElement.classList.toggle('sash-hidden', activeContainerCollapsed);

let adjacentContainer: Element | null;
if (sashType === SashType.TopSash) {
adjacentContainer = sashElement.previousElementSibling;
} else {
adjacentContainer = sashElement.nextElementSibling;
}

// Sash should only appear when the following two conditions are met:
// 1. the active `horizontal` container is expanded
// 2. the container that is above/below the active `horizontal` container is also expanded
if (!activeContainerCollapsed && adjacentContainer) {
sashElement.classList.toggle('sash-hidden', adjacentContainer.className.includes('collapsed'));
}
}
}
}

export namespace ViewContainerPart {
Expand Down
Loading

0 comments on commit 73346de

Please sign in to comment.