-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
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
Changes from 1 commit
dda0cdc
10f5dc7
08a6020
c6c0ad3
1fc29fa
979875e
c5c4f0f
97dfda7
6b647ad
99667aa
6c19579
0495aac
fb46fc1
d49c431
5ba6210
7ca8626
dcc0f16
bc703e0
68dc711
bb9f1c7
2c2e649
a99367f
3d1be34
afdd34c
5b49c7a
773db88
2ca6926
661417d
90ab64f
55dd149
15ac091
8cd241c
39f5b74
b20dade
ff94301
2cb4501
9d3c977
6e0b7e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
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: |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -3176,6 +3184,25 @@ def set_history_buttons(self): | |
|
||
|
||
class NavigationBase(object): | ||
""" Helper class that groups all the user interactions for a FigureManager | ||
|
||
Attributes | ||
---------- | ||
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, | ||
|
@@ -3244,13 +3271,43 @@ def _get_toolbar(self, toolbar, canvas): | |
return toolbar | ||
|
||
def get_active(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, it is the intended use. |
||
"""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) | ||
|
||
|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
|
@@ -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) | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find this if self.toolbar:
self.toolbar._set_state(name, self._toggled != name, False) and create the extra method in Toolbar{Base,GTK3,TK} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That implies more work for the people creating the toolbars. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? Quoting #2624 (comment)
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure thing :). Looking forward to it, my first github pull request :D. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually for setting the fork, is Then you go to the installation, and then to the workflow. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
What did you consider as
def addTool(self, name, tool)
tool.mpl_connect('tool_state_changed_event', self.set_state)
self._add_toolitem(self, name, tool.description, ...): Otherwise:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @OceanWolf Keep in mind the MAIN GOAL is to make it easy to create There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
@@ -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) | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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')) | ||
|
@@ -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 | ||
10000 | .. 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) | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
white space issue?