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

add drag and drop based on @btel's work #2720

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
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
726 changes: 726 additions & 0 deletions docs/source/examples/Drag and Drop.ipynb

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions ipywidgets/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@
from .widget_style import Style
from .widget_templates import TwoByTwoLayout, AppLayout, GridspecLayout
from .widget_upload import FileUpload
from .widget_dragdrop import DraggableBox, DropBox
4 changes: 2 additions & 2 deletions ipywidgets/widgets/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from .._version import __protocol_version__, __jupyter_widgets_base_version__
PROTOCOL_VERSION_MAJOR = __protocol_version__.split('.')[0]

def _widget_to_json(x, obj):
def _widget_to_json(x, obj=None):
if isinstance(x, dict):
return {k: _widget_to_json(v, obj) for k, v in x.items()}
elif isinstance(x, (list, tuple)):
Expand All @@ -30,7 +30,7 @@ def _widget_to_json(x, obj):
else:
return x

def _json_to_widget(x, obj):
def _json_to_widget(x, obj=None):
if isinstance(x, dict):
return {k: _json_to_widget(v, obj) for k, v in x.items()}
elif isinstance(x, (list, tuple)):
Expand Down
118 changes: 118 additions & 0 deletions ipywidgets/widgets/widget_dragdrop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

"""Contains the DropWidget class"""
from .widget import Widget, CallbackDispatcher, register, widget_serialization
from .domwidget import DOMWidget
from .widget_core import CoreWidget
import json
from traitlets import Bool, Dict, Unicode, Instance


class DropWidget(DOMWidget, CoreWidget):
"""Base widget for the single-child DropBox and DraggableBox widgets"""

draggable = Bool(default=False).tag(sync=True)
drag_data = Dict().tag(sync=True)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._drop_handlers = CallbackDispatcher()
self.on_msg(self._handle_dragdrop_msg)

def on_drop(self, callback, remove=False):
""" Register a callback to execute when an element is dropped.

The callback will be called with two arguments, the drop box
widget instance receiving the drop event, and the dropped element data.

Parameters
----------
remove: bool (optional)
Set to true to remove the callback from the list of callbacks.
"""
self._drop_handlers.register_callback(callback, remove=remove)

def drop(self, data):
""" Programmatically trigger a drop event.
This will call the callbacks registered to the drop event.
"""

if data.get('application/vnd.jupyter.widget-view+json'):
wolfv marked this conversation as resolved.
Show resolved Hide resolved
widget_mime = json.loads(data['application/vnd.jupyter.widget-view+json'])
data['widget'] = widget_serialization['from_json']('IPY_MODEL_' + widget_mime['model_id'])
Copy link
Member

@vidartf vidartf Apr 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking about this. Why is this method exposed as an API method? If the user is meant to use it from the kernel side, should we not handle the deserialization of the widget reference before calling this? This way, any kernel use could simply pass a widget reference as widget, and not have to worry about the model_id.


self._drop_handlers(self, data)

def _handle_dragdrop_msg(self, _, content, buffers):
""" Handle a msg from the front-end.

Parameters
----------
content: dict
Content of the msg.
"""
if content.get('event', '') == 'drop':
self.drop(content.get('data', {}))

@register
class DropBox(DropWidget):
""" A box that receives a drop event

The DropBox can have one child, and you can attach an `on_drop` handler to it.

Parameters
----------
child: Widget instance
The child widget instance that is displayed inside the DropBox

Examples
--------
>>> import ipywidgets as widgets
>>> dropbox_widget = widgets.DropBox(Label("Drop something on top of me"))
>>> dropbox_widget.on_drop(lambda box, data: print(data))
"""

_model_name = Unicode('DropBoxModel').tag(sync=True)
_view_name = Unicode('DropBoxView').tag(sync=True)
child = Instance(Widget, allow_none=True).tag(sync=True, **widget_serialization)

def __init__(self, child=None, **kwargs):
super(DropBox, self).__init__(**kwargs, child=child)

@register
class DraggableBox(DropWidget):
""" A draggable box

A box widget that can be dragged e.g. on top of a DropBox. The draggable box can
contain a single child, and optionally drag_data which will be received on the widget
it's dropped on.
Draggability can be modified by flipping the boolean ``draggable`` attribute.

Parameters
----------
child: Widget instance
The child widget instance that is displayed inside the DropBox

draggable: Boolean (default True)
Trait that flips whether the draggable box is draggable or not

drag_data: Dictionary
You can attach custom drag data here, which will be received as an argument on the receiver
side (in the ``on_drop`` event).

Examples
--------
>>> import ipywidgets as widgets
>>> draggable_widget = widgets.DraggableBox(Label("You can drag this button"))
>>> draggable_widget.drag_data = {"somerandomkey": "I have this data for you ..."}
"""

_model_name = Unicode('DraggableBoxModel').tag(sync=True)
_view_name = Unicode('DraggableBoxView').tag(sync=True)
child = Instance(Widget, allow_none=True).tag(sync=True, **widget_serialization)
draggable = Bool(True).tag(sync=True)
drag_data = Dict().tag(sync=True)

