Skip to content

Commit

Permalink
feat: compose MouseInteraction with other interacts
Browse files Browse the repository at this point in the history
For instance with PanZoom, for this specific case to work
we should not listen the the drag events, for this we also need
to filter on those.
  • Loading branch information
maartenbreddels committed Mar 2, 2021
1 parent 6af6d5b commit 56d9100
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 28 deletions.
9 changes: 8 additions & 1 deletion bqplot_image_gl/interacts.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from bqplot.interacts import BrushSelector, Interaction
from bqplot.scales import Scale
from traitlets import Float, Unicode, Dict, Instance, Int
from traitlets import Float, Unicode, Dict, Instance, Int, List
from ipywidgets.widgets.widget import widget_serialization
from bqplot_image_gl._version import __version__


drag_events = ["dragstart", "dragmove", "dragend"]
mouse_events = ['click', 'dblclick', 'mouseenter', 'mouseleave', 'contextmenu', 'mousemove']
keyboard_events = ['keydown', 'keyup']

class BrushEllipseSelector(BrushSelector):

"""BrushEllipse interval selector interaction.
Expand Down Expand Up @@ -93,3 +97,6 @@ class MouseInteraction(Interaction):
.tag(sync=True, dimension='y', **widget_serialization)
cursor = Unicode('auto').tag(sync=True)
move_throttle = Int(50).tag(sync=True)
next = Instance(Interaction).tag(sync=True, **widget_serialization)
events = List(Unicode, default_value=drag_events + mouse_events + keyboard_events,
allow_none=True).tag(sync=True)
22 changes: 16 additions & 6 deletions examples/mouse.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,16 @@
{
"cell_type": "code",
"execution_count": null,
"id": "sufficient-shirt",
"metadata": {},
"outputs": [],
"source": [
"from bqplot_image_gl.interacts import MouseInteraction"
"from bqplot_image_gl.interacts import MouseInteraction, keyboard_events, mouse_events\n",
"from bqplot import PanZoom"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "empirical-model",
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -100,11 +99,13 @@
{
"cell_type": "code",
"execution_count": null,
"id": "known-possession",
"metadata": {},
"outputs": [],
"source": [
"interaction = MouseInteraction(x_scale=scales_image['x'], y_scale=scales_image['y'], move_throttle=70)\n",
"# if we want to work together with PanZoom, we don't want to processess drag events\n",
"panzoom = PanZoom(scales={'x': [scales_image['x']], 'y': [scales_image['y']]})\n",
"interaction = MouseInteraction(x_scale=scales_image['x'], y_scale=scales_image['y'], move_throttle=70, next=panzoom,\n",
" events=keyboard_events + mouse_events)\n",
"figure.interaction = interaction\n",
"def on_mouse_msg(interaction, data, buffers):\n",
" # it might be a good idea to throttle on the Python side as well, for instance when many computations\n",
Expand Down Expand Up @@ -136,7 +137,16 @@
{
"cell_type": "code",
"execution_count": null,
"id": "federal-domestic",
"metadata": {},
"outputs": [],
"source": [
"# cherry pick particular events:\n",
"# interaction.events = ['click']"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
Expand Down
84 changes: 63 additions & 21 deletions js/lib/MouseInteraction.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,28 @@ class MouseInteractionModel extends base_1.WidgetModel {
scale_y: null,
scale_y: null,
move_throttle: 50,
cursor: 'auto' });
cursor: 'auto',
next: null,
events: [],
});
}
}

MouseInteractionModel.serializers = Object.assign({}, base_1.WidgetModel.serializers, { x_scale: { deserialize: base_1.unpack_models }, y_scale: { deserialize: base_1.unpack_models } });
MouseInteractionModel.serializers = Object.assign({}, base_1.WidgetModel.serializers, { x_scale: { deserialize: base_1.unpack_models }, y_scale: { deserialize: base_1.unpack_models }, next: { deserialize: base_1.unpack_models } });
exports.MouseInteractionModel = MouseInteractionModel;
class MouseInteraction extends Interaction_1.Interaction {
async render() {
// events for dragging etc
const eventElement = d3.select(this.d3el.node());
super.render();
this.el.setAttribute('display', 'none');
this.eventElement = d3.select(this.parent.interaction.node());
this.nextView = null;
this.x_scale = await this.create_child_view(this.model.get("x_scale"));
this.y_scale = await this.create_child_view(this.model.get("y_scale"));
this.last_mouse_point = [-1, -1];
this.parent.on("margin_updated", this.updateScaleRanges, this);
this.updateScaleRanges();
const updateCursor = () => {
eventElement.node().style.cursor = this.model.get('cursor');
this.eventElement.node().style.cursor = this.model.get('cursor');
};
this.listenTo(this.model, "change:cursor", updateCursor);
updateCursor();
Expand All @@ -51,19 +55,29 @@ class MouseInteraction extends Interaction_1.Interaction {
updateThrottle();
this.listenTo(this.model, 'change:move_throttle', updateThrottle);

eventElement.call(d3_drag_1.drag().on("start", () => {
const e = d3GetEvent();
this._emit('dragstart', { x: e.x, y: e.y });
}).on("drag", () => {
const e = d3GetEvent();
this._emit('dragmove', { x: e.x, y: e.y });
}).on("end", () => {
const e = d3GetEvent();
this._emit('dragend', { x: e.x, y: e.y });
}));
this.bindEvents();
await this.updateNextInteract();
}

bindEvents() {
const events = this.model.get("events");
// we don't want to bind these events if we don't need them, because drag events
// can call stop propagation
if (this.eventEnabled("dragstart") && this.eventEnabled("dragmove") && this.eventEnabled("dragend")) {
this.eventElement.call(d3_drag_1.drag().on("start", () => {
const e = d3GetEvent();
this._emit('dragstart', { x: e.x, y: e.y });
}).on("drag", () => {
const e = d3GetEvent();
this._emit('dragmove', { x: e.x, y: e.y });
}).on("end", () => {
const e = d3GetEvent();
this._emit('dragend', { x: e.x, y: e.y });
}));
}
// and click events
['click', 'dblclick', 'mouseenter', 'mouseleave', 'contextmenu'].forEach(eventName => {
eventElement.on(eventName, () => {
this.eventElement.on(eventName, () => {
this._emitThrottled.flush(); // we don't want mousemove events to come after enter/leave
if (eventName !== 'mouseleave') {
// to allow the div to get focus, but we will not allow it to be reachable by tab key
Expand All @@ -77,9 +91,12 @@ class MouseInteraction extends Interaction_1.Interaction {
}
const e = d3GetEvent();
// to be consistent with drag events, we need to user clientPoint
const [x, y] = d3_selection_1.clientPoint(eventElement.node(), e);
e.preventDefault();
e.stopPropagation();
const [x, y] = d3_selection_1.clientPoint(this.eventElement.node(), e);
const events = this.model.get("events");
if (eventName == 'contextmenu' && this.eventEnabled('contextmenu')) {
e.preventDefault();
e.stopPropagation();
}
this._emit(eventName, { x, y }, {button: e.button, altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey});
return false
});
Expand All @@ -99,15 +116,37 @@ class MouseInteraction extends Interaction_1.Interaction {
});
// throttled events
['mousemove'].forEach(eventName => {
eventElement.on(eventName, () => {
this.eventElement.on(eventName, () => {
const e = d3GetEvent();
// to be consistent with drag events, we need to user clientPoint
const [x, y] = d3_selection_1.clientPoint(eventElement.node(), e);
const [x, y] = d3_selection_1.clientPoint(this.eventElement.node(), e);
this.last_mouse_point = [x, y];
this._emitThrottled(eventName, { x, y });
});
});
}

eventEnabled(eventName) {
const events = this.model.get("events");
return (events == null) || events.includes(eventName);
}

async updateNextInteract() {
// this mimics Figure.set_iteraction
const next = this.model.get('next')
if(this.nextView) {
this.nextView.remove();
}
if(!next) {
return;
}
this.nextView = await this.parent.create_child_view(next);
this.parent.interaction.node().appendChild(this.nextView.el);
this.parent.displayed.then(() => {
this.nextView.trigger("displayed");
});
}

updateScaleRanges() {
this.x_scale.set_range(this.parent.padded_range("x", this.x_scale.model));
this.y_scale.set_range(this.parent.padded_range("y", this.y_scale.model));
Expand All @@ -119,6 +158,9 @@ class MouseInteraction extends Interaction_1.Interaction {
this._emitThrottled.flush();
}
_emit(name, { x, y }, extra) {
if(!this.eventEnabled(name)) {
return;
}
let domain = { x: this.x_scale.scale.invert(x), y: this.y_scale.scale.invert(y) };
this.send({ event: name, pixel: { x, y }, domain: domain, ...extra });
}
Expand Down

0 comments on commit 56d9100

Please sign in to comment.