Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Navigation indepenent of toolbar. #11

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
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion examples/user_interfaces/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def trigger(self, *args, **kwargs):
fig.canvas.manager.navigation.add_tool('List', ListTools)
if matplotlib.rcParams['backend'] == 'GTK3Cairo':
fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3)

fig.canvas.manager.toolbar.add_tool('zoom', 'foo')
Copy link
Owner

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, if we add List instead of zoom, it might be confusing to see two times the same tool

Copy link
Author

Choose a reason for hiding this comment

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

If you like, though I thought the example should show off (quirky?) possibilities...

# Uncomment to remove the forward button
# fig.canvas.manager.navigation.remove_tool('forward')

Expand Down
138 changes: 80 additions & 58 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
user interaction (key press, toolbar clicks, ..) and the actions in
response to the user inputs.

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

"""
Expand Down Expand Up @@ -3381,23 +3381,19 @@ def remove_tool(self, name):
del self._tools[name]

def add_tools(self, tools):
""" Add multiple tools to `Navigation`
""" Add multiple tools to `NavigationBase`

Parameters
----------
tools : List
List in the form
[[group1, [(Tool1, name1), (Tool2, name2) ...]][group2...]]
where group1 is the name of the group where the
Tool1, Tool2... are going to be added, and name1, name2... are the
names of the tools
tools : {str: class_like}
The tools to add in a {name: tool} dict, see `add_tool` for more
info.
"""

for group, grouptools in tools:
for position, tool in enumerate(grouptools):
self.add_tool(tool[1], tool[0], group, position)
for name, tool in six.iteritems(tools):
self.add_tool(name, tool)

def add_tool(self, name, tool, group=None, position=None):
def add_tool(self, name, tool, *args, **kwargs):
Copy link
Owner

Choose a reason for hiding this comment

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

Please remind me why we want to allow *args and **kwargs to be passed to the tool.__init__? this means, not all tools can be merged into master because the list of tools backend_tools.tools doesn't take arguments

Copy link
Author

Choose a reason for hiding this comment

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

Because passing we only want to pass classes and not objects to add_tool, this means that one can't set up the tool in init, which seems odd to me. The tool I have written, I have to create extra methods such as, and then when I come to add the tool do:

tool = navigation.add_tool('my tool', MyTool)
tool.set_foo(foo)
tool.set_bar(bar)

rather than

navigation.add_tool('my tool', MyTool, foo=foo, bar=bar)

It just feels nicer and more python and more user friendly.

I don't think this will affect tools from getting merged with master at all. If they require other stuff set, then they wouldn't get added via add_tools anyway, and if they didn't require it, then it doesn't matter that they don't get the settings, because they were designed not to require them.

Copy link
Owner

Choose a reason for hiding this comment

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

Ok, you convinced me

"""Add tool to `NavigationBase`

Add a tool to the tools controlled by Navigation
Expand All @@ -3408,14 +3404,18 @@ def add_tool(self, name, tool, group=None, position=None):

Parameters
----------
name : string
name : str
Name of the tool, treated as the ID, has to be unique
tool : string or `matplotlib.backend_tools.ToolBase` derived class
Reference to find the class of the Tool to be added
group: String
Group to position the tool in
position : int or None (default)
Position within its group in the toolbar, if None, it goes at the end
tool : class_like, i.e. str or type
Reference to find the class of the Tool to added.

Notes
-----
args and kwargs get passed directly to the tools constructor.

See Also
--------
matplotlib.backend_tools.ToolBase : The base class for tools.
"""

tool_cls = self._get_cls_to_instantiate(tool)
Expand All @@ -3426,9 +3426,10 @@ def add_tool(self, name, tool, group=None, position=None):
if name in self._tools:
warnings.warn('A tool_cls with the same name already exist, '
'not added')
return
return self._tools[name]

self._tools[name] = tool_cls(self, name, *args, **kwargs)
Copy link
Owner

Choose a reason for hiding this comment

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

why do we need this?
The tool object belongs to ONE and only ONE navigation

Copy link
Author

Choose a reason for hiding this comment

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

Yes, this ensures that. I allowed someone to three options to add a tool to the toolbar.

  1. Give it the name (string) of the tool that navigation already knows about.
  2. Give it a list of [name, class]
  3. A tool object (and thus already has a name).

