8000 MEP22 Navigation toolbar coexistence TODELETE by fariza · Pull Request #2759 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

MEP22 Navigation toolbar coexistence TODELETE #2759

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

Closed
wants to merge 38 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
dda0cdc
navigation and toolbar coexistence
fariza Jan 23, 2014
10f5dc7
mod keypress in figuremanager
fariza Jan 23, 2014
08a6020
extra files
fariza Jan 23, 2014
c6c0ad3
helper methods in toolbar and navigation
fariza Jan 24, 2014
1fc29fa
Adding doc to base methods
fariza Jan 24, 2014
979875e
property for active_toggle
fariza Jan 26, 2014
c5c4f0f
simulate click
fariza Jan 27, 2014
97dfda7
pep8 backend_tools
fariza Jan 27, 2014
6b647ad
activate renamed to trigger
fariza Jan 28, 2014
99667aa
toggle tools using enable/disable from its trigger method
fariza Jan 29, 2014
6c19579
simplifying _handle_toggle
fariza Jan 29, 2014
0495aac
reducing number of locks
fariza Jan 29, 2014
fb46fc1
pep8 correction
fariza Jan 29, 2014
d49c431
changing toggle and persistent attributes for issubclass
fariza Feb 4, 2014
5ba6210
bug in combined key press
fariza Feb 4, 2014
7ca8626
untoggle zoom and pan from keypress while toggled
fariza Feb 4, 2014
dcc0f16
classmethods for default tools modification
fariza Feb 6, 2014
bc703e0
six fixes
fariza Apr 24, 2014
68dc711
adding zaxis and some pep8
fariza May 1, 2014
bb9f1c7
removing legacy method dynamic update
fariza May 6, 2014
2c2e649
tk backend
fariza May 6, 2014
a99367f
pep8
fariza May 6, 2014
3d1be34
example working with Tk
fariza May 6, 2014
afdd34c
cleanup
fariza May 7, 2014
5b49c7a
duplicate code in keymap tool initialization
fariza Jul 24, 2014
773db88
grammar corrections
fariza Jul 24, 2014
2ca6926
moving views and positions to tools
fariza Jul 24, 2014
661417d
The views positions mixin automatically adds the clear as axobserver
fariza Jul 25, 2014
90ab64f
bug when navigation was not defined
fariza Jul 25, 2014
55dd149
Small refactor so that we first initiate the Navigation (ToolManager)…
OceanWolf Jul 28, 2014
15ac091
Update for Sphinx documentation
OceanWolf Jul 28, 2014
8cd241c
Moved default_tool initilisation to FigureManagerBase and cleaned.
OceanWolf Jul 29, 2014
39f5b74
Fix navigation
OceanWolf Jul 29, 2014
b20dade
Temporary fix to backends
OceanWolf Jul 29, 2014
ff94301
Merge pull request #1 from OceanWolf/navigation-toolbar-coexistence-e…
fariza Jul 29, 2014
2cb4501
removing persistent tools
fariza Sep 3, 2014
9d3c977
removing unregister
fariza Sep 4, 2014
6e0b7e6
change cursor inmediately after toggle
fariza Sep 5, 2014
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
Prev Previous commit
Next Next commit
Adding doc to base methods
  • Loading branch information
fariza committed Apr 24, 2014
commit 1fc29fa6c5bcaecf894fb32b2e19599b4e6a21ce
8 changes: 8 additions & 0 deletions doc/api/backend_tools_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

:mod:`matplotlib.backend_tools`
================================

.. automodule:: matplotlib.backend_tools
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions doc/api/index_backend_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ backends
.. toctree::

backend_bases_api.rst
backend_tools_api.rst
backend_gtkagg_api.rst
backend_qt4agg_api.rst
backend_wxagg_api.rst
Expand Down
226 changes: 204 additions & 22 deletions lib/matplotlib/backend_bases.py
10000
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
the 'show' callable is then set to Show.__call__, inherited from
ShowBase.

:class:`NavigationBase`
The base class for the Navigation class that makes the bridge between
user interaction (key press, toolbar clicks, ..) and the actions in
response to the user inputs.

:class:`ToolbarBase`
The base class for the Toolbar class of each interactive backend.

"""

from __future__ import (absolute_import, division, print_function,
Expand Down Expand Up @@ -3176,6 +3184,25 @@ def set_history_buttons(self):


class NavigationBase(object):
""" Helper class that groups all the user interactions for a FigureManager

Attributes
Copy link
Member

Choose a reason for hiding this comment

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

white space issue?

----------
canvas : `FigureCanvas` instance
toolbar : `Toolbar` instance that is controlled by this `Navigation`
keypresslock : `LockDraw` to direct the `canvas` key_press_event
movelock: `LockDraw` to direct the `canvas` motion_notify_event
presslock: `LockDraw` to direct the `canvas` button_press_event
releaselock: `LockDraw` to direct the `canvas` button_release_event
canvaslock: shortcut to `canvas.widgetlock`

