Skip to content

Commit 8fea1ac

Browse files
authored
Merge pull request #180 from martinRenou/resize_handle
Resize handle
2 parents 2442b4b + 594a2e8 commit 8fea1ac

File tree

3 files changed

+158
-47
lines changed

3 files changed

+158
-47
lines changed

ipympl/backend_nbagg.py

+5
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ class Canvas(DOMWidget, FigureCanvasWebAggCore):
162162

163163
_image_mode = Unicode('full').tag(sync=True)
164164

165+
_rubberband_x = CInt(0).tag(sync=True)
166+
_rubberband_y = CInt(0).tag(sync=True)
167+
_rubberband_width = CInt(0).tag(sync=True)
168+
_rubberband_height = CInt(0).tag(sync=True)
169+
165170
_closed = Bool(True)
166171

167172
# Must declare the superclass private members.

js/src/mpl_widget.js

+144-39
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ var MPLCanvasModel = widgets.DOMWidgetModel.extend({
2727
_message: '',
2828
_cursor: 'pointer',
2929
_image_mode: 'full',
30+
_rubberband_x: 0,
31+
_rubberband_y: 0,
32+
_rubberband_width: 0,
33+
_rubberband_height: 0,
3034
});
3135
},
3236

@@ -41,6 +45,8 @@ var MPLCanvasModel = widgets.DOMWidgetModel.extend({
4145
this.offscreen_context.msBackingStorePixelRatio ||
4246
this.offscreen_context.oBackingStorePixelRatio || 1;
4347

48+
this.requested_size = null;
49+
this.resize_requested = false;
4450
this.ratio = (window.devicePixelRatio || 1) / backingStore;
4551
this._init_image();
4652

@@ -84,18 +90,37 @@ var MPLCanvasModel = widgets.DOMWidgetModel.extend({
8490

8591
handle_resize: function(msg) {
8692
var size = msg['size'];
93+
8794
this.resize_canvas(size[0], size[1]);
95+
this.offscreen_context.drawImage(this.image, 0, 0);
96+
8897
this._for_each_view(function(view) {
8998
view.resize_canvas(size[0], size[1]);
9099
});
100+
91101
this.send_message('refresh');
102+
103+
this.resize_requested = false;
104+
if (this.requested_size !== null) {
105+
// Requesting saved resize
106+
this.resize(this.requested_size[0], this.requested_size[1]);
107+
this.requested_size = null;
108+
}
109+
},
110+
111+
resize: function(width, height) {
112+
if (this.resize_requested) {
113+
// If a resize was already requested, save the requested size for later
114+
this.requested_size = [width, height];
115+
} else {
116+
this.resize_requested = true;
117+
this.send_message('resize', {'width': width, 'height': height});
118+
}
92119
},
93120

94121
resize_canvas: function(width, height) {
95122
this.offscreen_canvas.setAttribute('width', width * this.ratio);
96123
this.offscreen_canvas.setAttribute('height', height * this.ratio);
97-
this.offscreen_canvas.style.width = width + 'px';
98-
this.offscreen_canvas.style.height = height + 'px';
99124
},
100125

101126
handle_rubberband: function(msg) {
@@ -107,16 +132,15 @@ var MPLCanvasModel = widgets.DOMWidgetModel.extend({
107132
y0 = Math.floor(y0) + 0.5;
108133
x1 = Math.floor(x1) + 0.5;
109134
y1 = Math.floor(y1) + 0.5;
110-
var min_x = Math.min(x0, x1);
111-
var min_y = Math.min(y0, y1);
112-
var width = Math.abs(x1 - x0);
113-
var height = Math.abs(y1 - y0);
114135

115-
this._for_each_view(function(view) {
116-
view.rubberband_context.clearRect(
117-
0, 0, view.rubberband_canvas.width, view.rubberband_canvas.height);
136+
this.set('_rubberband_x', Math.min(x0, x1));
137+
this.set('_rubberband_y', Math.min(y0, y1));
138+
this.set('_rubberband_width', Math.abs(x1 - x0));
139+
this.set('_rubberband_height', Math.abs(y1 - y0));
140+
this.save_changes();
118141

119-
view.rubberband_context.strokeRect(min_x, min_y, width, height);
142+
this._for_each_view(function(view) {
143+
view.update_canvas();
120144
});
121145
},
122146

@@ -177,7 +201,7 @@ var MPLCanvasModel = widgets.DOMWidgetModel.extend({
177201
that.offscreen_context.drawImage(that.image, 0, 0);
178202

179203
that._for_each_view(function(view) {
180-
view.context.drawImage(that.offscreen_canvas, 0, 0);
204+
view.update_canvas();
181205
});
182206
};
183207
},
@@ -189,6 +213,10 @@ var MPLCanvasModel = widgets.DOMWidgetModel.extend({
189213
});
190214
}
191215
},
216+
217+
remove: function() {
218+
this.send_message('closing');
219+
}
192220
}, {
193221
serializers: _.extend({
194222
toolbar: { deserialize: widgets.unpack_models }
@@ -199,17 +227,23 @@ var MPLCanvasView = widgets.DOMWidgetView.extend({
199227
render: function() {
200228
this.canvas = undefined;
201229
this.context = undefined;
202-
this.rubberband_canvas = undefined;
203-
this.rubberband_context = undefined;
230+
this.top_canvas = undefined;
231+
this.top_context = undefined;
232+
this.resizing = false;
233+
this.resize_handle_size = 20;
204234

205235
this.figure = document.createElement('div');
206-
this.figure.addEventListener('remove', this.close.bind(this));
207236
this.figure.classList = 'jupyter-matplotlib-figure jupyter-widgets widget-container widget-box widget-vbox';
208237

209238
this._init_header();
210239
this._init_canvas();
211240
this._init_footer();
212241

242+
this._resize_event = this.resize_event.bind(this);
243+
this._stop_resize_event = this.stop_resize_event.bind(this);
244+
window.addEventListener('mousemove', this._resize_event);
245+
window.addEventListener('mouseup', this._stop_resize_event);
246+
213247
this.waiting = false;
214248

215249
var that = this;
@@ -320,41 +354,82 @@ var MPLCanvasView = widgets.DOMWidgetView.extend({
320354

321355
this.context = canvas.getContext('2d');
322356

323-
var rubberband_canvas = this.rubberband_canvas = document.createElement('canvas');
324-
rubberband_canvas.style.display = 'block';
325-
rubberband_canvas.style.position = 'absolute';
326-
rubberband_canvas.style.left = 0;
327-
rubberband_canvas.style.top = 0;
328-
rubberband_canvas.style.zIndex = 1;
357+
var top_canvas = this.top_canvas = document.createElement('canvas');
358+
top_canvas.style.display = 'block';
359+
top_canvas.style.position = 'absolute';
360+
top_canvas.style.left = 0;
361+
top_canvas.style.top = 0;
362+
top_canvas.style.zIndex = 1;
329363

330-
rubberband_canvas.addEventListener('mousedown', this.mouse_event('button_press'));
331-
rubberband_canvas.addEventListener('mouseup', this.mouse_event('button_release'));
332-
rubberband_canvas.addEventListener('mousemove', this.mouse_event('motion_notify'));
364+
top_canvas.addEventListener('mousedown', this.mouse_event('button_press'));
365+
top_canvas.addEventListener('mouseup', this.mouse_event('button_release'));
366+
top_canvas.addEventListener('mousemove', this.mouse_event('motion_notify'));
333367

334-
rubberband_canvas.addEventListener('mouseenter', this.mouse_event('figure_enter'));
335-
rubberband_canvas.addEventListener('mouseleave', this.mouse_event('figure_leave'));
368+
top_canvas.addEventListener('mouseenter', this.mouse_event('figure_enter'));
369+
top_canvas.addEventListener('mouseleave', this.mouse_event('figure_leave'));
336370

337-
rubberband_canvas.addEventListener('wheel', this.mouse_event('scroll'));
371+
top_canvas.addEventListener('wheel', this.mouse_event('scroll'));
338372

339373
canvas_div.appendChild(canvas);
340-
canvas_div.appendChild(rubberband_canvas);
374+
canvas_div.appendChild(top_canvas);
341375

342-
this.rubberband_context = rubberband_canvas.getContext('2d');
343-
this.rubberband_context.strokeStyle = '#000000';
376+
this.top_context = top_canvas.getContext('2d');
377+
this.top_context.strokeStyle = 'rgba(0, 0, 0, 255)';
344378

345379
// Disable right mouse context menu.
346-
this.rubberband_canvas.addEventListener('contextmenu', function(e) {
380+
this.top_canvas.addEventListener('contextmenu', function(e) {
347381
event.preventDefault();
348382
event.stopPropagation();
349383
return false;
350384
});
351385

352386
this.resize_canvas(this.model.get('_width'), this.model.get('_height'));
387+
this.update_canvas();
388+
},
389+
390+
update_canvas: function() {
391+
if (this.canvas.width == 0 || this.canvas.height == 0) {
392+
return;
393+
}
394+
395+
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
353396
this.context.drawImage(this.model.offscreen_canvas, 0, 0);
397+
398+
this.top_context.clearRect(0, 0, this.top_canvas.width, this.top_canvas.height);
399+
400+
// Draw rubberband
401+
if (this.model.get('_rubberband_width') != 0 && this.model.get('_rubberband_height') != 0) {
402+
this.top_context.strokeRect(
403+
this.model.get('_rubberband_x'), this.model.get('_rubberband_y'),
404+
this.model.get('_rubberband_width'), this.model.get('_rubberband_height')
405+
);
406+
}
407+
408+
// Draw resize handle
409+
this.top_context.save();
410+
411+
var gradient = this.top_context.createLinearGradient(
412+
this.top_canvas.width - this.resize_handle_size / 3, this.top_canvas.height - this.resize_handle_size / 3,
413+
this.top_canvas.width - this.resize_handle_size / 4, this.top_canvas.height - this.resize_handle_size / 4
414+
);
415+
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
416+
gradient.addColorStop(1, 'rgba(0, 0, 0, 255)');
417+
418+
this.top_context.fillStyle = gradient;
419+
420+
this.top_context.globalAlpha = 0.3;
421+
this.top_context.beginPath();
422+
this.top_context.moveTo(this.top_canvas.width, this.top_canvas.height);
423+
this.top_context.lineTo(this.top_canvas.width, this.top_canvas.height - this.resize_handle_size);
424+
this.top_context.lineTo(this.top_canvas.width - this.resize_handle_size, this.top_canvas.height);
425+
this.top_context.closePath();
426+
this.top_context.fill();
427+
428+
this.top_context.restore();
354429
},
355430

356431
_update_cursor: function() {
357-
this.rubberband_canvas.style.cursor = this.model.get('_cursor');
432+
this.top_canvas.style.cursor = this.model.get('_cursor');
358433
},
359434

360435
_init_footer: function() {
@@ -376,11 +451,13 @@ var MPLCanvasView = widgets.DOMWidgetView.extend({
376451
this.canvas.style.width = width + 'px';
377452
this.canvas.style.height = height + 'px';
378453

379-
this.rubberband_canvas.setAttribute('width', width);
380-
this.rubberband_canvas.setAttribute('height', height);
454+
this.top_canvas.setAttribute('width', width);
455+
this.top_canvas.setAttribute('height', height);
381456

382457
this.canvas_div.style.width = width + 'px';
383458
this.canvas_div.style.height = height + 'px';
459+
460+
this.update_canvas();
384461
},
385462

386463
mouse_event: function(name) {
@@ -399,8 +476,25 @@ var MPLCanvasView = widgets.DOMWidgetView.extend({
399476
}
400477

401478
if (name === 'button_press') {
402-
that.canvas.focus();
403-
that.canvas_div.focus();
479+
// If clicking on the resize handle
480+
if (canvas_pos.x >= that.top_canvas.width - that.resize_handle_size &&
481+
canvas_pos.y >= that.top_canvas.height - that.resize_handle_size) {
482+
that.resizing = true;
483+
return;
484+
} else {
485+
that.canvas.focus();
486+
that.canvas_div.focus();
487+
}
488+
}
489+
490+
if (name === 'motion_notify') {
491+
// If the mouse is on the handle, change the cursor style
492+
if (canvas_pos.x >= that.top_canvas.width - that.resize_handle_size &&
493+
canvas_pos.y >= that.top_canvas.height - that.resize_handle_size) {
494+
that.top_canvas.style.cursor = 'nw-resize';
495+
} else {
496+
that.top_canvas.style.cursor = that.model.get('_cursor');
497+
}
404498
}
405499

406500
// Rate-limit the position text updates so that we don't overwhelm the
@@ -421,10 +515,21 @@ var MPLCanvasView = widgets.DOMWidgetView.extend({
421515
* to control all of the cursor setting manually through the
422516
* 'cursor' event from matplotlib */
423517
event.preventDefault();
424-
return false;
425518
};
426519
},
427520

521+
resize_event: function(event) {
522+
if (this.resizing) {
523+
var new_size = utils.get_mouse_position(event, this.top_canvas);
524+
525+
this.model.resize(new_size.x, new_size.y);
526+
}
527+
},
528+
529+
stop_resize_event: function() {
530+
this.resizing = false;
531+
},
532+
428533
key_event: function(name) {
429534
var that = this;
430535
return function(event) {
@@ -458,9 +563,9 @@ var MPLCanvasView = widgets.DOMWidgetView.extend({
458563
};
459564
},
460565

461-
close: function(){
462-
this.send_message('closing');
463-
this.trigger('close');
566+
remove: function(){
567+
window.removeEventListener('mousemove', this._resize_event);
568+
window.removeEventListener('mouseup', this._stop_resize_event);
464569
}
465570
});
466571

js/src/utils.js

+9-8
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@ function offset(el) {
88
};
99

1010
// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas
11-
function get_mouse_position(e) {
11+
function get_mouse_position(e, targ) {
1212
//this section is from http://www.quirksmode.org/js/events_properties.html
13-
var targ;
1413
if (!e)
1514
e = window.event;
16-
if (e.target)
17-
targ = e.target;
18-
else if (e.srcElement)
19-
targ = e.srcElement;
20-
if (targ.nodeType == 3) // defeat Safari bug
21-
targ = targ.parentNode;
15+
if (!targ) {
16+
if (e.target)
17+
targ = e.target;
18+
else if (e.srcElement)
19+
targ = e.srcElement;
20+
if (targ.nodeType == 3) // defeat Safari bug
21+
targ = targ.parentNode;
22+
}
2223

2324
// offset() returns the position of the element relative to the document
2425
var targ_offset = offset(targ);

0 commit comments

Comments
 (0)