Skip to content

MEP22 implementation for QT backend #9934

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 3 commits into from
Feb 5, 2018
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
20 changes: 20 additions & 0 deletions doc/users/next_whats_new/qt-toolmanager.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Added support for QT in new ToolManager
=======================================

Now it is possible to use the ToolManager with Qt5
For example

import matplotlib

matplotlib.use('QT5AGG')
matplotlib.rcParams['toolbar'] = 'toolmanager'
import matplotlib.pyplot as plt

plt.plot([1,2,3])
plt.show()


Treat the new Tool classes experimental for now, the API will likely change and perhaps the rcParam as well

The main example `examples/user_interfaces/toolmanager_sgskip.py` shows more
details, just adjust the header to use QT instead of GTK3
4 changes: 3 additions & 1 deletion examples/user_interfaces/toolmanager_sgskip.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@

from __future__ import print_function
import matplotlib
# Change to the desired backend
matplotlib.use('GTK3Cairo')
# matplotlib.use('TkAgg')
# matplotlib.use('QT5Agg')
matplotlib.rcParams['toolbar'] = 'toolmanager'
import matplotlib.pyplot as plt
from matplotlib.backend_tools import ToolBase, ToolToggleBase
from gi.repository import Gtk, Gdk


class ListTools(ToolBase):
Expand Down
177 changes: 171 additions & 6 deletions lib/matplotlib/backends/backend_qt5.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
from matplotlib._pylab_helpers import Gcf
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
TimerBase, cursors)
TimerBase, cursors, ToolContainerBase, StatusbarBase)
import matplotlib.backends.qt_editor.figureoptions as figureoptions
from matplotlib.backends.qt_editor.formsubplottool import UiSubplotTool
from matplotlib.figure import Figure
from matplotlib.backend_managers import ToolManager
from matplotlib import backend_tools

from .qt_compat import (
QtCore, QtGui, QtWidgets, _getSaveFileName, is_pyqt5, __version__, QT_API)
Expand Down Expand Up @@ -489,14 +491,23 @@ def __init__(self, canvas, num):

self.window._destroying = False

# add text label to status bar
self.statusbar_label = QtWidgets.QLabel()
self.window.statusBar().addWidget(self.statusbar_label)

self.toolmanager = self._get_toolmanager()
self.toolbar = self._get_toolbar(self.canvas, self.window)
self.statusbar = None

if self.toolmanager:
backend_tools.add_tools_to_manager(self.toolmanager)
if self.toolbar:
backend_tools.add_tools_to_container(self.toolbar)
self.statusbar = StatusbarQt(self.window, self.toolmanager)

if self.toolbar is not None:
self.window.addToolBar(self.toolbar)
self.toolbar.message.connect(self.statusbar_label.setText)
if not self.toolmanager:
# add text label to status bar
statusbar_label = QtWidgets.QLabel()
Copy link
Member

Choose a reason for hiding this comment

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

This code used to happen regardless of self.toolbar. Is it OK that it only happens if its none?

Copy link
Member Author

Choose a reason for hiding this comment

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

toolmanager and the old navigation toolbar are different. So that widget is initialized only if there is no toolmanager

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 can self.toolbar be None? Because even if it was, the old lines 492-494 were called. Now they aren't.

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 really don't know if that was a mistake before.
The reasoning behind is: If you don't want a toolbar, you don't want a statusbar, we don't have separate controls but maybe we should

Copy link
Member

Choose a reason for hiding this comment

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

Isn't the status bar the thing at the top that says "Figure 1"? I think you want that regardless of whether there is a toolbar... Or is it something else?

self.window.statusBar().addWidget(statusbar_label)
self.toolbar.message.connect(statusbar_label.setText)
tbs_height = self.toolbar.sizeHint().height()
else:
tbs_height = 0
Expand Down Expand Up @@ -545,10 +556,19 @@ def _get_toolbar(self, canvas, parent):
# attrs are set
if matplotlib.rcParams['toolbar'] == 'toolbar2':
toolbar = NavigationToolbar2QT(canvas, parent, False)
elif matplotlib.rcParams['toolbar'] == 'toolmanager':
toolbar = ToolbarQt(self.toolmanager, self.window)
else:
toolbar = None
return toolbar