In this third case, we want to ensure that the tool belongs to THIS navigation, in case it was not set before (or got transferred here from another navigation)

Copy link
Owner

Choose a reason for hiding this comment

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

I really don't like the idea of opening the door to transfter tools between navigations..... I don't know....

Copy link
Author

Choose a reason for hiding this comment

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

Sure, but what do you think of the main-use case, someone calling:

my_tool = MyTool(None, 'foo')
navigation.add_tool('foo', my_tool)

My thoughts went along the lines of we allow this, then we want to make sure that the navigation gets set to this one.

If you think the user should do this, then could make make navigation optional until we make it so in navigation.add_tool. I don't mind either way about whether we allow a tool object to get passed in.

Copy link
Owner

Choose a reason for hiding this comment

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

Navigation is not really optional because the user is not supposed to instantiate the tool by himself, it's navigation that does it.
I don't thing we should plan for something like that, if that starts to happen, then we can raise an error in the __init__ method of the tools

Copy link
Author

Choose a reason for hiding this comment

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

Fair enough, in that case, I would like to add args and kwargs to navigation.add_tool to pass on to the tool when it gets instantiated and I will remove the passing of a tool directly to ToolbarBase.add_tool


self._tools[name] = tool_cls(self, name)
if tool_cls.keymap is not None:
self.set_tool_keymap(name, tool_cls.keymap)

Expand All @@ -3441,14 +3442,12 @@ def add_tool(self, name, tool, group=None, position=None):
else:
self._toggled.setdefault(tool_cls.radio_group, None)

self._tool_added_event(self._tools[name], group, position)
self._tool_added_event(self._tools[name])
return self._tools[name]

def _tool_added_event(self, tool, group, position):
def _tool_added_event(self, tool):
s = 'tool_added_event'
event = ToolEvent(s,
self,
tool,
data={'group': group, 'position': position})
event = ToolEvent(s, self, tool)
self._callbacks.process(s, event)

def _handle_toggle(self, tool, sender, canvasevent, data):
Expand Down Expand Up @@ -3558,67 +3557,88 @@ def tools(self):

return self._tools

def get_tool(self, name):
def get_tool(self, name, warn=True):
"""Return the tool object, also accepts the actual tool for convenience

Parameters
-----------
name : String, ToolBase
name : str, ToolBase
Name of the tool, or the tool itself
warn : bool
If this method should give warnings.
"""
if isinstance(name, tools.ToolBase):
if isinstance(name, tools.ToolBase) and name.name in self._tools:
return name
if name not in self._tools:
warnings.warn("%s is not a tool controlled by Navigation" % name)
if warn:
warnings.warn("Navigation does not control tool %s" % name)
return None
return self._tools[name]


class ToolbarBase(object):
"""Base class for `Toolbar` implementation
class ToolContainerBase(object):
"""Base class for all tool containers, e.g. toolbars.

Attributes
----------
manager : `FigureManager` object that integrates this `Toolbar`
navigation : `NavigationBase` object that hold the tools that
this `Toolbar` wants to communicate with
navigation : `NavigationBase` object that holds the tools that
this `ToolContainer` wants to communicate with.
"""

def __init__(self, navigation):
self.navigation = navigation

self.navigation.nav_connect('tool_message_event', self._message_cbk)
self.navigation.nav_connect('tool_added_event', self._add_tool_cbk)
self.navigation.nav_connect('tool_removed_event',
self._remove_tool_cbk)

def _message_cbk(self, event):
"""Captures the 'tool_message_event' to set the message on the toolbar"""
self.set_message(event.message)

def _tool_triggered_cbk(self, event):
def _tool_toggled_cbk(self, event):
"""Captures the 'tool-trigger-toolname

This only gets used for toggled tools
"""
if event.sender is self:
return
self.toggle_toolitem(event.tool.name, event.tool.toggled)
Copy link
Owner

Choose a reason for hiding this comment

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

why allow to toggle when triggered by self?

Copy link
Author

Choose a reason for hiding this comment

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

Try running the example and see...

Copy link
Owner

Choose a reason for hiding this comment

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

Ok, multiple times the same tool?

Copy link
Author

Choose a reason for hiding this comment

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

Correct. I don't see any reason to outright ban it, i.e. putting in safeguards to prevent it. So we might as well go the whole hog and allow it. It doesn't use up any extra lines of code, and we gain an extra feature, compared to banning it which would add more lines of code.

Copy link
Owner

Choose a reason for hiding this comment

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

Ok, agree


def add_tools(self, tools):
""" Add multiple tools to the container.