Notes
--------_
The following methos are for implementation pourposes and not for user use
For these reason they are defined as **_methodname** (private)

.. automethod:: _toolbar_callback
"""
_default_cursor = cursors.POINTER
_default_tools = [tools.ToolToggleGrid,
tools.ToolToggleFullScreen,
Expand Down Expand Up @@ -3244,13 +3271,43 @@ def _get_toolbar(self, toolbar, canvas):
return toolbar

def get_active(self):
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 it would be better to split this into two functions, one which returns a a list of what is installed, and one function that returns self._toggled. It might also be better to do the later as a property.

The use case for this check is to be able to easily check if one of the tools is toggled to disable functions in user written functions so it should be as streamlined as possible, something like

@property
def active_toggle(self):
    return self._toggled
if tool_bar.active_toggle is None:
    # do stuff

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree, it is the intended use.
At the same time, I hope people will use the Tools clases, so they create their own tools, that takes care of the currently toggled conflict.

"""Get the active tools

Returns
----------
A dictionary with the following elements
* `toggled`: The currently toggled Tool or None
* `instances`: List of the currently active tool instances
that are registered with Navigation

"""
return {'toggled': self._toggled, 'instances': self._instances.keys()}

def get_tool_keymap(self, name):
"""Get the keymap associated with a tool

Parameters
----------
name : string
Name of the Tool

Returns
----------
Keymap : list of keys associated with the Tool
"""
keys = [k for k, i in self._keys.items() if i == name]
return keys

def set_tool_keymap(self, name, *keys):
"""Set the keymap associated with a tool

Parameters
----------
name : string
Name of the Tool
keys : keys to associated with the Tool
"""

if name not in self._tools:
raise AttributeError('%s not in Tools' % name)

Expand All @@ -3266,50 +3323,79 @@ def set_tool_keymap(self, name, *keys):
self._keys[k] = name

def unregister(self, name):
"""Unregister the tool from the active instances

Notes
-----
This method is used by `PersistentTools` to remove the reference kept
by `Navigation`.

It is usually called by the `deactivate` method or during
destroy if it is a graphical Tool.

If called, next time the `Tool` is used it will be reinstantiated instead
of using the existing instance.
"""
if self._toggled == name:
self._handle_toggle(name, from_toolbar=False)
if name in self._instances:
del self._instances[name]

def remove_tool(self, name):
"""Remove tool from the `Navigation`

Parameters
----------
name : string
Name of the Tool
"""
self.unregister(name)
del self._tools[name]
keys = [k for k, v in self._keys.items() if v == name]
for k in keys:
del self._keys[k]

if self.toolbar:
self.toolbar.remove_toolitem(name)
self.toolbar._remove_toolitem(name)

def add_tool(self, tool):
"""Add tool to `Navigation`

def add_tool(self, callback_class):
tool = self._get_cls_to_instantiate(callback_class)
name = tool.name
Parameters
----------
tool : string or `Tool` class
Reference to find the class of the Tool to be added
"""
tool_cls = self._get_cls_to_instantiate(tool)
name = tool_cls.name
if name is None:
warnings.warn('Tools need a name to be added, it is used as ID')
warnings.warn('tool_clss need a name to be added, it is used '
'as ID')
return
if name in self._tools:
warnings.warn('A tool with the same name already exist, not added')
warnings.warn('A tool_cls with the same name already exist, '
'not added')

return

self._tools[name] = tool
if tool.keymap is not None:
for k in validate_stringlist(tool.keymap):
self._tools[name] = tool_cls
if tool_cls.keymap is not None:
for k in validate_stringlist(tool_cls.keymap):
Copy link
Member

Choose a reason for hiding this comment

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

We have duplicated code here, not so nice for maintenance, should replace for loop with something like:

self.set_tool_keymap(name, tool_cls.keymap)

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch. I didn't see it before

if k in self._keys:
warnings.warn('Key %s changed from %s to %s' %
(k, self._keys[k], name))
self._keys[k] = name

if self.toolbar and tool.position is not None:
if self.toolbar and tool_cls.position is not None:
basedir = os.path.join(rcParams['datapath'], 'images')
if tool.image is not None:
fname = os.path.join(basedir, tool.image + '.png')
if tool_cls.image is not None:
fname = os.path.join(basedir, tool_cls.image + '.png')
else:
fname = None
self.toolbar.add_toolitem(name, tool.description,
self.toolbar._add_toolitem(name, tool_cls.description,
fname,
tool.position,
tool.toggle)
tool_cls.position,
tool_cls.toggle)

def _get_cls_to_instantiate(self, callback_class):
if isinstance(callback_class, basestring):
Expand Down Expand Up @@ -3359,7 +3445,18 @@ def _get_instance(self, name):

return self._instances[name]

def toolbar_callback(self, name):
def _toolbar_callback(self, name):
"""Callback for the `Toolbar`

