From cdbc1734b19335db7036a703375bc5d09345e22c Mon Sep 17 00:00:00 2001 From: Duc Trung LE Date: Sat, 11 Dec 2021 23:23:45 +0100 Subject: [PATCH 01/10] Prevent duplicated item in menu on widget adding --- ipyflex/flex_layout.py | 3 --- src/menuWidget.tsx | 10 ++++++---- src/reactWidget.tsx | 10 ++++++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/ipyflex/flex_layout.py b/ipyflex/flex_layout.py index 164412e..3c7687e 100644 --- a/ipyflex/flex_layout.py +++ b/ipyflex/flex_layout.py @@ -39,9 +39,6 @@ class FlexLayout(DOMWidget): _view_module = Unicode(module_name).tag(sync=True) _view_module_version = Unicode(module_version).tag(sync=True) - # children = TypedTuple( - # trait=Instance(Widget), help="List of widget children" - # ).tag(sync=True, **widget_serialization) children = Dict( key_trait=Unicode, value_trait=Instance(Widget), diff --git a/src/menuWidget.tsx b/src/menuWidget.tsx index 715e30e..53c48a9 100644 --- a/src/menuWidget.tsx +++ b/src/menuWidget.tsx @@ -34,10 +34,12 @@ export class WidgetMenu extends Component { case 'update_children': { const wName: string = payload.name; - this.setState((old) => ({ - ...old, - widgetList: [...old.widgetList, wName], - })); + if (!this.state.widgetList.includes(wName)) { + this.setState((old) => ({ + ...old, + widgetList: [...old.widgetList, wName], + })); + } } return null; diff --git a/src/reactWidget.tsx b/src/reactWidget.tsx index 04d5ebd..f2a2245 100644 --- a/src/reactWidget.tsx +++ b/src/reactWidget.tsx @@ -79,10 +79,12 @@ export class FlexWidget extends Component { case 'update_children': { const wName: string = payload.name; - this.setState((old) => ({ - ...old, - widgetList: [...old.widgetList, wName], - })); + if (!this.state.widgetList.includes(wName)) { + this.setState((old) => ({ + ...old, + widgetList: [...old.widgetList, wName], + })); + } } return null; From 302c6e1dceb2d31d751262820a0bcfa96e07f6e0 Mon Sep 17 00:00:00 2001 From: Duc Trung LE Date: Sun, 12 Dec 2021 11:16:40 +0100 Subject: [PATCH 02/10] Create widget from factory --- css/widget.css | 486 +---------------------------------------- ipyflex/flex_layout.py | 47 +++- src/menuWidget.tsx | 49 ++++- src/reactWidget.tsx | 19 +- src/utils.tsx | 7 + src/widget.tsx | 1 + src/widgetWrapper.tsx | 36 ++- 7 files changed, 150 insertions(+), 495 deletions(-) diff --git a/css/widget.css b/css/widget.css index 7315f08..544a341 100644 --- a/css/widget.css +++ b/css/widget.css @@ -30,6 +30,7 @@ position: unset !important; overflow: unset !important; height: 100%; + background: var(--jp-layout-color0); } .flexlayout__tabset-selected { @@ -83,478 +84,15 @@ div.inner__flexlayout__tabset > div.flexlayout__tabset_tabbar_outer_top { border-style: solid; border-color: #e2e2e2; } -/* .custom-widget .panel { - height:100%; - display:flex; - justify-content:center; - align-items:center; - background-color:white; - border:1px solid #555; - box-sizing: border-box; -} */ -/* -.flexlayout__layout { - height: 100%; -} -.flexlayout__splitter { - background-color: #f7f7f7; -} -@media (hover: hover) { - .flexlayout__splitter:hover { - background-color: #e2e2e2; - } -} -.flexlayout__splitter_border { - z-index: 10; -} -.flexlayout__splitter_drag { - z-index: 1000; - background-color: #e2e2e2; -} -.flexlayout__splitter_extra { - background-color: transparent; -} -.flexlayout__outline_rect { - position: absolute; - pointer-events: none; - box-sizing: border-box; - border: 2px solid red; - box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.2); - border-radius: 5px; - z-index: 1000; -} -.flexlayout__outline_rect_edge { - pointer-events: none; - border: 2px solid green; - box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.2); - border-radius: 5px; - z-index: 1000; - box-sizing: border-box; -} -.flexlayout__edge_rect { - position: absolute; - z-index: 1000; - box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); - background-color: gray; - pointer-events: none; -} -.flexlayout__drag_rect { - position: absolute; - cursor: move; - color: black; - background-color: #f7f7f7; - border: 2px solid #e2e2e2; - box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.3); - border-radius: 5px; - z-index: 1000; - box-sizing: border-box; - opacity: 0.9; - text-align: center; - display: flex; - justify-content: center; - flex-direction: column; - overflow: hidden; - padding: 10px; - word-wrap: break-word; - font-size: medium; - font-family: Roboto, Arial, sans-serif; -} -.flexlayout__tabset { - overflow: hidden; - background-color: #f7f7f7; - box-sizing: border-box; - font-size: medium; - font-family: Roboto, Arial, sans-serif; - background-color: white; -} -.flexlayout__tabset_header { - position: absolute; - display: flex; - align-items: center; - left: 0; - right: 0; - padding: 3px 3px 3px 5px; - box-sizing: border-box; - border-bottom: 1px solid #e9e9e9; - color: black; - background-color: white; -} -.flexlayout__tabset_header_content { - flex-grow: 1; -} -.flexlayout__tabset_tabbar_outer { - box-sizing: border-box; - background-color: #f7f7f7; - position: absolute; - left: 0; - right: 0; - overflow: hidden; - display: flex; - background-color: white; -} -.flexlayout__tabset_tabbar_outer_top { - border-bottom: 1px solid #e9e9e9; -} -.flexlayout__tabset_tabbar_outer_bottom { - border-top: 1px solid #e9e9e9; -} -.flexlayout__tabset_tabbar_inner { - position: relative; - box-sizing: border-box; - display: flex; - flex-grow: 1; - overflow: hidden; -} -.flexlayout__tabset_tabbar_inner_tab_container { - display: flex; - box-sizing: border-box; - position: absolute; - top: 0; - bottom: 0; - width: 10000px; -} -.flexlayout__tabset_tabbar_inner_tab_container_top { - border-top: 2px solid transparent; -} -.flexlayout__tabset_tabbar_inner_tab_container_bottom { - border-bottom: 2px solid transparent; -} -.flexlayout__tabset-selected { - background-color: #f7f7f7; -} -.flexlayout__tabset-maximized { - background-color: #d4d4d4; -} -.flexlayout__tab { - overflow: auto; - position: absolute; - box-sizing: border-box; - color: black; - background-color: white; -} -.flexlayout__tab_button { - display: inline-flex; - align-items: center; - box-sizing: border-box; - padding: 3px 8px; - margin: 0px 2px; - cursor: pointer; -} -.flexlayout__tab_button--selected { - background-color: #e9e9e9; - color: black; -} -@media (hover: hover) { - .flexlayout__tab_button:hover { - background-color: #e9e9e9; - color: black; - } -} -.flexlayout__tab_button--unselected { - color: gray; -} -.flexlayout__tab_button_leading { - display: inline-block; -} -.flexlayout__tab_button_content { - display: inline-block; -} -.flexlayout__tab_button_textbox { - border: none; - color: green; - background-color: #e9e9e9; -} -.flexlayout__tab_button_textbox:focus { - outline: none; -} -.flexlayout__tab_button_trailing { - display: inline-block; - margin-left: 8px; - min-width: 8px; - min-height: 8px; -} -@media (pointer: coarse) { - .flexlayout__tab_button_trailing { - min-width: 20px; - min-height: 20px; - } -} -@media (hover: hover) { - .flexlayout__tab_button:hover .flexlayout__tab_button_trailing { - background: transparent url("../images/close.png") no-repeat center; - } -} -.flexlayout__tab_button--selected .flexlayout__tab_button_trailing { - background: transparent url("../images/close.png") no-repeat center; -} -.flexlayout__tab_button_overflow { - margin-left: 10px; - padding-left: 12px; - border: none; - color: gray; - font-size: inherit; - background: transparent url("../images/more2.png") no-repeat left; -} -.flexlayout__tab_toolbar { - display: flex; - align-items: center; -} -.flexlayout__tab_toolbar_button { - min-width: 20px; - min-height: 20px; - border: none; - outline: none; -} -.flexlayout__tab_toolbar_button-min { - background: transparent url("../images/maximize.png") no-repeat center; -} -.flexlayout__tab_toolbar_button-max { - background: transparent url("../images/restore.png") no-repeat center; -} -.flexlayout__tab_toolbar_button-float { - background: transparent url("../images/popout.png") no-repeat center; -} -.flexlayout__tab_toolbar_button-close { - background: transparent url("../images/close.png") no-repeat center; -} -.flexlayout__tab_toolbar_sticky_buttons_container { - display: flex; - align-items: center; -} -.flexlayout__tab_floating { - overflow: auto; - position: absolute; - box-sizing: border-box; - color: black; - background-color: white; - display: flex; - justify-content: center; - align-items: center; -} -.flexlayout__tab_floating_inner { - overflow: auto; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} -.flexlayout__tab_floating_inner div { - margin-bottom: 5px; - text-align: center; -} -.flexlayout__tab_floating_inner div a { - color: royalblue; -} -.flexlayout__border { - box-sizing: border-box; - overflow: hidden; - display: flex; - font-size: medium; - font-family: Roboto, Arial, sans-serif; - background-color: white; -} -.flexlayout__border_top { - border-bottom: 1px solid #e9e9e9; - align-items: center; -} -.flexlayout__border_bottom { - border-top: 1px solid #e9e9e9; - align-items: center; -} -.flexlayout__border_left { - border-right: 1px solid #e9e9e9; - align-content: center; - flex-direction: column; -} -.flexlayout__border_right { - border-left: 1px solid #e9e9e9; - align-content: center; - flex-direction: column; -} -.flexlayout__border_inner { - position: relative; - box-sizing: border-box; - display: flex; - overflow: hidden; - flex-grow: 1; -} -.flexlayout__border_inner_tab_container { - white-space: nowrap; - display: flex; - box-sizing: border-box; - position: absolute; - top: 0; - bottom: 0; - width: 10000px; -} -.flexlayout__border_inner_tab_container_right { - transform-origin: top left; - transform: rotate(90deg); -} -.flexlayout__border_inner_tab_container_left { - flex-direction: row-reverse; - transform-origin: top right; - transform: rotate(-90deg); -} -.flexlayout__border_button { - display: flex; - align-items: center; - cursor: pointer; - padding: 3px 8px; - margin: 2px; - box-sizing: border-box; - white-space: nowrap; - background-color: #f0f0f0; -} -.flexlayout__border_button--selected { - background-color: #e9e9e9; - color: black; -} -@media (hover: hover) { - .flexlayout__border_button:hover { - background-color: #e9e9e9; - color: black; - } -} -.flexlayout__border_button--unselected { - color: gray; -} -.flexlayout__border_button_leading { - display: inline; -} -.flexlayout__border_button_content { - display: inline-block; -} -.flexlayout__border_button_trailing { - display: inline-block; - margin-left: 8px; - min-width: 8px; - min-height: 8px; -} -@media (pointer: coarse) { - .flexlayout__border_button_trailing { - min-width: 20px; - min-height: 20px; - } -} -@media (hover: hover) { - .flexlayout__border_button:hover .flexlayout__border_button_trailing { - background: transparent url("../images/close.png") no-repeat center; - } -} -.flexlayout__border_button--selected .flexlayout__border_button_trailing { - background: transparent url("../images/close.png") no-repeat center; -} -.flexlayout__border_toolbar { - display: flex; - align-items: center; -} -.flexlayout__border_toolbar_left { - flex-direction: column; -} -.flexlayout__border_toolbar_right { - flex-direction: column; -} -.flexlayout__border_toolbar_button { - min-width: 20px; - min-height: 20px; - border: none; - outline: none; -} -.flexlayout__border_toolbar_button-float { - background: transparent url("../images/popout.png") no-repeat center; -} -.flexlayout__border_toolbar_button_overflow { - border: none; - padding-left: 12px; - color: gray; - font-size: inherit; - background: transparent url("../images/more2.png") no-repeat left; -} -.flexlayout__border_toolbar_button_overflow_top, .flexlayout__border_toolbar_button_overflow_bottom { - margin-left: 10px; -} -.flexlayout__border_toolbar_button_overflow_right, .flexlayout__border_toolbar_button_overflow_left { - padding-right: 0px; - margin-top: 5px; -} -.flexlayout__popup_menu { - font-size: medium; - font-family: Roboto, Arial, sans-serif; -} -.flexlayout__popup_menu_item { - padding: 2px 10px 2px 10px; - white-space: nowrap; -} -@media (hover: hover) { - .flexlayout__popup_menu_item:hover { - background-color: #d4d4d4; - } -} -.flexlayout__popup_menu_container { - box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.15); - border: 1px solid #d4d4d4; - color: black; - background: white; - border-radius: 3px; - position: absolute; - z-index: 1000; - max-height: 50%; - min-width: 100px; - overflow: auto; -} -.flexlayout__floating_window _body { - height: 100%; -} -.flexlayout__floating_window_content { - left: 0; - top: 0; - right: 0; - bottom: 0; - position: absolute; -} -.flexlayout__floating_window_tab { - overflow: auto; - left: 0; - top: 0; - right: 0; - bottom: 0; - position: absolute; - box-sizing: border-box; - background-color: white; - color: black; -} -.flexlayout__error_boundary_container { - left: 0; - top: 0; - right: 0; - bottom: 0; - position: absolute; - display: flex; - justify-content: center; -} -.flexlayout__error_boundary_content { - display: flex; - align-items: center; -} -.flexlayout__tabset_sizer { - padding-top: 5px; - padding-bottom: 3px; - font-size: medium; - font-family: Roboto, Arial, sans-serif; -} -.flexlayout__tabset_header_sizer { - padding-top: 3px; - padding-bottom: 3px; - font-size: medium; - font-family: Roboto, Arial, sans-serif; -} -.flexlayout__border_sizer { - padding-top: 6px; - padding-bottom: 5px; - font-size: medium; - font-family: Roboto, Arial, sans-serif; -} */ - -/*# sourceMappingURL=light.css.map */ +hr.ipyflex-divider { + margin: 1px 4px 1px 4px; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + border-width: 0; + border-style: solid; + border-color: rgb(0 0 0 / 45%); + border-bottom-width: thin; + list-style: none; +} \ No newline at end of file diff --git a/ipyflex/flex_layout.py b/ipyflex/flex_layout.py index 3c7687e..0b18e7c 100644 --- a/ipyflex/flex_layout.py +++ b/ipyflex/flex_layout.py @@ -11,12 +11,12 @@ import json import os from enum import Enum -from typing import Dict as TypeDict +from typing import Callable, Dict as TypeDict from typing import List as TypeList from typing import Union from ipywidgets import DOMWidget, Widget, widget_serialization -from traitlets.traitlets import Bool, Dict, Instance, Unicode +from traitlets.traitlets import Bool, Dict, Instance, Unicode, List from ._frontend import module_name, module_version from .utils import get_nonexistant_path @@ -26,6 +26,8 @@ class MESSAGE_ACTION(str, Enum): SAVE_TEMPLATE = 'save_template' UPDATE_CHILDREN = 'update_children' + REQUEST_FACTORY = 'request_factory' + RENDER_FACTORY = 'render_factory' class FlexLayout(DOMWidget): @@ -45,6 +47,8 @@ class FlexLayout(DOMWidget): help='Dict of widget children', ).tag(sync=True, **widget_serialization) + widget_factories = List(trait=Unicode, default_value=[]).tag(sync=True) + layout_config = Dict( {'borderLeft': False, 'borderRight': False}, help='Layout configuration', @@ -70,7 +74,7 @@ class FlexLayout(DOMWidget): def __init__( self, widgets: Union[TypeDict, TypeList] = [], - # layout_config: TypeDict, + factories: TypeDict[str, Callable] = {}, **kwargs, ): super().__init__(**kwargs) @@ -83,9 +87,23 @@ def __init__( } else: raise TypeError('Invalid input!') - if len(list(self.RESERVED_NAME & set(self.children))) > 0: - raise KeyError('Please do not use widget name in reserved list!') - + children_set = set(self.children) + factories_set = set(factories) + if ( + len(self.RESERVED_NAME & children_set) > 0 + or len(self.RESERVED_NAME & factories_set) > 0 + ): + raise KeyError( + f'Please do not use widget name in reserved list: {self.RESERVED_NAME}' + ) + + if len(children_set & factories_set) > 0: + raise ValueError( + 'Please do not use a same name for both widget and factory' + ) + + self.widget_factories = list(factories_set) + self._factories = factories self.template_json = None if self.template is not None: try: @@ -105,6 +123,11 @@ def add(self, name: str, widget: Widget) -> None: if name in self.RESERVED_NAME: raise KeyError('Please do not use widget name in reserved list!') + if name in self.widget_factories: + raise KeyError( + 'A factory with the same name is already registered!' + ) + old = copy.copy(self.children) old[name] = widget self.children = old @@ -134,3 +157,15 @@ def _handle_frontend_msg( with open(file_path, 'w') as f: json.dump(json_data, f) self.template = file_path + elif action == MESSAGE_ACTION.REQUEST_FACTORY: + factory_name = payload['factory_name'] + uuid = payload['uuid'] + if factory_name in self.widget_factories: + w_model = self._factories[factory_name]() + model_msg = widget_serialization['to_json'](w_model, None) + self.send( + { + 'action': MESSAGE_ACTION.RENDER_FACTORY, + 'payload': {'model_id': model_msg, 'uuid': uuid}, + } + ) diff --git a/src/menuWidget.tsx b/src/menuWidget.tsx index 53c48a9..ceb7d36 100644 --- a/src/menuWidget.tsx +++ b/src/menuWidget.tsx @@ -4,11 +4,12 @@ import MenuItem from '@mui/material/MenuItem'; import dialogBody from './dialogWidget'; import { showDialog } from '@jupyterlab/apputils'; -import { JUPYTER_BUTTON_CLASS } from './utils'; +import { JUPYTER_BUTTON_CLASS, MESSAGE_ACTION } from './utils'; interface IProps { nodeId: string; tabsetId: string; widgetList: Array; + factoryList: Array; addTabToTabset: (name: string, nodeId: string, tabsetId: string) => void; model: any; } @@ -31,7 +32,7 @@ export class WidgetMenu extends Component { on_msg = (data: { action: string; payload: any }, buffer: any[]): void => { const { action, payload } = data; switch (action) { - case 'update_children': + case MESSAGE_ACTION.UPDATE_CHILDREN: { const wName: string = payload.name; if (!this.state.widgetList.includes(wName)) { @@ -57,10 +58,10 @@ export class WidgetMenu extends Component { render(): JSX.Element { const menuId = `add_widget_menu_${this.props.tabsetId}@${this.props.nodeId}`; - const menuItem = []; + const widgetItems = []; for (const name of this.state.widgetList) { if (name !== CREATE_NEW) { - menuItem.push( + widgetItems.push( { @@ -77,6 +78,25 @@ export class WidgetMenu extends Component { ); } } + const factoryItems = []; + for (const name of this.props.factoryList) { + factoryItems.push( + { + this.props.addTabToTabset( + name, + this.props.nodeId, + this.props.tabsetId + ); + this.handleClose(); + }} + > + {name} + + ); + } + const createNew: JSX.Element = ( { ); if (result.button.label === 'Save' && result.value) { widgetName = result.value; + if (this.props.widgetList.includes(widgetName)) { + alert('A widget with the same name is already registered!'); + return; + } + if (this.props.factoryList.includes(widgetName)) { + alert('A factory with the same name is already registered!'); + return; + } this.setState((old) => ({ ...old, widgetList: [...old.widgetList, widgetName], @@ -106,7 +134,7 @@ export class WidgetMenu extends Component { {CREATE_NEW} ); - menuItem.push(createNew); + // menuItem.push(createNew); return (