Parameters
----------
tools : list
List in the form
[[group1, [tool1, tool2 ...]], [group2, [...]]]
Where the tools given by tool1, and tool2 will display in group1.
See `add_tool` for details.
"""

self.toggle_toolitem(event.tool.name)

def _add_tool_cbk(self, event):
"""Captures 'tool_added_event' and adds the tool to the toolbar"""
image = self._get_image_filename(event.tool.image)
toggle = getattr(event.tool, 'toggled', None) is not None
self.add_toolitem(event.tool.name,
event.data['group'],
event.data['position'],
image,
event.tool.description,
toggle)
for group, grouptools in tools:
for position, tool in enumerate(grouptools):
self.add_tool(tool, group, position)

def add_tool(self, tool, group, position=-1):
"""Adds a tool to this container

Parameters
----------
tool : tool_like
The tool to add, see `NavigationBase.get_tool`.
group : str
The name of the group to add this tool to.
position : int (optional)
The position within the group to place this tool. Defaults to end.
"""
tool = self.navigation.get_tool(tool)
image = self._get_image_filename(tool.image)
toggle = getattr(tool, 'toggled', None) is not None
self.add_toolitem(tool.name, group, position,
image, tool.description, toggle)
if toggle:
self.navigation.nav_connect('tool_trigger_%s' % event.tool.name,
self._tool_triggered_cbk)
self.navigation.nav_connect('tool_trigger_%s' % tool.name,
self._tool_toggled_cbk)

def _remove_tool_cbk(self, event):
"""Captures the 'tool_removed_event' signal and removes the tool"""
Expand All @@ -3641,13 +3661,13 @@ def trigger_tool(self, name):
Parameters
----------
name : String
Name(id) of the tool triggered from within the toolbar
Name(id) of the tool triggered from within the container

"""
self.navigation.tool_trigger_event(name, sender=self)

def add_toolitem(self, name, group, position, image, description, toggle):
"""Add a toolitem to the toolbar
"""Add a toolitem to the container

This method must get implemented per backend

Expand Down Expand Up @@ -3687,18 +3707,20 @@ def set_message(self, s):

pass

def toggle_toolitem(self, name):
def toggle_toolitem(self, name, toggled):
"""Toggle the toolitem without firing event

Parameters
----------
name : String
Id of the tool to toggle
toggled : bool
Whether to set this tool as toggled or not.
"""
raise NotImplementedError

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

This method must get implemented per backend

Expand Down
43 changes: 21 additions & 22 deletions lib/matplotlib/backend_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -903,26 +903,25 @@ def _mouse_move(self, event):
self.navigation.canvas.draw_idle()


tools = [['navigation', [(ToolHome, 'home'),
(ToolBack, 'back'),
(ToolForward, 'forward')]],

['zoompan', [(ToolZoom, 'zoom'),
(ToolPan, 'pan')]],

['layout', [('ToolConfigureSubplots', 'subplots'), ]],

['io', [('ToolSaveFigure', 'save'), ]],

[None, [(ToolGrid, 'grid'),
(ToolFullScreen, 'fullscreen'),
(ToolQuit, 'quit'),
(ToolEnableAllNavigation, 'allnav'),
(ToolEnableNavigation, 'nav'),
(ToolXScale, 'xscale'),
(ToolYScale, 'yscale'),
(ToolCursorPosition, 'position'),
(ToolViewsPositions, 'viewpos'),
('ToolSetCursor', 'cursor'),
('ToolRubberband', 'rubberband')]]]
tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward,
'zoom': ToolZoom, 'pan': ToolPan,
'subplots': 'ToolConfigureSubplots',
'save': 'ToolSaveFigure',
'grid': ToolGrid,
'fullscreen': ToolFullScreen,
'quit': ToolQuit,
'allnav': ToolEnableAllNavigation,
'nav': ToolEnableNavigation,
'xscale': ToolXScale,
'yscale': ToolYScale,
'position': ToolCursorPosition,
'viewpos': ToolViewsPositions,
'cursor': 'ToolSetCursor',
'rubberband': 'ToolRubberband'}
"""Default tools"""