All Toolbar implementations have to call this method to signal that a
toolitem was clicked on

Parameters
----------
name : string
Name of the tool that was activated (click) by the user using the
toolbar
"""
tool = self._tools[name]
if tool.toggle:
self._handle_toggle(name, from_toolbar=True)
Expand All @@ -3372,7 +3469,7 @@ def toolbar_callback(self, name):
def _handle_toggle(self, name, event=None, from_toolbar=False):
#toggle toolbar without callback
if not from_toolbar and self.toolbar:
self. 8000 toolbar.toggle(name, False)
self.toolbar._toggle(name, False)
Copy link
Member

Choose a reason for hiding this comment

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

I find this from_toolbar argument messy, it shouldn't matter where the request to toggle the tool between an on/off state came from. I can see why you did it but I think, we don't need it, basically rewrite the above if block as:

if self.toolbar:
  self.toolbar._set_state(name, self._toggled != name, False)

and create the extra method in Toolbar{Base,GTK3,TK}

Copy link
Member Author

Choose a reason for hiding this comment

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

That implies more work for the people creating the toolbars.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, but not too much extra work, only a 5 lines of extra code, per backend I would imagine; and makes the method calls clearer, especially in the future if/when other methods for selecting tools come about.

If the extra work remains the only concern, then the _toggle method could get taken out of the specific backends entirely, replacing the _toggle method with a _set_state method, with ToolbarBase still having a toggle method, but it can do the logic of which state to set instead of the specific toolbar. Taking ToolbarGTK3 as an example this would use even less code.

Taking these ideas one step further, what do you think about replacing my suggested codeblock above with something more like:

event = ToolStateChangedEvent(tool)
self.callbacks.process('tool_state_changed_event', event)

where the argument passed to ToolStateChangedEvent, either the name and the state, or the tool itself (which should encapsulate those two items).

Through something like this we generalise the toggling mechanism, and allow for an arbitrary number of observers, say for example someone in the future wants to add a right click menu with tools inside.

Copy link
Member Author

Choose a reason for hiding this comment

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

@OceanWolf can you try to make a PR against my branch with this change?
Please include only this change so I can review it easily.

Quoting #2624 (comment)

@fariza If you have a local copy of my branch, make your changes on the tip and then push your branch to your github repo (just like making changes against master).

The PR interface in github has issues with large networks. The advice they gave us was to edit the URL by hand to point the PR against the correct repo/branch.

Copy link
Member

Choose a reason for hiding this comment

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

Sure thing :). Looking forward to it, my first github pull request :D.

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 only difference is that you create a branch based on my branch, and that the PR is against my branch (on my repo) not master (on matplotlib repo)

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually for setting the fork, is
http://matplotlib.org/devel/gitwash/forking_hell.html#forking
http://matplotlib.org/devel/gitwash/set_up_fork.html#set-up-fork

Then you go to the installation, and then to the workflow.

Copy link
Member

Choose a reason for hiding this comment

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

Okay, I have the navigation example working! :D. Thank you for your help!

To clarify, by:

Please include only this change so I can review it easily.

What did you consider as this change? As I have started coding, I have realised that:

  1. The event framework should get moved out of FigureManagerBase and into a mixin, otherwise I would duplicate code.
  2. The most elegant solution IMO lies in making the toolbar aware of its tools as discussed previously, i.e. in ToolbarBase the following rough code:
def addTool(self, name, tool)
  tool.mpl_connect('tool_state_changed_event', self.set_state)
  self._add_toolitem(self, name, tool.description, ...):

Otherwise:

  • ToolbarBase needs to know of all Navigation, and thus has access to all tools, and...
  • The toolbar gets notified of tool status changes even if the tool does not exist within the toolbar, which I see happens at the moment and gives an error in the message area of the toolbar, which seems like an unneeded notification, and repetitive code for the toolbar which the specific backend people will have to write.

Copy link
Member Author

Choose a reason for hiding this comment

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

@OceanWolf
The event framework has to remain in FigureManagerBase. If you want, later it can be addressed with other MEP. That is too big of a change to be included in this MEP.
I know it is tempting to solve all the problems in one shot ;).

Keep in mind the MAIN GOAL is to make it easy to create Tools, and Toolbars, and to reconfigure them. Navigation is just a side effect needed to get there.
I think we are there, we attain the main goal, currently we are just sanding the corners to see if the core developers like what they see.

Copy link
Member

Choose a reason for hiding this comment

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

@fariza Yes, I see what you mean. Very tempting indeed, for me because of the desire to keep backward comparability, knowing that when other problems get solved, it may well in turn enable a better/clearer API for tools.

I have made quite a bit of progress, so I shall keep going a bit further to see the end result, if I may. Not too much further to go :).


instance = self._get_instance(name)
if self._toggled is None:
Expand All @@ -3385,7 +3482,7 @@ def _handle_toggle(self, name, event=None, from_toolbar=False):

else:
if self.toolbar:
self.toolbar.toggle(self._toggled, False)
self.toolbar._toggle(self._toggled, False)

self._get_instance(self._toggled).deactivate(None)
instance.activate(None)
Expand All @@ -3395,6 +3492,7 @@ def _handle_toggle(self, name, event=None, from_toolbar=False):
a.set_navigate_mode(self._toggled)

def list_tools(self):
"""Print the list the tools controlled by `Navigation`"""
print ('_' * 80)
Copy link
Member

Choose a reason for hiding this comment

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

you are going to need to import the print function from the future and use the print function here.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's already imported at the beginning of the file

print ("{0:20} {1:50} {2}".format('Name (id)', 'Tool description',
'Keymap'))
Expand Down Expand Up @@ -3541,29 +3639,113 @@ def draw_rubberband(self, event, x0, y0, x1, y1):


class ToolbarBase(object):
"""Base class for `Toolbar` implementation