def _get_toolmanager(self):
if matplotlib.rcParams['toolbar'] == 'toolmanager':
toolmanager = ToolManager(self.canvas.figure)
else:
toolmanager = None
return toolmanager

def resize(self, width, height):
'set the canvas size in pixels'
self.window.resize(width, height + self._status_and_tool_height)
Expand Down Expand Up @@ -818,6 +838,151 @@ def _reset(self):
self._widgets[attr].setValue(value)


class ToolbarQt(ToolContainerBase, QtWidgets.QToolBar):
def __init__(self, toolmanager, parent):
ToolContainerBase.__init__(self, toolmanager)
QtWidgets.QToolBar.__init__(self, parent)
self._toolitems = {}
self._groups = {}
self._last = None

@property
def _icon_extension(self):
if is_pyqt5():
return '_large.png'
return '.png'

def add_toolitem(
self, name, group, position, image_file, description, toggle):

button = QtWidgets.QToolButton(self)
button.setIcon(self._icon(image_file))
Copy link
Member

Choose a reason for hiding this comment

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

  • These don't seem to have hi-dpi enabled
  • Whatever the lightning bolt thing is, is not implimented.

figure_1_and_mep22_implementation_for_qt_backend_by_fariza_ _pull_request__9934_ _matplotlib_matplotlib 2

figure_1_and_4__python_testplot_py__python3_6__and_1__jklymak_valdez____leewaves17_lwregrid2neweb_and_7__jklymak_valdez____leewaves17_archivedruns_lwregrid2_python__zsh__and_testplot_py_ ___matplotlib

Copy link
Member Author

Choose a reason for hiding this comment

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

For the hi-dpi I have to check, but for the lightning bolt thing, this is normal. One of the main ideas behind MEP22 and MEP27 is to homogenize the tools and what is available in each backend.
Right now that tool is only available on QT with the navigationtoolbar

This PR is just to implement the basics of MEP22 to QT
Later another PR can add that tool for QT or preferably for all the backends

Copy link
Member

Choose a reason for hiding this comment

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

Fo hi dpi, NavigationToolbar2QT has some logic associated with is_pyqt5() that switches to '_large.png and calls setIconSize.

Copy link
Member Author

Choose a reason for hiding this comment

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

Here the logic is the same, we use a _icon_extension attribute in backend_bases.ToolContainerBase.
In the case of qt it gets replaced with a property

@property
 def _icon_extension(self):
        if is_pyqt5():
            return '_large.png'
        return '.png'

I have to carefully check this. But you are right, this can be done in a separate PR, because it might affect all the backends and not just this one.

button.setText(name)
if description:
button.setToolTip(description)

def handler():
self.trigger_tool(name)
if toggle:
button.setCheckable(True)
button.toggled.connect(handler)
else:
button.clicked.connect(handler)

self._last = button
self._toolitems.setdefault(name, [])
self._add_to_group(group, name, button, position)
self._toolitems[name].append((button, handler))

def _add_to_group(self, group, name, button, position):
gr = self._groups.get(group, [])
if not gr:
sep = self.addSeparator()
gr.append(sep)
before = gr[position]
widget = self.insertWidget(before, button)
gr.insert(position, widget)
self._groups[group] = gr

def _icon(self, name):
pm = QtGui.QPixmap(name)
if hasattr(pm, 'setDevicePixelRatio'):
pm.setDevicePixelRatio(self.toolmanager.canvas._dpi_ratio)
return QtGui.QIcon(pm)

def toggle_toolitem(self, name, toggled):
if name not in self._toolitems:
return
for button, handler in self._toolitems[name]:
button.toggled.disconnect(handler)
button.setChecked(toggled)
button.toggled.connect(handler)

