Skip to content

Js fixes for key events + ipython notebooks #3965

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

Merged
merged 14 commits into from
Jan 11, 2015

Conversation

tacaswell
Copy link
Member

First pass at getting key events to work in ipython notebooks

Closes #3963

Demo, mouse over the figure to give it focus. Clicking any mouse button or pushing any key should color-cycle the line.

import matplotlib
import itertools
import numpy as np
matplotlib.use('nbagg')
import matplotlib.pyplot as plt
plt.close('all')
fig, ax = plt.subplots()
x = np.linspace(0,10,10000)
y = np.sin(x)
ln, = ax.plot(x,y)
evt = []
colors = iter(itertools.cycle(['r', 'g', 'b', 'k', 'c']))
def on_event(event):
    evt.append(event)
    ln.set_color(next(colors))
    fig.canvas.draw()
    fig.canvas.draw_idle()
fig.canvas.mpl_connect('button_press_event', on_event)
fig.canvas.mpl_connect('key_press_event', on_event)
plt.show()

Remaining issues:

  • mousing out of the figure area does not release focus, is this the correct behavior?
  • not sure I injected the extra style at the right place in the DOM or than I used the right pattern to do it

It was unhappy without it (no idea why)
 - added hooks to inject style onto elements before they are
   added to the DOM (this might be excessive/an anti pattern, but
   I have been doing js for < 6 hrs and this worked)
 - re-ordered to group things in a way that made more sense to me
Changed from the canvas to the canvas div

Not strictly sure this is the right thing to do, but it was on the path
to get things working.
Don't let the handling keep bubbling up from this function.

Again, not sure this is the right thing to do/if it matters.
 - need to make the canvas_div 'focusable'
 - register the element with the keyboard manager
@tacaswell
Copy link
Member Author

I broke this up into a bunch of commits as I spent a long time wandering before I sorted out what the problem was. They can be squashed/discarded before merge.

Also just realized that I should have been doing this against 1.4.x.....

@blink1073
Copy link
Member

Works for me! Yay!

@blink1073
Copy link
Member

I'd say that it should lose focus when the mouse is not there. Also, there is probably a CSS setting to not draw a box around it when it has focus (it interferes with the navigation bar).

@blink1073
Copy link
Member

The following diff removes the outline:

diff --git a/lib/matplotlib/backends/web_backend/mpl.js b/lib/matplotlib/backends/web_backend/mpl.js
index 1836488..b619018 100644
--- a/lib/matplotlib/backends/web_backend/mpl.js
+++ b/lib/matplotlib/backends/web_backend/mpl.js
@@ -112,8 +112,7 @@ mpl.figure.prototype._init_canvas = function() {
         function(event, ui) { fig.request_resize(ui.size.width, ui.size.height); }
         , 50)});

-    canvas_div.attr('style', 'position: relative; clear: both;');
-
+    canvas_div.attr('style', 'position: relative; clear: both; outline: 0');

     function canvas_keyboard_event(event) {
         return fig.key_event(event, event['data']);

@blink1073
Copy link
Member

Diff including mouseout handling:

diff --git a/lib/matplotlib/backends/web_backend/mpl.js b/lib/matplotlib/backends/web_backend/mpl.js
index 1836488..2adcb2f 100644
--- a/lib/matplotlib/backends/web_backend/mpl.js
+++ b/lib/matplotlib/backends/web_backend/mpl.js
@@ -112,8 +112,7 @@ mpl.figure.prototype._init_canvas = function() {
         function(event, ui) { fig.request_resize(ui.size.width, ui.size.height); }
         , 50)});