Attributes
----------
manager : `FigureManager` instance that integrates this `Toolbar`

Notes
-----
The following methos are for implementation pourposes and not for user use.
For these reason they are defined as **_methodname** (private)

.. automethod:: _toggle
.. automethod:: _add_toolitem
.. automethod:: _remove_toolitem
"""
def __init__(self, manager):
self.manager = manager

def add_toolitem(self, name, description, image_file, position,
def _add_toolitem(self, name, description, image_file, position,
toggle):
"""Add a toolitem to the toolbar

The callback associated with the button click event,
must be **EXACTLY** `self.manager.navigation._toolbar_callback(name)`

Parameters
----------
name : string
Name of the tool to add, this is used as ID and as default label
of the buttons
description : string
Description of the tool, used for the tooltips
image_file : string
Filename of the image for the button or `None`
position : integer
Position of the toolitem within the other toolitems
if -1 at the End
toggle : bool
* `True` : The button is a toggle (change the pressed/unpressed
state between consecutive clicks)
* `False` : The button is a normal button (returns to unpressed
state after release)
"""
Copy link
Member

Choose a reason for hiding this comment

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

Do we need so many arguments to this function? Could we not simplify this to:

def _add_toolitem(self, name, tool, position=-1):

where name and position give the same as above (where I use a -1 to indicate the end); the other arguments simply appear in tool, all nice and encapsulated.

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 idea, is that the toolbar, doesn't need to know anything about the tool. In my experience, this simplifies the toolbar creation for a specific backend

Copy link
Member

Choose a reason for hiding this comment

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

Ahh okay. I don't think it has to apply to the specific backends though, just that the logic moves a bit closer to where it gets used, i.e. here in ToolbarBase which should do this work, no need to touch any of the specific backends.

I guess my philosophy for this comes from the thin-controller, thick-model paradigm, viewing NavigationBase as a controller; the specific Toolbars as the views; the Tools as the models; and ToolbarBase as the general model for the specific Toolbars.

From this perspective, the Toolbar model has Tools associated with it, using OOP terminology. So NavigationBase becomes the ToolManager for all tools; and ToolbarBase knows, as a model, which subset of Tools it has the responsibility of displaying.

raise NotImplementedError

def add_separator(self, pos):
"""Add a separator

Parameters
----------
pos : integer
Position where to add the separator within the toolitems
if -1 at the end
"""
pass

def set_message(self, s):
"""Display a message on toolbar or in status bar"""
pass

def toggle(self, name, callback=False):
def _toggle(self, name, callback=False):
"""Toogle a button

Parameters
----------
name : string
Name of the button to toggle
callback : bool
* `True`: call the button callback during toggle
* `False`: toggle the button without calling the callback

"""
#carefull, callback means to perform or not the callback while toggling
raise NotImplementedError

def remove_toolitem(self, name):
pass
def _remove_toolitem(self, name):
"""Remove a toolitem from the `Toolbar`

Parameters
----------
name : string
Name of the tool to remove

"""
raise NotImplementedError

def move_toolitem(self, pos_ini, pos_fin):
"""Change the position of a toolitem

Parameters
----------
pos_ini : integer
Initial position of the toolitem to move
pos_fin : integer
Final position of the toolitem
"""
pass

def set_toolitem_visibility(self, name, visible):
"""Change the visibility of a toolitem

Parameters
----------
name : string
Name of the `Tool`
visible : bool
* `True`: set the toolitem visible
* `False`: set the toolitem invisible
"""
pass
Loading
0