def remove_toolitem(self, name):
for button, handler in self._toolitems[name]:
button.setParent(None)
del self._toolitems[name]


class StatusbarQt(StatusbarBase, QtWidgets.QLabel):
Copy link
Member

Choose a reason for hiding this comment

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

Sorry, maybe I need to read the MEP, but all these buttons existed in Qt before. How did they get set before and do these new buttons supercede the old way or are they alongside the old way? If they replace the old way, then I why is no code deleted?

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 new toolmanager and tools are set to replace the old navigationtoolbar. That is why we have to "repeat" some code
We can not just delete the old one, because many users play with the internals of navigationtoolbar. So for compatibility reasons we have to keep the old code there until it is fully deprecated

Copy link
Member

Choose a reason for hiding this comment

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

OK, but if I use Qt5Agg now, w/o mucking with internals I will get these widgets?

Copy link
Member Author

Choose a reason for hiding this comment

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

@jklymak if you look at the example, the only thing you have to do is

import matplotlib
matplotlib.use('QT5Agg')
matplotlib.rcParams['toolbar'] = 'toolmanager'

at the begining of your code

Copy link
Member

Choose a reason for hiding this comment

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

OK, I figured it out - toolmanager2 is still the default, and this only works if toolmanager is called.

def __init__(self, window, *args, **kwargs):
StatusbarBase.__init__(self, *args, **kwargs)
QtWidgets.QLabel.__init__(self)
window.statusBar().addWidget(self)

def set_message(self, s):
self.setText(s)


class ConfigureSubplotsQt(backend_tools.ConfigureSubplotsBase):
def trigger(self, *args):
image = os.path.join(matplotlib.rcParams['datapath'],
'images', 'matplotlib.png')
parent = self.canvas.manager.window
dia = SubplotToolQt(self.figure, parent)
dia.setWindowIcon(QtGui.QIcon(image))
dia.exec_()


class SaveFigureQt(backend_tools.SaveFigureBase):
def trigger(self, *args):
filetypes = self.canvas.get_supported_filetypes_grouped()
sorted_filetypes = sorted(six.iteritems(filetypes))
default_filetype = self.canvas.get_default_filetype()

startpath = os.path.expanduser(
matplotlib.rcParams['savefig.directory'])
start = os.path.join(startpath, self.canvas.get_default_filename())
filters = []
selectedFilter = None
for name, exts in sorted_filetypes:
exts_list = " ".join(['*.%s' % ext for ext in exts])
filter = '%s (%s)' % (name, exts_list)
if default_filetype in exts:
selectedFilter = filter
filters.append(filter)
filters = ';;'.join(filters)

parent = self.canvas.manager.window
fname, filter = _getSaveFileName(parent,
"Choose a filename to save to",
start, filters, selectedFilter)
if fname:
# Save dir for next time, unless empty str (i.e., use cwd).
if startpath != "":
matplotlib.rcParams['savefig.directory'] = (
os.path.dirname(six.text_type(fname)))
try:
self.canvas.figure.savefig(six.text_type(fname))
except Exception as e:
QtWidgets.QMessageBox.critical(
self, "Error saving file", six.text_type(e),
QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.NoButton)


class SetCursorQt(backend_tools.SetCursorBase):
def set_cursor(self, cursor):
self.canvas.setCursor(cursord[cursor])


class RubberbandQt(backend_tools.RubberbandBase):
def draw_rubberband(self, x0, y0, x1, y1):
height = self.canvas.figure.bbox.height
y1 = height - y1
y0 = height - y0
rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)]
self.canvas.drawRectangle(rect)

def remove_rubberband(self):
self.canvas.drawRectangle(None)


backend_tools.ToolSaveFigure = SaveFigureQt
backend_tools.ToolConfigureSubplots = ConfigureSubplotsQt
backend_tools.ToolSetCursor = SetCursorQt
backend_tools.ToolRubberband = RubberbandQt


def error_msg_qt(msg, parent=None):
if not isinstance(msg, six.string_types):
msg = ','.join(map(str, msg))
Expand Down