-    canvas_div.attr('style', 'position: relative; clear: both;');
-
+    canvas_div.attr('style', 'position: relative; clear: both; outline: 0');

     function canvas_keyboard_event(event) {
         return fig.key_event(event, event['data']);
@@ -145,6 +144,11 @@ mpl.figure.prototype._init_canvas = function() {
     canvas_div.append(canvas);
     canvas_div.append(rubberband);

+    function mouse_out_fn(event) {
+        canvas_div.blur();
+    }
+    canvas_div.on("mouseout", mouse_out_fn);
+
     this.rubberband = rubberband;
     this.rubberband_canvas = rubberband[0];
     this.rubberband_context = rubberband[0].getContext("2d");

 - remove frame around canvas_div
 - add mouse-out event to release focus (restoring the keyboard manager
   snarfing of the keyevents) when the mouse leaves the
@tacaswell
Copy link
Member Author

@blink1073 added your diff (faking you as the author).

// off when our div gets focus

// location in version 3
// IPython.notebook.keyboard_manager.register_events(canvas_div);
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, we're going to have to know the IPython version in our JS aren't we? I wanted to avoid it, but there is no alternative as fas as I can see.

Copy link
Member

Choose a reason for hiding this comment

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

FWIW, they kept IPython.keyboard_manager as an alias for backward compatibility.

@pelson
Copy link
Member

pelson commented Jan 4, 2015

@tacaswell - great work. Congratulations on digging into such a complex codebase - there is a lot going on here (mpl backends, mpl web agg backend, mpl nbagg backend, IPython, IPython's JS, browser interactions etc etc.) and you seem to have kept the changes in keeping with the existing codebase. 👍

Can you put a UAT in https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb which describes and tests the functionality? Sadly, I'm not a sophisticated enough JS developer to have automated testing so I pumped for the manual testing approach instead. Also, could you confirm that you've tested these changes and they don't break the web_agg backend?

As for when the keyboard event should get focus, I'm not the biggest fan of needing the mouse over the figure, but equally, I can't think of a better approach on a page which potentially has multiple figures.

Finally, it would be good to figure out what is needed to get plt.draw to work on nbagg - my own experience is that you need to call draw/draw_idle on the figure (as you have done) but that shouldn't be necessary.

I think this can certainly be targeted at v1.4.x (one could argue it is adding a feature, but nbagg is sufficiently experimental in v1.4 that I'm comfortable with making minor improvements for the benefit of fixing a few compatibility issues).

Other than that, awesome! 👍

@tacaswell
Copy link
Member Author

I started to look at the draw/draw_idle issue, but decided to stay focused on getting the keyboard events working.

I am enthusiastic about getting these fixes into 1.4.3 as skimage is looking at using nbagg to replace their qt-based image-viewer code so we should do our best to support that. It also means that nbagg will get it's tires kicked/exposure which is good.

@WeatherGod
Copy link
Member

I am not an expert in focus issues, but from what I have encountered
exploring the traditional backends, the canvas object was always set up to
acquire focus immediately (particularly, Qt has a "StrongFocus" policy set
for it). Then only the currently active window gets interactive events,
irrespective of where the mouse is. This would seem to be a "focus follows
mouse" policy, which would deviate from the traditional backends.

OTOH, perhaps nbagg is probably more akin to an embedding situation rather
than a stand-alone figure window, in which case there is no manager object
that would set up keymaps and focus policies?

As a side note, if the focus terminates when the mouse leaves the figure,
does it come back when the mouse enters the figure? There is a
mouse_enter_figure event (or something like that, IIRC).

On Sun, Jan 4, 2015 at 10:47 AM, Thomas A Caswell notifications@github.com
wrote:

I started to look at the draw/draw_idle issue, but decided to stay
focused on getting the keyboard events working.

I am enthusiastic about getting these fixes into 1.4.3 as skimage is
looking at using nbagg to replace their qt-based image-viewer code so we
should do our best to support that. It also means that nbagg will get it's
tires kicked/exposure which is good.


Reply to this email directly or view it on GitHub
#3965 (comment)
.

@blink1073
Copy link
Member

The following diff makes it behave more like a figure in a Qt window (focus on creation and when the user clicks on the figure):

diff --git a/lib/matplotlib/backends/web_backend/mpl.js b/lib/matplotlib/backends/web_backend/mpl.js
index d9053d8..7aee81c 100644
--- a/lib/matplotlib/backends/web_backend/mpl.js
+++ b/lib/matplotlib/backends/web_backend/mpl.js
@@ -120,6 +120,7 @@ mpl.figure.prototype._init_canvas = function() {

     canvas_div.keydown('key_press', canvas_keyboard_event);
     canvas_div.keydown('key_release', canvas_keyboard_event);
+
     this.canvas_div = canvas_div
     this._canvas_extra_style(canvas_div)
     this.root.append(canvas_div);
@@ -144,11 +145,6 @@ mpl.figure.prototype._init_canvas = function() {
     canvas_div.append(canvas);
     canvas_div.append(rubberband);

-    function mouse_out_fn(event) {
-        canvas_div.blur();
-    }
-    canvas_div.on("mouseout", mouse_out_fn);
-
     this.rubberband = rubberband;
     this.rubberband_canvas = rubberband[0];
     this.rubberband_context = rubberband[0].getContext("2d");
@@ -412,7 +408,7 @@ mpl.findpos = function(e) {
 mpl.figure.prototype.mouse_event = function(event, name) {
     var canvas_pos = mpl.findpos(event)

-    if (this.focus_on_mouseover && name === 'motion_notify')
+    if (name === 'button_press')
     {
         this.canvas_div.focus();
     }
diff --git a/lib/matplotlib/backends/web_backend/nbagg_mpl.js b/lib/matplotlib/backends/web_backend/nbagg_mpl.js
index 7706891..d08b7ca 100644
--- a/lib/matplotlib/backends/web_backend/nbagg_mpl.js
+++ b/lib/matplotlib/backends/web_backend/nbagg_mpl.js
@@ -49,6 +49,8 @@ mpl.mpl_figure_comm = function(comm, msg) {
         return false;
     });

+    fig.canvas_div.focus();
+
 };

 mpl.figure.prototype.handle_close = function(fig, msg) {

@blink1073
Copy link
Member

Better version of the previous (affects webagg as well):

diff --git a/lib/matplotlib/backends/web_backend/mpl.js b/lib/matplotlib/backends/web_backend/mpl.js
index d9053d8..54bccdb 100644
--- a/lib/matplotlib/backends/web_backend/mpl.js
+++ b/lib/matplotlib/backends/web_backend/mpl.js
@@ -120,6 +120,7 @@ mpl.figure.prototype._init_canvas = function() {

     canvas_div.keydown('key_press', canvas_keyboard_event);
     canvas_div.keydown('key_release', canvas_keyboard_event);
+
     this.canvas_div = canvas_div
     this._canvas_extra_style(canvas_div)
     this.root.append(canvas_div);
@@ -144,11 +145,6 @@ mpl.figure.prototype._init_canvas = function() {
     canvas_div.append(canvas);
     canvas_div.append(rubberband);

-    function mouse_out_fn(event) {
-        canvas_div.blur();
-    }
-    canvas_div.on("mouseout", mouse_out_fn);
-
     this.rubberband = rubberband;
     this.rubberband_canvas = rubberband[0];
     this.rubberband_context = rubberband[0].getContext("2d");
@@ -170,6 +166,8 @@ mpl.figure.prototype._init_canvas = function() {
     // Set the figure to an initial 600x600px, this will subsequently be updated
     // upon first draw.
     this._resize_canvas(600, 600);
+
+    this.canvas_div.focus();
 }

 mpl.figure.prototype._init_toolbar = function() {
@@ -412,7 +410,7 @@ mpl.findpos = function(e) {
 mpl.figure.prototype.mouse_event = function(event, name) {
     var canvas_pos = mpl.findpos(event)

-    if (this.focus_on_mouseover && name === 'motion_notify')
+    if (name === 'button_press')
     {
         this.canvas_div.focus();
     }

@blink1073
Copy link
Member

Sad panda 😦, webagg no longer supports keypress or scroll events with these changes, and nbagg still does not support scroll events. (These changes being the PR without the diff above).

@blink1073
Copy link
Member

It looks like webagg needs to focus on canvas, while nbagg needs to focus on canvas_div (from some guess and checking).

@blink1073
Copy link
Member

I may have been mistaken about scroll_event working in webagg before. I reverted to master and it is not working.

@blink1073
Copy link
Member

Another diff for you, that allows keyboard events in both nbagg and webagg:

diff --git a/lib/matplotlib/backends/web_backend/mpl.js b/lib/matplotlib/backends/web_backend/mpl.js
index d9053d8..6ff5f33 100644
--- a/lib/matplotlib/backends/web_backend/mpl.js
+++ b/lib/matplotlib/backends/web_backend/mpl.js
@@ -414,6 +414,7 @@ mpl.figure.prototype.mouse_event = function(event, name) {

     if (this.focus_on_mouseover && name === 'motion_notify')
     {
+        this.canvas.focus();
         this.canvas_div.focus();
     }

@WeatherGod
Copy link
Member

Steven, it is great that you are helping Thomas out with this, I know I
certainly don't have the mental fortitude to grok javascript (there is a
reason why I work in python...). I don't know if you realize it, but you
can submit PR's against Thomas's branch that can make it very easy for him
to incorporate your efforts (and you get the credit, too!).

On Sun, Jan 4, 2015 at 12:56 PM, Steven Silvester notifications@github.com
wrote:

Another diff for you, that allows keyboard events in both nbagg and webagg
:

diff --git a/lib/matplotlib/backends/web_backend/mpl.js b/lib/matplotlib/backends/web_backend/mpl.js
index d9053d8..6ff5f33 100644--- a/lib/matplotlib/backends/web_backend/mpl.js+++ b/lib/matplotlib/backends/web_backend/mpl.js@@ -414,6 +414,7 @@ mpl.figure.prototype.mouse_event = function(event, name) {

 if (this.focus_on_mouseover && name === 'motion_notify')
 {+        this.canvas.focus();
     this.canvas_div.focus();
 }


Reply to this email directly or view it on GitHub
#3965 (comment)
.

@blink1073
Copy link
Member

@WeatherGod, I'm more throwing out ideas than anything (shots in the dark), but point taken. I still can't figure out how to get webagg to take focus on creation. canvas.focus() only seems to work in the callback.

@pelson
Copy link
Member

pelson commented Jan 4, 2015

If I can help with anything specific, just shout - I don't have a huge amount of bandwidth, but I'll happily try to help.

@blink1073
Copy link
Member

Victory! (PR sent).

@blink1073
Copy link
Member

@tacaswell, we'll have to tackle scroll_event in another issue. We use it to set the thickness of the line tool and the size of the paint tool.

@tacaswell
Copy link
Member Author

Right, sorry forgot about that last night.

On Tue, Jan 6, 2015, 02:02 Phil Elson notifications@github.com wrote:

Can you put a UAT in
https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb
which describes and tests the functionality? Sadly, I'm not a sophisticated
enough JS developer to have automated testing so I pumped for the manual
testing approach instead. Also, could you confirm that you've tested these
changes and they don't break the web_agg backend?

Just this action to do from me.


Reply to this email directly or view it on GitHub
#3965 (comment)
.

// off when our div gets focus

// location in version 3
if (typeof IPython.notebook.keyboard_manager != 'undefined') {
Copy link
Member

Choose a reason for hiding this comment

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

I think the canonical way of doing this is if (IPython.notebook.keyboard_manager).

@tacaswell
Copy link
Member Author

@pelson Is that what you had in mind for the UAT?

@tacaswell tacaswell added this to the v1.4.3 milestone Jan 8, 2015
@pelson
Copy link
Member

pelson commented Jan 8, 2015

@pelson Is that what you had in mind for the UAT?

Exactly. I didn't run it yet, but the test seems well described and I imagine it is working for you so 👍 from me.

@@ -93,6 +94,15 @@ mpl.figure.prototype._init_header = function() {
this.header = titletext[0];
}

mpl.figure.prototype._canvas_extra_style = function(canvas_div){
Copy link
Member

Choose a reason for hiding this comment

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

Please insert a space before the {

@mdboom
Copy link
Member

mdboom commented Jan 8, 2015

Just the one minor style nit. Other than that this seems to "work for me"[TM], so 👍

@blink1073
Copy link
Member

@tacaswell, it looks like the error is tangential, perhaps just restart that one job?

@tacaswell
Copy link
Member Author

Restarted

@@ -394,9 +413,10 @@ mpl.findpos = function(e) {
mpl.figure.prototype.mouse_event = function(event, name) {
var canvas_pos = mpl.findpos(event)

if (this.focus_on_mouseover && name === 'motion_notify')
if (name === 'button_press')
Copy link
Member

Choose a reason for hiding this comment

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

I'm not entirely certain why motion notify is no longer important. Would you mind talking me through it? Does the status bar (with the x and y coordinates) still work with this change?

Copy link
Member

Choose a reason for hiding this comment

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

My take: we switched to a more Qt-like focus policy, in that the figure takes focus immediately and upon clicking. When the figure has focus, the status bar receives updates.

Copy link
Member

Choose a reason for hiding this comment

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

I stand corrected, the motion_notify event is triggered regardless of focus, and the coordinates are updated (I just ran the example code).

Copy link
Member Author

Choose a reason for hiding this comment

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

The mouse-move events fire independent of the focus state of the figure. The reason that we had to go through such contortions to get the key events to work is that IPython has a handler at the top of the document tree that eats all of the keyboard events and uses them for their own shortcuts.

Copy link
Member Author

Choose a reason for hiding this comment

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

er, sorry, for the repeat, my gh got logged out due to having issues with ipython + js + clearing the cache and didn't see above until I had already posted.

pelson added a commit that referenced this pull request Jan 11, 2015
Js fixes for key events + ipython notebooks
@pelson pelson merged commit 1a1b82d into matplotlib:master Jan 11, 2015
@pelson
Copy link
Member

pelson commented Jan 11, 2015

Thanks @tacaswell. I merged, though I still have one outstanding question (see above re status bar).
Did you mean for this to go against master and not v1.4.x?

@tacaswell
Copy link
Member Author

It should have been against 1.4.x, but I screwed up, I will back-port.

@tacaswell tacaswell deleted the js_fixes branch January 11, 2015 21:43
@tacaswell
Copy link
Member Author

and @blink1073 should get much of the credit here

@blink1073
Copy link
Member

Yay! Where do I redeem my bitcoin-XP? 😄

@pelson
Copy link
Member

pelson commented Jan 11, 2015

Where do I redeem my bitcoin-XP?

A bar in Austin in July? 😄

@blink1073
Copy link
Member

Perfect.

@WeatherGod
Copy link
Member

Say that after experiencing Austin in July...

On Sun, Jan 11, 2015 at 4:52 PM, Steven Silvester notifications@github.com
wrote:

Perfect.


Reply to this email directly or view it on GitHub
#3965 (comment)
.

@blink1073
Copy link
Member

I was at the conference last year, and being from Florida it was actually quite nice (less humid).

pelson added a commit that referenced this pull request Jan 11, 2015
Js fixes for key events + ipython notebooks
@tacaswell
Copy link
Member Author

backported as 893352f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Missing Events in NbAgg Backend
5 participants