瀏覽代碼

Manual backport of PR #10727: Removed debug switch (#11185)

Co-authored-by: Piyush Jain <pijain@amazon.com>
Piyush Jain 3 年之前
父節點
當前提交
81b1a31c05

+ 92 - 4
packages/apputils/src/toolbar.tsx

@@ -672,6 +672,10 @@ export namespace ToolbarButtonComponent {
     tooltip?: string;
     onClick?: () => void;
     enabled?: boolean;
+    pressed?: boolean;
+    pressedIcon?: LabIcon.IMaybeResolvable;
+    pressedTooltip?: string;
+    disabledTooltip?: string;
 
     /**
      * Trigger the button on the actual onClick event rather than onMouseDown.
@@ -719,6 +723,16 @@ export function ToolbarButtonComponent(props: ToolbarButtonComponent.IProps) {
     }
   };
 
+  const getTooltip = () => {
+    if (props.enabled === false && props.disabledTooltip) {
+      return props.disabledTooltip;
+    } else if (props.pressed && props.pressedTooltip) {
+      return props.pressedTooltip;
+    } else {
+      return props.tooltip || props.iconLabel;
+    }
+  };
+
   return (
     <Button
       className={
@@ -726,18 +740,20 @@ export function ToolbarButtonComponent(props: ToolbarButtonComponent.IProps) {
           ? props.className + ' jp-ToolbarButtonComponent'
           : 'jp-ToolbarButtonComponent'
       }
+      aria-pressed={props.pressed}
+      aria-disabled={props.enabled === false}
       disabled={props.enabled === false}
       onClick={props.actualOnClick ?? false ? handleClick : undefined}
       onMouseDown={
         !(props.actualOnClick ?? false) ? handleMouseDown : undefined
       }
       onKeyDown={handleKeyDown}
-      title={props.tooltip || props.iconLabel}
+      title={getTooltip()}
       minimal
     >
       {(props.icon || props.iconClass) && (
         <LabIcon.resolveReact
-          icon={props.icon}
+          icon={props.pressed ? props.pressedIcon : props.icon}
           iconClass={
             // add some extra classes for proper support of icons-as-css-background
             classes(props.iconClass, 'jp-Icon')
@@ -774,10 +790,82 @@ export class ToolbarButton extends ReactWidget {
   constructor(private props: ToolbarButtonComponent.IProps = {}) {
     super();
     addToolbarButtonClass(this);
+    this._enabled = props.enabled ?? true;
+    this._pressed = this._enabled! && (props.pressed ?? false);
+    this._onClick = props.onClick!;
   }
-  render() {
-    return <ToolbarButtonComponent {...this.props} />;
+
+  /**
+   * Sets the pressed state for the button
+   * @param value true if button is pressed, false otherwise
+   */
+  set pressed(value: boolean) {
+    if (this.enabled && value !== this._pressed) {
+      this._pressed = value;
+      this.update();
+    }
+  }
+
+  /**
+   * Returns true if button is pressed, false otherwise
+   */
+  get pressed(): boolean {
+    return this._pressed!;
+  }
+
+  /**
+   * Sets the enabled state for the button
+   * @param value true to enable the button, false otherwise
+   */
+  set enabled(value: boolean) {
+    if (value != this._enabled) {
+      this._enabled = value;
+      if (!this._enabled) {
+        this._pressed = false;
+      }
+      this.update();
+    }
   }
+
+  /**
+   * Returns true if button is enabled, false otherwise
+   */
+  get enabled(): boolean {
+    return this._enabled;
+  }
+
+  /**
+   * Sets the click handler for the button
+   * @param value click handler
+   */
+  set onClick(value: () => void) {
+    if (value !== this._onClick) {
+      this._onClick = value;
+      this.update();
+    }
+  }
+
+  /**
+   * Returns the click handler for the button
+   */
+  get onClick() {
+    return this._onClick!;
+  }
+
+  render(): JSX.Element {
+    return (
+      <ToolbarButtonComponent
+        {...this.props}
+        pressed={this.pressed}
+        enabled={this.enabled}
+        onClick={this.onClick}
+      />
+    );
+  }
+
+  private _pressed: boolean;
+  private _enabled: boolean;
+  private _onClick: () => void;
 }
 
 /**

+ 113 - 23
packages/apputils/test/toolbar.spec.ts

@@ -18,6 +18,7 @@ import { CommandRegistry } from '@lumino/commands';
 import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
 import { PanelLayout, Widget } from '@lumino/widgets';
 import { simulate } from 'simulate-event';
+import { bugDotIcon, bugIcon } from '@jupyterlab/ui-components';
 
 const server = new JupyterServer();
 
@@ -530,28 +531,117 @@ describe('@jupyterlab/apputils', () => {
       });
     });
 
-    //   describe('#onAfterAttach()', () => {
-    //     it('should add event listeners to the node', () => {
-    //       const button = new LogToolbarButton();
-    //       Widget.attach(button, document.body);
-    //       expect(button.methods).to.contain('onAfterAttach');
-    //       simulate(button.node, 'click');
-    //       expect(button.events).to.contain('click');
-    //       button.dispose();
-    //     });
-    //   });
-
-    //   describe('#onBeforeDetach()', () => {
-    //     it('should remove event listeners from the node', async () => {
-    //       const button = new LogToolbarButton();
-    //       Widget.attach(button, document.body);
-    //       await framePromise();
-    //       Widget.detach(button);
-    //       expect(button.methods).to.contain('onBeforeDetach');
-    //       simulate(button.node, 'click');
-    //       expect(button.events).to.not.contain('click');
-    //       button.dispose();
-    //     });
-    //   });
+    describe('#pressed()', () => {
+      it('should update the pressed state', async () => {
+        const widget = new ToolbarButton({
+          icon: bugIcon,
+          tooltip: 'tooltip',
+          pressedTooltip: 'pressed tooltip',
+          pressedIcon: bugDotIcon
+        });
+        Widget.attach(widget, document.body);
+        await framePromise();
+        const button = widget.node.firstChild as HTMLElement;
+        expect(widget.pressed).toBe(false);
+        expect(button.title).toBe('tooltip');
+        expect(button.getAttribute('aria-pressed')).toEqual('false');
+        let icon = button.querySelectorAll('svg');
+        expect(icon[0].getAttribute('data-icon')).toEqual('ui-components:bug');
+        widget.pressed = true;
+        await framePromise();
+        expect(widget.pressed).toBe(true);
+        expect(button.title).toBe('pressed tooltip');
+        expect(button.getAttribute('aria-pressed')).toEqual('true');
+        icon = button.querySelectorAll('svg');
+        expect(icon[0].getAttribute('data-icon')).toEqual(
+          'ui-components:bug-dot'
+        );
+        widget.dispose();
+      });
+
+      it('should not have the pressed state when not enabled', async () => {
+        const widget = new ToolbarButton({
+          icon: bugIcon,
+          tooltip: 'tooltip',
+          pressedTooltip: 'pressed tooltip',
+          disabledTooltip: 'disabled tooltip',
+          pressedIcon: bugDotIcon,
+          enabled: false
+        });
+        Widget.attach(widget, document.body);
+        await framePromise();
+        const button = widget.node.firstChild as HTMLElement;
+        expect(widget.pressed).toBe(false);
+        expect(button.title).toBe('disabled tooltip');
+        expect(button.getAttribute('aria-pressed')).toEqual('false');
+        widget.pressed = true;
+        await framePromise();
+        expect(widget.pressed).toBe(false);
+        expect(button.title).toBe('disabled tooltip');
+        expect(button.getAttribute('aria-pressed')).toEqual('false');
+        const icon = button.querySelectorAll('svg');
+        expect(icon[0].getAttribute('data-icon')).toEqual('ui-components:bug');
+        widget.dispose();
+      });
+    });
+
+    describe('#enabled()', () => {
+      it('should update the enabled state', async () => {
+        const widget = new ToolbarButton({
+          icon: bugIcon,
+          tooltip: 'tooltip',
+          pressedTooltip: 'pressed tooltip',
+          disabledTooltip: 'disabled tooltip',
+          pressedIcon: bugDotIcon
+        });
+        Widget.attach(widget, document.body);
+        await framePromise();
+        const button = widget.node.firstChild as HTMLElement;
+        expect(widget.enabled).toBe(true);
+        expect(widget.pressed).toBe(false);
+        expect(button.getAttribute('aria-disabled')).toEqual('false');
+
+        widget.pressed = true;
+        await framePromise();
+        expect(widget.pressed).toBe(true);
+
+        widget.enabled = false;
+        await framePromise();
+        expect(widget.enabled).toBe(false);
+        expect(widget.pressed).toBe(false);
+        expect(button.getAttribute('aria-disabled')).toEqual('true');
+        widget.dispose();
+      });
+    });
+
+    describe('#onClick()', () => {
+      it('should update the onClick state', async () => {
+        let mockCalled = false;
+        const mockOnClick = () => {
+          mockCalled = true;
+        };
+        const widget = new ToolbarButton({
+          icon: bugIcon,
+          tooltip: 'tooltip',
+          onClick: mockOnClick
+        });
+        Widget.attach(widget, document.body);
+        await framePromise();
+        simulate(widget.node.firstChild as HTMLElement, 'mousedown');
+        expect(mockCalled).toBe(true);
+
+        mockCalled = false;
+        let mockUpdatedCalled = false;
+        const mockOnClickUpdated = () => {
+          mockUpdatedCalled = true;
+        };
+        widget.onClick = mockOnClickUpdated;
+        await framePromise();
+        simulate(widget.node.firstChild as HTMLElement, 'mousedown');
+        expect(mockCalled).toBe(false);
+        expect(mockUpdatedCalled).toBe(true);
+        widget.dispose();
+      });
+    });
   });
 });

+ 67 - 76
packages/debugger/src/handler.ts

@@ -23,7 +23,7 @@ import { Kernel, KernelMessage, Session } from '@jupyterlab/services';
 
 import { ITranslator, nullTranslator } from '@jupyterlab/translation';
 
-import { bugIcon, Switch } from '@jupyterlab/ui-components';
+import { bugDotIcon, bugIcon } from '@jupyterlab/ui-components';
 
 import { Debugger } from './debugger';
 
@@ -44,53 +44,51 @@ import { NotebookHandler } from './handlers/notebook';
 function updateIconButton(
   widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
   onClick: () => void,
-  translator?: ITranslator
+  enabled?: boolean,
+  pressed?: boolean,
+  translator: ITranslator = nullTranslator
 ): ToolbarButton {
-  translator = translator || nullTranslator;
   const trans = translator.load('jupyterlab');
   const icon = new ToolbarButton({
     className: 'jp-DebuggerBugButton',
     icon: bugIcon,
-    tooltip: trans.__('Enable / Disable Debugger'),
+    tooltip: trans.__('Enable Debugger'),
+    pressedIcon: bugDotIcon,
+    pressedTooltip: trans.__('Disable Debugger'),
+    disabledTooltip: trans.__(
+      'Select a kernel that supports debugging to enable debugger'
+    ),
+    enabled,
+    pressed,
     onClick
   });
-  widget.toolbar.addItem('debugger-icon', icon);
+  widget.toolbar.insertBefore('kernelName', 'debugger-icon', icon);
 
   return icon;
 }
 
 /**
- * Add a toggle button to the widget toolbar to enable and disable debugging.
+ * Updates button state to on/off,
+ * adds/removes css class to update styling
  *
- * @param widget The widget to add the debug toolbar button to.
- * @param onClick The callback when the toolbar button is clicked.
+ * @param widget the debug button widget
+ * @param pressed true if pressed, false otherwise
+ * @param enabled true if widget enabled, false otherwise
+ * @param onClick click handler
  */