def __init__(self, child=None, **kwargs):
super(DraggableBox, self).__init__(**kwargs, child=child)
1 change: 1 addition & 0 deletions packages/controls/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ export * from './widget_selectioncontainer';
export * from './widget_string';
export * from './widget_description';
export * from './widget_upload';
export * from './widget_dragdrop';

export const version = (require('../package.json') as any).version;
172 changes: 172 additions & 0 deletions packages/controls/src/widget_dragdrop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import {
CoreDOMWidgetModel
} from './widget_core';

import {
DOMWidgetView, unpack_models, WidgetModel, WidgetView, JupyterLuminoPanelWidget, reject
} from '@jupyter-widgets/base';

import $ from 'jquery';

export
class DraggableBoxModel extends CoreDOMWidgetModel {
defaults(): Backbone.ObjectHash {
return {
...super.defaults(),
_view_name: 'DraggableBoxView',
_model_name: 'DraggableBoxModel',
child: null,
draggable: true,
drag_data: {}
};
}

static serializers = {
...CoreDOMWidgetModel.serializers,
child: {deserialize: unpack_models}
};
}

export
class DropBoxModel extends CoreDOMWidgetModel {
defaults(): Backbone.ObjectHash {
return {
...super.defaults(),
_view_name: 'DropBoxView',
_model_name: 'DropBoxModel',
child: null
};
}

static serializers = {
...CoreDOMWidgetModel.serializers,
child: {deserialize: unpack_models}
};
}

class DragDropBoxViewBase extends DOMWidgetView {
child_view: DOMWidgetView | null;
pWidget: JupyterLuminoPanelWidget;

_createElement(tagName: string): HTMLElement {
this.pWidget = new JupyterLuminoPanelWidget({ view: this });
return this.pWidget.node;
}

_setElement(el: HTMLElement): void {
if (this.el || el !== this.pWidget.node) {
// Boxes don't allow setting the element beyond the initial creation.
throw new Error('Cannot reset the DOM element.');
}
this.el = this.pWidget.node;
this.$el = $(this.pWidget.node);
}

initialize(parameters: WidgetView.IInitializeParameters): void {
super.initialize(parameters);
this.add_child_model(this.model.get('child'));
this.listenTo(this.model, 'change:child', this.update_child);

this.pWidget.addClass('jupyter-widgets');
this.pWidget.addClass('widget-container');
this.pWidget.addClass('widget-draggable-box');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this class needed? It doesn't seem to be used.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I just tried to follow conventions:

this.pWidget.addClass('jupyter-widgets');
this.pWidget.addClass('widget-container');
this.pWidget.addClass('widget-box');

someone who wants to style the drag drop widgets might want to add CSS to this?
On the other hand, the two leaf classes should probably add another class as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

someone who wants to style the drag drop widgets might want to add CSS to this?

That could indeed be useful.

For the case of the widget_box, there is actually some default css:

.widget-box {
box-sizing: border-box;
display: flex;
margin: 0;
overflow: auto;
}

}

add_child_model(model: WidgetModel): Promise<DOMWidgetView> {
return this.create_child_view(model).then((view: DOMWidgetView) => {
if (this.child_view && this.child_view !== null) {
this.child_view.remove();
}
this.pWidget.addWidget(view.pWidget);
this.child_view = view;
return view;
}).catch(reject('Could not add child view to box', true));
}

update_child(): void {
this.add_child_model(this.model.get('child'));
}

remove(): void {
this.child_view = null;
super.remove();
}
}

const JUPYTER_VIEW_MIME = 'application/vnd.jupyter.widget-view+json';

export
class DraggableBoxView extends DragDropBoxViewBase {
initialize(parameters: WidgetView.IInitializeParameters): void {
super.initialize(parameters);
this.dragSetup();
}

events(): {[e: string]: string} {
return {'dragstart' : 'on_dragstart'};
}

on_dragstart(event: DragEvent): void {
if (event.dataTransfer) {
if (this.model.get('child').get('value')) {
event.dataTransfer?.setData('text/plain', this.model.get('child').get('value'));
}
const drag_data = this.model.get('drag_data');
for (const datatype in drag_data) {
event.dataTransfer.setData(datatype, drag_data[datatype]);
}
event.dataTransfer.setData(JUPYTER_VIEW_MIME, JSON.stringify({
"model_id": this.model.model_id,
"version_major": 2,
"version_minor": 0
}));
event.dataTransfer.dropEffect = 'copy';
}
}

dragSetup(): void {
this.el.draggable = this.model.get('draggable');
this.model.on('change:draggable', this.on_change_draggable, this);
}

on_change_draggable(): void {
this.el.draggable = this.model.get('draggable');
}
}

export
class DropBoxView extends DragDropBoxViewBase {
events(): {[e: string]: string} {
return {
'drop': '_handle_drop',
'dragover': 'on_dragover'
};
}

_handle_drop(event: DragEvent): void {
event.preventDefault();

const datamap: {[e: string]: string} = {};

if (event.dataTransfer)
{
for (let i=0; i < event.dataTransfer.types.length; i++) {
const t = event.dataTransfer.types[i];
datamap[t] = event.dataTransfer?.getData(t);
}
}

this.send({event: 'drop', data: datamap});
}

on_dragover(event: DragEvent): void {
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy';
}
}
}
Loading