toolbar_tools = [['navigation', ['home', 'back', 'forward']],
Copy link
Owner

Choose a reason for hiding this comment

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

what happen with the idea of the ordereddict?

Copy link
Author

Choose a reason for hiding this comment

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

In the other PR... I put this one forward first to show my plans for getting rid of this altogether:

if group is None:
  return

A convergence of multiple PRs working on the same bit of code to implement different new features ;).

['zoompan', ['zoom', 'pan']],
['layout', ['subplots']],
['io', ['save']]]
"""Default tools in the toolbar"""
31 changes: 14 additions & 17 deletions lib/matplotlib/backends/backend_gtk3.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ def fn_name(): return sys._getframe(1).f_code.co_name
from matplotlib._pylab_helpers import Gcf
from matplotlib.backend_bases import RendererBase, GraphicsContextBase, \
FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, TimerBase
from matplotlib.backend_bases import ShowBase, ToolbarBase, NavigationBase
from matplotlib.backend_bases import ShowBase, ToolContainerBase, NavigationBase
from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase, \
tools, SetCursorBase, RubberbandBase
tools, toolbar_tools, SetCursorBase, RubberbandBase

from matplotlib.cbook import is_string_like, is_writable_file_like
from matplotlib.colors import colorConverter
Expand Down Expand Up @@ -418,6 +418,7 @@ def __init__(self, canvas, num):
self.toolbar = self._get_toolbar()
if matplotlib.rcParams['toolbar'] == 'navigation':
self.navigation.add_tools(tools)
self.toolbar.add_tools(toolbar_tools)

# calculate size for window
w = int (self.canvas.figure.bbox.width)
Expand Down Expand Up @@ -752,9 +753,9 @@ def draw_rubberband(self, x0, y0, x1, y1):
ToolRubberband = RubberbandGTK3


class ToolbarGTK3(ToolbarBase, Gtk.Box):
class ToolbarGTK3(ToolContainerBase, Gtk.Box):
def __init__(self, navigation):
ToolbarBase.__init__(self, navigation)
ToolContainerBase.__init__(self, navigation)
Gtk.Box.__init__(self)
self.set_property("orientation", Gtk.Orientation.VERTICAL)

Expand All @@ -763,7 +764,6 @@ def __init__(self, navigation):
self.pack_start(self._toolbar, False, False, 0)
self._toolbar.show_all()
self._toolitems = {}
self._signals = {}
self._setup_message_area()

def _setup_message_area(self):
Expand All @@ -784,9 +784,6 @@ def _setup_message_area(self):

def add_toolitem(self, name, group, position, image_file, description,
toggle):
if group is None:
return

if toggle:
tbutton = Gtk.ToggleToolButton()
else:
Expand All @@ -805,29 +802,29 @@ def add_toolitem(self, name, group, position, image_file, description,
signal = tbutton.connect('clicked', self._call_tool, name)
tbutton.set_tooltip_text(description)
tbutton.show_all()
self._toolitems[name] = tbutton
self._signals[name] = signal
self._toolitems.setdefault(name, [])
self._toolitems[name].append((tbutton, signal))

def _call_tool(self, btn, name):
self.trigger_tool(name)

def set_message(self, s):
self.message.set_label(s)

def toggle_toolitem(self, name):
def toggle_toolitem(self, name, toggled):
if name not in self._toolitems:
return

status = self._toolitems[name].get_active()
self._toolitems[name].handler_block(self._signals[name])
self._toolitems[name].set_active(not status)
self._toolitems[name].handler_unblock(self._signals[name])
for toolitem, signal in self._toolitems[name]:
Copy link
Owner

Choose a reason for hiding this comment

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

why for?
toolitem, signal = self._toolitems[name]

Copy link
Owner

Choose a reason for hiding this comment

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

ups, sorry, forget about this comment. I didn't realize we were allowing multiple times the same tool

toolitem.handler_block(signal)
toolitem.set_active(toggled)
toolitem.handler_unblock(signal)

def remove_toolitem(self, name):
if name not in self._toolitems:
self.set_message('%s Not in toolbar' % name)
return
self._toolbar.remove(self._toolitems[name])
for toolitem, signal in self._toolitems[name]:
self._toolbar.remove(toolitem)
del self._toolitems[name]

def add_separator(self, pos=-1):
Expand Down