-function updateToggleButton(
-  widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
-  sessionStarted: boolean,
-  onClick: () => void,
-  translator?: ITranslator
-): Switch {
-  translator = translator || nullTranslator;
-  const trans = translator.load('jupyterlab');
-
-  const button = new Switch();
-  button.id = 'jp-debugger';
-  button.value = sessionStarted;
-  button.caption = trans.__('Enable / Disable Debugger');
-  button.handleEvent = (event: Event) => {
-    event.preventDefault();
-    switch (event.type) {
-      case 'click':
-        onClick();
-        break;
-      default:
-        break;
+function updateIconButtonState(
+  widget: ToolbarButton,
+  pressed: boolean,
+  enabled: boolean = true,
+  onClick?: () => void
+) {
+  if (widget) {
+    widget.enabled = enabled;
+    widget.pressed = pressed;
+    if (onClick) {
+      widget.onClick = onClick;
     }
-  };
-
-  widget.toolbar.addItem('debugger-button', button);
-  return button;
+  }
 }
 
 /**
@@ -288,37 +286,42 @@ export class DebuggerHandler {
       updateAttribute();
     };
 
-    const addToolbarButton = (): void => {
-      if (!this._iconButtons[widget.id]) {
+    const addToolbarButton = (enabled: boolean = true): void => {
+      const debugButton = this._iconButtons[widget.id];
+      if (!debugButton) {
         this._iconButtons[widget.id] = updateIconButton(
           widget,
-          toggleDebugging
+          toggleDebugging,
+          this._service.isStarted,
+          enabled
         );
-      }
-      if (!this._toggleButtons[widget.id]) {
-        this._toggleButtons[widget.id] = updateToggleButton(
-          widget,
+      } else {
+        updateIconButtonState(
+          debugButton,
           this._service.isStarted,
+          enabled,
           toggleDebugging
         );
       }
-      this._toggleButtons[widget.id]!.value = this._service.isStarted;
     };
 
-    const removeToolbarButton = (): void => {
-      if (!this._iconButtons[widget.id]) {
-        return;
-      } else {
-        this._iconButtons[widget.id]!.dispose();
-        delete this._iconButtons[widget.id];
-      }
+    const isDebuggerOn = (): boolean => {
+      return (
+        this._service.isStarted &&
+        this._previousConnection?.id === connection?.id
+      );
+    };
 
-      if (!this._toggleButtons[widget.id]) {
-        return;
-      } else {
-        this._toggleButtons[widget.id]!.dispose();
-        delete this._toggleButtons[widget.id];
-      }
+    const stopDebugger = async (): Promise<void> => {
+      this._service.session!.connection = connection;
+      await this._service.stop();
+    };
+
+    const startDebugger = async (): Promise<void> => {
+      this._service.session!.connection = connection;
+      this._previousConnection = connection;
+      await this._service.restoreState(true);
+      await this._service.displayDefinedVariables();
     };
 
     const toggleDebugging = async (): Promise<void> => {
@@ -326,33 +329,23 @@ export class DebuggerHandler {
       if (!hasFocus()) {
         return;
       }
-
-      if (
-        this._service.isStarted &&
-        this._previousConnection?.id === connection?.id
-      ) {
-        if (this._toggleButtons[widget.id]) {
-          this._toggleButtons[widget.id]!.value = false;
-        }
-        this._service.session!.connection = connection;
-        await this._service.stop();
+      const debugButton = this._iconButtons[widget.id]!;
+      if (isDebuggerOn()) {
+        await stopDebugger();
         removeHandlers();
+        updateIconButtonState(debugButton, false);
       } else {
-        if (this._toggleButtons[widget.id]) {
-          this._toggleButtons[widget.id]!.value = true;
-        }
-        this._service.session!.connection = connection;
-        this._previousConnection = connection;
-        await this._service.restoreState(true);
-        await this._service.displayDefinedVariables();
+        await startDebugger();
         createHandler();
+        updateIconButtonState(debugButton, true);
       }
     };
 
+    addToolbarButton(false);
     const debuggingEnabled = await this._service.isAvailable(connection);
     if (!debuggingEnabled) {
       removeHandlers();
-      removeToolbarButton();
+      updateIconButtonState(this._iconButtons[widget.id]!, false, false);
       return;
     }
 
@@ -369,7 +362,8 @@ export class DebuggerHandler {
     if (this._service.isStarted && !this._service.hasStoppedThreads()) {
       await this._service.displayDefinedVariables();
     }
-    addToolbarButton();
+
+    updateIconButtonState(this._iconButtons[widget.id]!, false, true);
 
     // check the state of the debug session
     if (!this._service.isStarted) {
@@ -430,9 +424,6 @@ export class DebuggerHandler {
   private _iconButtons: {
     [id: string]: ToolbarButton | undefined;
   } = {};
-  private _toggleButtons: {
-    [id: string]: Switch | undefined;
-  } = {};
 }
 
 /**

+ 4 - 0
packages/debugger/style/base.css

@@ -19,3 +19,7 @@
 #jp-debugger .jp-switch-label {
   margin-right: 0px;
 }
+
+.jp-DebuggerBugButton[aria-pressed='true'] path {
+  fill: var(--jp-warn-color0);
+}

+ 2 - 0
packages/ui-components/src/icon/iconimports.ts

@@ -9,6 +9,7 @@ import { LabIcon } from './labicon';
 
 // icon svg import statements
 import addSvgstr from '../../style/icons/toolbar/add.svg';
+import bugDotSvgstr from '../../style/icons/toolbar/bug-dot.svg';
 import bugSvgstr from '../../style/icons/toolbar/bug.svg';
 import buildSvgstr from '../../style/icons/sidebar/build.svg';
 import caretDownEmptySvgstr from '../../style/icons/arrow/caret-down-empty.svg';
@@ -89,6 +90,7 @@ import yamlSvgstr from '../../style/icons/filetype/yaml.svg';
 
 // LabIcon instance construction
 export const addIcon = new LabIcon({ name: 'ui-components:add', svgstr: addSvgstr });
+export const bugDotIcon = new LabIcon({ name: 'ui-components:bug-dot', svgstr: bugDotSvgstr });
 export const bugIcon = new LabIcon({ name: 'ui-components:bug', svgstr: bugSvgstr });
 export const buildIcon = new LabIcon({ name: 'ui-components:build', svgstr: buildSvgstr });
 export const caretDownEmptyIcon = new LabIcon({ name: 'ui-components:caret-down-empty', svgstr: caretDownEmptySvgstr });

+ 4 - 0
packages/ui-components/style/deprecated.css

@@ -13,6 +13,7 @@
 
 :root {
   --jp-icon-add: url('icons/toolbar/add.svg');
+  --jp-icon-bug-dot: url('icons/toolbar/bug-dot.svg');
   --jp-icon-bug: url('icons/toolbar/bug.svg');
   --jp-icon-build: url('icons/sidebar/build.svg');
   --jp-icon-caret-down-empty-thin: url('icons/arrow/caret-down-empty-thin.svg');
@@ -97,6 +98,9 @@
 .jp-AddIcon {
   background-image: var(--jp-icon-add);
 }
+.jp-BugDotIcon {
+  background-image: var(--jp-icon-bug-dot);
+}
 .jp-BugIcon {
   background-image: var(--jp-icon-bug);
 }

+ 6 - 0
packages/ui-components/style/icons/toolbar/bug-dot.svg

@@ -0,0 +1,6 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+        <path fill-rule="evenodd" clip-rule="evenodd" d="M17.19 8H20V10H17.91C17.96 10.33 18 10.66 18 11V12H20V14H18.5H18V14.0275C15.75 14.2762 14 16.1837 14 18.5C14 19.208 14.1635 19.8779 14.4549 20.4739C13.7063 20.8117 12.8757 21 12 21C9.78 21 7.85 19.79 6.81 18H4V16H6.09C6.04 15.67 6 15.34 6 15V14H4V12H6V11C6 10.66 6.04 10.33 6.09 10H4V8H6.81C7.26 7.22 7.88 6.55 8.62 6.04L7 4.41L8.41 3L10.59 5.17C11.04 5.06 11.51 5 12 5C12.49 5 12.96 5.06 13.42 5.17L15.59 3L17 4.41L15.37 6.04C16.12 6.55 16.74 7.22 17.19 8ZM10 16H14V14H10V16ZM10 12H14V10H10V12Z" fill="#616161"/>
+        <path d="M22 18.5C22 20.433 20.433 22 18.5 22C16.567 22 15 20.433 15 18.5C15 16.567 16.567 15 18.5 15C20.433 15 22 16.567 22 18.5Z" fill="#616161"/>
+    </g>
+</svg>