From 44ce103457bd4df125bdab905144e0afc943c11d Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 28 Oct 2015 16:05:38 -0700 Subject: [PATCH 01/11] Sort and uniquify style entries in figure options. Fixes the first two points of #5341. --- .../backends/qt_editor/figureoptions.py | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 75abf9ddeeda..d434bb43ecf8 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -78,29 +78,38 @@ def figure_edit(axes, parent=None): continue linedict[label] = line curves = [] - linestyles = list(six.iteritems(LINESTYLES)) - drawstyles = list(six.iteritems(DRAWSTYLES)) - markers = list(six.iteritems(MARKERS)) + + def prepare_data(d, init): + """Prepare entry for FormLayout. + """ + # List items in dict, dropping duplicate values, sorting by values. + kvs = [(k, v) for v, k in + sorted({v: k for k, v in d.items()}.items())] + # Find the unique kept key with the same value as the init value. + canonical_init, = ({k for k, v in d.items() if v == d[init]}. + intersection(k for k, v in kvs)) + return [canonical_init] + kvs + curvelabels = sorted(linedict.keys()) for label in curvelabels: line = linedict[label] color = rgb2hex(colorConverter.to_rgb(line.get_color())) ec = rgb2hex(colorConverter.to_rgb(line.get_markeredgecolor())) fc = rgb2hex(colorConverter.to_rgb(line.get_markerfacecolor())) - curvedata = [('Label', label), - sep, - (None, 'Line'), - ('Line Style', [line.get_linestyle()] + linestyles), - ('Draw Style', [line.get_drawstyle()] + drawstyles), - ('Width', line.get_linewidth()), - ('Color', color), - sep, - (None, 'Marker'), - ('Style', [line.get_marker()] + markers), - ('Size', line.get_markersize()), - ('Facecolor', fc), - ('Edgecolor', ec), - ] + curvedata = [ + ('Label', label), + sep, + (None, 'Line'), + ('Line Style', prepare_data(LINESTYLES, line.get_linestyle())), + ('Draw Style', prepare_data(DRAWSTYLES, line.get_drawstyle())), + ('Width', line.get_linewidth()), + ('Color', color), + sep, + (None, 'Marker'), + ('Style', prepare_data(MARKERS, line.get_marker())), + ('Size', line.get_markersize()), + ('Facecolor', fc), + ('Edgecolor', ec)] curves.append([curvedata, label, ""]) # make sure that there is at least one displayed curve From 68b42d971f47ca28b8233f111491a141c5260d84 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 12 Nov 2015 09:35:47 -0800 Subject: [PATCH 02/11] Clarify the implementation of prepare_data. --- .../backends/qt_editor/figureoptions.py | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index d434bb43ecf8..c72c48852150 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -81,14 +81,27 @@ def figure_edit(axes, parent=None): def prepare_data(d, init): """Prepare entry for FormLayout. + + `d` is a mapping of shorthands to style names (a single style may + have multiple shorthands, in particular the shorthands `None`, + `"None"`, `"none"` and `""` are synonyms); `init` is one shorthand + of the initial style. + + This function returns an list suitable for initializing a + FormLayout combobox, namely `[initial_name, (shorthand, + style_name), (shorthand, style_name), ...]`. """ - # List items in dict, dropping duplicate values, sorting by values. - kvs = [(k, v) for v, k in - sorted({v: k for k, v in d.items()}.items())] - # Find the unique kept key with the same value as the init value. - canonical_init, = ({k for k, v in d.items() if v == d[init]}. - intersection(k for k, v in kvs)) - return [canonical_init] + kvs + # Drop duplicate shorthands from dict (by overwriting them during + # the dict comprehension). + name2short = {name: short for short, name in d.items()} + # Convert back to {shorthand: name}. + short2name = {short: name for name, short in name2short.items()} + # Find the kept shorthand for the style specified by init. + canonical_init = name2short[d[init]] + # Sort by representation and prepend the initial value. + return ([canonical_init] + + sorted(short2name.items(), + key=lambda short_and_name: short_and_name[1])) curvelabels = sorted(linedict.keys()) for label in curvelabels: From 240d24b15e29e1ec4f9689e51725fb01d4ba124d Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 12 Nov 2015 01:24:12 -0800 Subject: [PATCH 03/11] Set image cmap from figure options. This patch allows the user to interactively set an image's colormap from the axes and lines (and now images) parameters editor (in the Qt backend). Colormaps are listed from those registered with matplotlib.cm. Colorbars seem to be handled properly. --- lib/matplotlib/axes/_base.py | 2 + .../backends/qt_editor/figureoptions.py | 198 ++++++++++-------- 2 files changed, 114 insertions(+), 86 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 1e50188d14f0..05c4e43265d9 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1732,6 +1732,8 @@ def add_image(self, image): Returns the image. """ self._set_artist_props(image) + if not image.get_label(): + image.set_label('_image%d' % len(self.images)) self.images.append(image) image._remove_method = lambda h: self.images.remove(h) self.stale = True diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index c72c48852150..37668f4f961a 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -16,7 +16,7 @@ import matplotlib.backends.qt_editor.formlayout as formlayout from matplotlib.backends.qt_compat import QtGui -from matplotlib import markers +from matplotlib import cm, markers from matplotlib.colors import colorConverter, rgb2hex @@ -43,8 +43,6 @@ def figure_edit(axes, parent=None): """Edit matplotlib figure options""" sep = (None, None) # separator - has_curve = len(axes.get_lines()) > 0 - # Get / General xmin, xmax = axes.get_xlim() ymin, ymax = axes.get_ylim() @@ -69,79 +67,103 @@ def figure_edit(axes, parent=None): xunits = axes.xaxis.get_units() yunits = axes.yaxis.get_units() - if has_curve: - # Get / Curves - linedict = {} - for line in axes.get_lines(): - label = line.get_label() - if label == '_nolegend_': - continue - linedict[label] = line - curves = [] - - def prepare_data(d, init): - """Prepare entry for FormLayout. - - `d` is a mapping of shorthands to style names (a single style may - have multiple shorthands, in particular the shorthands `None`, - `"None"`, `"none"` and `""` are synonyms); `init` is one shorthand - of the initial style. - - This function returns an list suitable for initializing a - FormLayout combobox, namely `[initial_name, (shorthand, - style_name), (shorthand, style_name), ...]`. - """ - # Drop duplicate shorthands from dict (by overwriting them during - # the dict comprehension). - name2short = {name: short for short, name in d.items()} - # Convert back to {shorthand: name}. - short2name = {short: name for name, short in name2short.items()} - # Find the kept shorthand for the style specified by init. - canonical_init = name2short[d[init]] - # Sort by representation and prepend the initial value. - return ([canonical_init] + - sorted(short2name.items(), - key=lambda short_and_name: short_and_name[1])) - - curvelabels = sorted(linedict.keys()) - for label in curvelabels: - line = linedict[label] - color = rgb2hex(colorConverter.to_rgb(line.get_color())) - ec = rgb2hex(colorConverter.to_rgb(line.get_markeredgecolor())) - fc = rgb2hex(colorConverter.to_rgb(line.get_markerfacecolor())) - curvedata = [ - ('Label', label), - sep, - (None, 'Line'), - ('Line Style', prepare_data(LINESTYLES, line.get_linestyle())), - ('Draw Style', prepare_data(DRAWSTYLES, line.get_drawstyle())), - ('Width', line.get_linewidth()), - ('Color', color), - sep, - (None, 'Marker'), - ('Style', prepare_data(MARKERS, line.get_marker())), - ('Size', line.get_markersize()), - ('Facecolor', fc), - ('Edgecolor', ec)] - curves.append([curvedata, label, ""]) - - # make sure that there is at least one displayed curve - has_curve = bool(curves) + # Get / Curves + linedict = {} + for line in axes.get_lines(): + label = line.get_label() + if label == '_nolegend_': + continue + linedict[label] = line + curves = [] + + def prepare_data(d, init): + """Prepare entry for FormLayout. + + `d` is a mapping of shorthands to style names (a single style may + have multiple shorthands, in particular the shorthands `None`, + `"None"`, `"none"` and `""` are synonyms); `init` is one shorthand + of the initial style. + + This function returns an list suitable for initializing a + FormLayout combobox, namely `[initial_name, (shorthand, + style_name), (shorthand, style_name), ...]`. + """ + # Drop duplicate shorthands from dict (by overwriting them during + # the dict comprehension). + name2short = {name: short for short, name in d.items()} + # Convert back to {shorthand: name}. + short2name = {short: name for name, short in name2short.items()} + # Find the kept shorthand for the style specified by init. + canonical_init = name2short[d[init]] + # Sort by representation and prepend the initial value. + return ([canonical_init] + + sorted(short2name.items(), + key=lambda short_and_name: short_and_name[1])) + + curvelabels = sorted(linedict.keys()) + for label in curvelabels: + line = linedict[label] + color = rgb2hex(colorConverter.to_rgb(line.get_color())) + ec = rgb2hex(colorConverter.to_rgb(line.get_markeredgecolor())) + fc = rgb2hex(colorConverter.to_rgb(line.get_markerfacecolor())) + curvedata = [ + ('Label', label), + sep, + (None, 'Line'), + ('Line Style', prepare_data(LINESTYLES, line.get_linestyle())), + ('Draw Style', prepare_data(DRAWSTYLES, line.get_drawstyle())), + ('Width', line.get_linewidth()), + ('Color', color), + sep, + (None, 'Marker'), + ('Style', prepare_data(MARKERS, line.get_marker())), + ('Size', line.get_markersize()), + ('Facecolor', fc), + ('Edgecolor', ec)] + curves.append([curvedata, label, ""]) + # Is there a curve displayed? + has_curve = bool(curves) + + # Get / Images + imagedict = {} + for image in axes.get_images(): + label = image.get_label() + if label == '_nolegend_': + continue + imagedict[label] = image + imagelabels = sorted(imagedict) + images = [] + cmaps = [(cmap, name) for name, cmap in sorted(cm.cmap_d.items())] + for label in imagelabels: + image = imagedict[label] + cmap = image.get_cmap() + if cmap not in cm.cmap_d: + cmaps = [(cmap, cmap.name)] + cmaps + imagedata = [ + ('Label', label), + ('Colormap', [cmap.name] + cmaps) + ] + images.append([imagedata, label, ""]) + # Is there an image displayed? + has_image = bool(images) datalist = [(general, "Axes", "")] - if has_curve: + if curves: datalist.append((curves, "Curves", "")) + if images: + datalist.append((images, "Images", "")) def apply_callback(data): """This function will be called to apply changes""" - if has_curve: - general, curves = data - else: - general, = data + general = data.pop(0) + curves = data.pop(0) if has_curve else [] + images = data.pop(0) if has_image else [] + if data: + raise ValueError("Unexpected field") # Set / General - title, xmin, xmax, xlabel, xscale, ymin, ymax, ylabel, yscale, \ - generate_legend = general + (title, xmin, xmax, xlabel, xscale, ymin, ymax, ylabel, yscale, + generate_legend) = general if axes.get_xscale() != xscale: axes.set_xscale(xscale) @@ -162,26 +184,30 @@ def apply_callback(data): axes.xaxis._update_axisinfo() axes.yaxis._update_axisinfo() - if has_curve: - # Set / Curves - for index, curve in enumerate(curves): - line = linedict[curvelabels[index]] - label, linestyle, drawstyle, linewidth, color, \ - marker, markersize, markerfacecolor, markeredgecolor \ - = curve - line.set_label(label) - line.set_linestyle(linestyle) - line.set_drawstyle(drawstyle) - line.set_linewidth(linewidth) - line.set_color(color) - if marker is not 'none': - line.set_marker(marker) - line.set_markersize(markersize) - line.set_markerfacecolor(markerfacecolor) - line.set_markeredgecolor(markeredgecolor) + # Set / Curves + for index, curve in enumerate(curves): + line = linedict[curvelabels[index]] + (label, linestyle, drawstyle, linewidth, color, marker, markersize, + markerfacecolor, markeredgecolor) = curve + line.set_label(label) + line.set_linestyle(linestyle) + line.set_drawstyle(drawstyle) + line.set_linewidth(linewidth) + line.set_color(color) + if marker is not 'none': + line.set_marker(marker) + line.set_markersize(markersize) + line.set_markerfacecolor(markerfacecolor) + line.set_markeredgecolor(markeredgecolor) + + # Set / Images + for index, image_settings in enumerate(images): + image = imagedict[imagelabels[index]] + label, cmap = image_settings + image.set_label(label) + image.set_cmap(cm.get_cmap(cmap)) # re-generate legend, if checkbox is checked - if generate_legend: draggable = None ncol = 1 From cf4b2196a06742c2d1cdea756b5f11274b7bd62c Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 28 Apr 2016 10:57:45 -0700 Subject: [PATCH 04/11] Avoid duplicate cmap in image options. The previous implementation was incorrect and would yield a double entry in the cmap combobox. --- lib/matplotlib/backends/qt_editor/figureoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 37668f4f961a..b83e6a9b394c 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -137,7 +137,7 @@ def prepare_data(d, init): for label in imagelabels: image = imagedict[label] cmap = image.get_cmap() - if cmap not in cm.cmap_d: + if cmap not in cm.cmap_d.values(): cmaps = [(cmap, cmap.name)] + cmaps imagedata = [ ('Label', label), From 21174b57cb7b8f636a4ec80150030ced8823ed8f Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 28 Apr 2016 11:39:51 -0700 Subject: [PATCH 05/11] Allow setting image clims in Qt options editor. Currently, `(cmin, cmax)` is swapped if `cmin > cmax` because it's not easy to impose the constraint using formlayout. --- lib/matplotlib/backends/qt_editor/figureoptions.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index b83e6a9b394c..344db5d9abb3 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -139,10 +139,12 @@ def prepare_data(d, init): cmap = image.get_cmap() if cmap not in cm.cmap_d.values(): cmaps = [(cmap, cmap.name)] + cmaps + low, high = image.get_clim() imagedata = [ ('Label', label), - ('Colormap', [cmap.name] + cmaps) - ] + ('Colormap', [cmap.name] + cmaps), + ('Min. value', low), + ('Max. value', high)] images.append([imagedata, label, ""]) # Is there an image displayed? has_image = bool(images) @@ -203,9 +205,10 @@ def apply_callback(data): # Set / Images for index, image_settings in enumerate(images): image = imagedict[imagelabels[index]] - label, cmap = image_settings + label, cmap, low, high = image_settings image.set_label(label) image.set_cmap(cm.get_cmap(cmap)) + image.set_clim(*sorted([low, high])) # re-generate legend, if checkbox is checked if generate_legend: From 6d9b4a5ae7685ac244f799f9391c0403227d63e9 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 4 May 2016 17:37:46 -0700 Subject: [PATCH 06/11] Sort default labels numerically in Qt editor. --- lib/matplotlib/backends/qt_editor/figureoptions.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 344db5d9abb3..fb146418fd5f 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -13,6 +13,7 @@ import six import os.path as osp +import re import matplotlib.backends.qt_editor.formlayout as formlayout from matplotlib.backends.qt_compat import QtGui @@ -67,6 +68,14 @@ def figure_edit(axes, parent=None): xunits = axes.xaxis.get_units() yunits = axes.yaxis.get_units() + # Sorting for default labels (_lineXXX, _imageXXX). + def cmp_key(label): + match = re.match(r"(_line|_image)(\d+)", label) + if match: + return match.group(1), int(match.group(2)) + else: + return label, 0 + # Get / Curves linedict = {} for line in axes.get_lines(): @@ -100,7 +109,7 @@ def prepare_data(d, init): sorted(short2name.items(), key=lambda short_and_name: short_and_name[1])) - curvelabels = sorted(linedict.keys()) + curvelabels = sorted(linedict, key=cmp_key) for label in curvelabels: line = linedict[label] color = rgb2hex(colorConverter.to_rgb(line.get_color())) @@ -131,7 +140,7 @@ def prepare_data(d, init): if label == '_nolegend_': continue imagedict[label] = image - imagelabels = sorted(imagedict) + imagelabels = sorted(imagedict, key=cmp_key) images = [] cmaps = [(cmap, name) for name, cmap in sorted(cm.cmap_d.items())] for label in imagelabels: From 5d179baf3f454a152687a86188af84fc332dd16c Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 5 May 2016 11:23:19 -0700 Subject: [PATCH 07/11] DOC: add whats_new for qt configuration editor. --- doc/users/whats_new/2016-04_qt_config-AL.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 doc/users/whats_new/2016-04_qt_config-AL.rst diff --git a/doc/users/whats_new/2016-04_qt_config-AL.rst b/doc/users/whats_new/2016-04_qt_config-AL.rst new file mode 100644 index 000000000000..434ec43c7cda --- /dev/null +++ b/doc/users/whats_new/2016-04_qt_config-AL.rst @@ -0,0 +1,14 @@ +Improvements for the Qt figure options editor +--------------------------------------------- + +Various usability improvements were implemented for the Qt figure options +editor, among which: +- Line style entries are now sorted without duplicates. +- The colormap and normalization limits can now be set for images. +- Line edits for floating values now display only as many digits as necessary + to avoid precision loss. An important bug was also fixed regarding input + validation using Qt5 and a locale where the decimal separator is ",". +- The axes selector now uses shorter, more user-friendly names for axes, and + does not crash if there are no axes. +- Line and image entries using the default labels ("_lineX", "_imageX") are now + sorted numerically even when there are more than 10 entries. From 46dd6891ae326256dfee10267beeaa9240ecf911 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 4 May 2016 17:52:34 -0700 Subject: [PATCH 08/11] Minor cleanups for FormLayout. --- .../backends/qt_editor/formlayout.py | 62 +++++++++---------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/formlayout.py b/lib/matplotlib/backends/qt_editor/formlayout.py index a786a6105342..34395a5f1e33 100644 --- a/lib/matplotlib/backends/qt_editor/formlayout.py +++ b/lib/matplotlib/backends/qt_editor/formlayout.py @@ -32,43 +32,33 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from __future__ import (absolute_import, division, print_function, - unicode_literals) - -import six -from six.moves import xrange # History: # 1.0.10: added float validator (disable "Ok" and "Apply" button when not valid) # 1.0.7: added support for "Apply" button # 1.0.6: code cleaning +from __future__ import (absolute_import, division, print_function, + unicode_literals) + __version__ = '1.0.10' __license__ = __doc__ DEBUG = False -import sys -STDERR = sys.stderr +import six -from matplotlib.colors import is_color_like -from matplotlib.colors import rgb2hex -from matplotlib.colors import colorConverter +import copy +import datetime +import warnings +from matplotlib.colors import colorConverter, is_color_like, rgb2hex from matplotlib.backends.qt_compat import QtGui, QtWidgets, QtCore -if not hasattr(QtWidgets, 'QFormLayout'): - raise ImportError("Warning: formlayout requires PyQt4 >v4.3 or PySide") -import datetime BLACKLIST = set(["title", "label"]) -def col2hex(color): - """Convert matplotlib color to hex before passing to Qt""" - return rgb2hex(colorConverter.to_rgb(color)) - - class ColorButton(QtWidgets.QPushButton): """ Color choosing push button @@ -83,7 +73,8 @@ def __init__(self, parent=None): self._color = QtGui.QColor() def choose_color(self): - color = QtWidgets.QColorDialog.getColor(self._color, self.parentWidget(), '') + color = QtWidgets.QColorDialog.getColor( + self._color, self.parentWidget(), '') if color.isValid(): self.set_color(color) @@ -101,10 +92,12 @@ def set_color(self, color): color = QtCore.Property(QtGui.QColor, get_color, set_color) + def col2hex(color): """Convert matplotlib color to hex before passing to Qt""" return rgb2hex(colorConverter.to_rgb(color)) + def to_qcolor(color): """Create a QColor from a matplotlib color""" qcolor = QtGui.QColor() @@ -112,7 +105,7 @@ def to_qcolor(color): try: color = col2hex(color) except ValueError: - #print('WARNING: ignoring invalid color %r' % color) + warnings.warn('Ignoring invalid color %r' % color) return qcolor # return invalid QColor qcolor.setNamedColor(color) # set using hex color return qcolor # return valid QColor @@ -146,7 +139,7 @@ def text(self): def font_is_installed(font): """Check if font is installed""" return [fam for fam in QtGui.QFontDatabase().families() - if six.text_type(fam) == font] + if six.text_type(fam) == font] def tuple_to_qfont(tup): @@ -154,11 +147,11 @@ def tuple_to_qfont(tup): Create a QFont from tuple: (family [string], size [int], italic [bool], bold [bool]) """ - if not isinstance(tup, tuple) or len(tup) != 4 \ - or not font_is_installed(tup[0]) \ - or not isinstance(tup[1], int) \ - or not isinstance(tup[2], bool) \ - or not isinstance(tup[3], bool): + if not (isinstance(tup, tuple) and len(tup) == 4 + and font_is_installed(tup[0]) + and isinstance(tup[1], int) + and isinstance(tup[2], bool) + and isinstance(tup[3], bool)): return None font = QtGui.QFont() family, size, italic, bold = tup @@ -189,7 +182,7 @@ def __init__(self, value, parent=None): # Font size self.size = QtWidgets.QComboBox(parent) self.size.setEditable(True) - sizelist = list(xrange(6, 12)) + list(xrange(12, 30, 2)) + [36, 48, 72] + sizelist = list(range(6, 12)) + list(range(12, 30, 2)) + [36, 48, 72] size = font.pointSize() if size not in sizelist: sizelist.append(size) @@ -227,8 +220,7 @@ class FormWidget(QtWidgets.QWidget): update_buttons = QtCore.Signal() def __init__(self, data, comment="", parent=None): QtWidgets.QWidget.__init__(self, parent) - from copy import deepcopy - self.data = deepcopy(data) + self.data = copy.deepcopy(data) self.widgets = [] self.formlayout = QtWidgets.QFormLayout(self) if comment: @@ -284,8 +276,9 @@ def setup(self): elif selindex in keys: selindex = keys.index(selindex) elif not isinstance(selindex, int): - print("Warning: '%s' index is invalid (label: " - "%s, value: %s)" % (selindex, label, value), file=STDERR) + warnings.warn( + "index '%s' is invalid (label: %s, value: %s)" % + (selindex, label, value)) selindex = 0 field.setCurrentIndex(selindex) elif isinstance(value, bool): @@ -431,8 +424,8 @@ def __init__(self, data, title="", comment="", self.formwidget.setup() # Button box - self.bbox = bbox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok - | QtWidgets.QDialogButtonBox.Cancel) + self.bbox = bbox = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) self.formwidget.update_buttons.connect(self.update_buttons) if self.apply_callback is not None: apply_btn = bbox.addButton(QtWidgets.QDialogButtonBox.Apply) @@ -457,7 +450,8 @@ def update_buttons(self): for field in self.float_fields: if not is_edit_valid(field): valid = False - for btn_type in (QtWidgets.QDialogButtonBox.Ok, QtWidgets.QDialogButtonBox.Apply): + for btn_type in (QtWidgets.QDialogButtonBox.Ok, + QtWidgets.QDialogButtonBox.Apply): btn = self.bbox.button(btn_type) if btn is not None: btn.setEnabled(valid) From c077c565dade84378196da5cfb5d0a050164f9dc Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 4 May 2016 19:11:29 -0700 Subject: [PATCH 09/11] Qt editor alpha handling. --- .../backends/qt_editor/figureoptions.py | 28 +++++++++------- .../backends/qt_editor/formlayout.py | 32 +++++++++---------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index fb146418fd5f..038073edd6a1 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -15,17 +15,17 @@ import os.path as osp import re +import matplotlib +from matplotlib import cm, markers, colors as mcolors import matplotlib.backends.qt_editor.formlayout as formlayout from matplotlib.backends.qt_compat import QtGui -from matplotlib import cm, markers -from matplotlib.colors import colorConverter, rgb2hex def get_icon(name): - import matplotlib basedir = osp.join(matplotlib.rcParams['datapath'], 'images') return QtGui.QIcon(osp.join(basedir, name)) + LINESTYLES = {'-': 'Solid', '--': 'Dashed', '-.': 'DashDot', @@ -112,23 +112,25 @@ def prepare_data(d, init): curvelabels = sorted(linedict, key=cmp_key) for label in curvelabels: line = linedict[label] - color = rgb2hex(colorConverter.to_rgb(line.get_color())) - ec = rgb2hex(colorConverter.to_rgb(line.get_markeredgecolor())) - fc = rgb2hex(colorConverter.to_rgb(line.get_markerfacecolor())) + color = mcolors.to_hex( + mcolors.to_rgba(line.get_color(), line.get_alpha()), + keep_alpha=True) + ec = mcolors.to_hex(line.get_markeredgecolor(), keep_alpha=True) + fc = mcolors.to_hex(line.get_markerfacecolor(), keep_alpha=True) curvedata = [ ('Label', label), sep, (None, 'Line'), - ('Line Style', prepare_data(LINESTYLES, line.get_linestyle())), - ('Draw Style', prepare_data(DRAWSTYLES, line.get_drawstyle())), + ('Line style', prepare_data(LINESTYLES, line.get_linestyle())), + ('Draw style', prepare_data(DRAWSTYLES, line.get_drawstyle())), ('Width', line.get_linewidth()), - ('Color', color), + ('Color (RGBA)', color), sep, (None, 'Marker'), ('Style', prepare_data(MARKERS, line.get_marker())), ('Size', line.get_markersize()), - ('Facecolor', fc), - ('Edgecolor', ec)] + ('Face color (RGBA)', fc), + ('Edge color (RGBA)', ec)] curves.append([curvedata, label, ""]) # Is there a curve displayed? has_curve = bool(curves) @@ -204,7 +206,9 @@ def apply_callback(data): line.set_linestyle(linestyle) line.set_drawstyle(drawstyle) line.set_linewidth(linewidth) - line.set_color(color) + rgba = mcolors.to_rgba(color) + line.set_color(rgba[:3]) + line.set_alpha(rgba[-1]) if marker is not 'none': line.set_marker(marker) line.set_markersize(markersize) diff --git a/lib/matplotlib/backends/qt_editor/formlayout.py b/lib/matplotlib/backends/qt_editor/formlayout.py index 34395a5f1e33..00a1a03a36ee 100644 --- a/lib/matplotlib/backends/qt_editor/formlayout.py +++ b/lib/matplotlib/backends/qt_editor/formlayout.py @@ -46,13 +46,13 @@ DEBUG = False -import six - import copy import datetime import warnings -from matplotlib.colors import colorConverter, is_color_like, rgb2hex +import six + +from matplotlib import colors as mcolors from matplotlib.backends.qt_compat import QtGui, QtWidgets, QtCore @@ -74,7 +74,8 @@ def __init__(self, parent=None): def choose_color(self): color = QtWidgets.QColorDialog.getColor( - self._color, self.parentWidget(), '') + self._color, self.parentWidget(), "", + QtWidgets.QColorDialog.ShowAlphaChannel) if color.isValid(): self.set_color(color) @@ -93,22 +94,16 @@ def set_color(self, color): color = QtCore.Property(QtGui.QColor, get_color, set_color) -def col2hex(color): - """Convert matplotlib color to hex before passing to Qt""" - return rgb2hex(colorConverter.to_rgb(color)) - - def to_qcolor(color): """Create a QColor from a matplotlib color""" qcolor = QtGui.QColor() - color = str(color) try: - color = col2hex(color) + rgba = mcolors.to_rgba(color) except ValueError: warnings.warn('Ignoring invalid color %r' % color) return qcolor # return invalid QColor - qcolor.setNamedColor(color) # set using hex color - return qcolor # return valid QColor + qcolor.setRgbF(*rgba) + return qcolor class ColorLayout(QtWidgets.QHBoxLayout): @@ -116,7 +111,8 @@ class ColorLayout(QtWidgets.QHBoxLayout): def __init__(self, color, parent=None): QtWidgets.QHBoxLayout.__init__(self) assert isinstance(color, QtGui.QColor) - self.lineedit = QtWidgets.QLineEdit(color.name(), parent) + self.lineedit = QtWidgets.QLineEdit( + mcolors.to_hex(color.getRgbF(), keep_alpha=True), parent) self.lineedit.editingFinished.connect(self.update_color) self.addWidget(self.lineedit) self.colorbtn = ColorButton(parent) @@ -130,7 +126,7 @@ def update_color(self): self.colorbtn.color = qcolor # defaults to black if not qcolor.isValid() def update_text(self, color): - self.lineedit.setText(color.name()) + self.lineedit.setText(mcolors.to_hex(color.getRgbF(), keep_alpha=True)) def text(self): return self.lineedit.text() @@ -256,7 +252,8 @@ def setup(self): continue elif tuple_to_qfont(value) is not None: field = FontLayout(value, self) - elif label.lower() not in BLACKLIST and is_color_like(value): + elif (label.lower() not in BLACKLIST + and mcolors.is_color_like(value)): field = ColorLayout(to_qcolor(value), self) elif isinstance(value, six.string_types): field = QtWidgets.QLineEdit(value, self) @@ -319,7 +316,8 @@ def get(self): continue elif tuple_to_qfont(value) is not None: value = field.get_font() - elif isinstance(value, six.string_types) or is_color_like(value): + elif (isinstance(value, six.string_types) + or mcolors.is_color_like(value)): value = six.text_type(field.text()) elif isinstance(value, (list, tuple)): index = int(field.currentIndex()) From 61a16e4e755c420868fe9f2bb43b3cd0d228ddd3 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 17 Jun 2016 01:35:37 -0700 Subject: [PATCH 10/11] Drop conditional import of figureoptions. As far as I can tell, the only way the import of figureoptions could fail is under PyQt 4.3 or earlier (QFormLayout was introduced in Qt 4.4). PyQt 4.4 was released in 2008 and the oldest version still downloadable on SourceForge seems to be 4.9, so that seems safe... Also up the version requirement on PyQt. --- INSTALL | 2 +- doc/conf.py | 3 ++ lib/matplotlib/backends/backend_qt4.py | 5 -- lib/matplotlib/backends/backend_qt5.py | 67 ++++++++++---------------- 4 files changed, 30 insertions(+), 47 deletions(-) diff --git a/INSTALL b/INSTALL index 279c50a94f3d..771dd6acc7ce 100644 --- a/INSTALL +++ b/INSTALL @@ -233,7 +233,7 @@ backends and the capabilities they provide. Versions 8.6.0 and 8.6.1 are known to have issues that may result in segfaults when closing multiple windows in the wrong order. -:term:`pyqt` 4.0 or later +:term:`pyqt` 4.4 or later The Qt4 widgets library python wrappers for the Qt4Agg backend :term:`pygtk` 2.4 or later diff --git a/doc/conf.py b/doc/conf.py index 54273ca5bc6e..976ca29c38f8 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -318,6 +318,9 @@ class QWidget(object): class QMainWindow(object): pass + class QPushButton(object): + pass + class MySip(MagicMock): def getapi(*args): diff --git a/lib/matplotlib/backends/backend_qt4.py b/lib/matplotlib/backends/backend_qt4.py index d0facc419b56..0a8e68440178 100644 --- a/lib/matplotlib/backends/backend_qt4.py +++ b/lib/matplotlib/backends/backend_qt4.py @@ -22,12 +22,7 @@ from matplotlib._pylab_helpers import Gcf from matplotlib.figure import Figure - from matplotlib.widgets import SubplotTool -try: - import matplotlib.backends.qt_editor.figureoptions as figureoptions -except ImportError: - figureoptions = None from .qt_compat import QtCore, QtWidgets, _getSaveFileName, __version__ from matplotlib.backends.qt_editor.formsubplottool import UiSubplotTool diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 89e39a1dbd3e..c593bccb28ff 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -23,10 +23,7 @@ from matplotlib.figure import Figure from matplotlib.widgets import SubplotTool -try: - import matplotlib.backends.qt_editor.figureoptions as figureoptions -except ImportError: - figureoptions = None +import matplotlib.backends.qt_editor.figureoptions as figureoptions from .qt_compat import (QtCore, QtGui, QtWidgets, _getSaveFileName, __version__, is_pyqt5) @@ -589,7 +586,7 @@ def _init_toolbar(self): a.setCheckable(True) if tooltip_text is not None: a.setToolTip(tooltip_text) - if figureoptions is not None and text == 'Subplots': + if text == 'Subplots': a = self.addAction(self._icon("qt4_editor_options.png"), 'Customize', self.edit_parameters) a.setToolTip('Edit axis, curve and image parameters') @@ -620,43 +617,31 @@ def _init_toolbar(self): self.layout().setSpacing(12) self.setMinimumHeight(48) - if figureoptions is not None: - def edit_parameters(self): - allaxes = self.canvas.figure.get_axes() - if not allaxes: - QtWidgets.QMessageBox.warning( - self.parent, "Error", "There are no axes to edit.") - return - if len(allaxes) == 1: - axes = allaxes[0] + def edit_parameters(self): + allaxes = self.canvas.figure.get_axes() + if not allaxes: + QtWidgets.QMessageBox.warning( + self.parent, "Error", "There are no axes to edit.") + return + if len(allaxes) == 1: + axes = allaxes[0] + else: + titles = [] + for axes in allaxes: + name = (axes.get_title() or + " - ".join(filter(None, [axes.get_xlabel(), + axes.get_ylabel()])) or + "".format( + type(axes).__name__, id(axes))) + titles.append(name) + item, ok = QtWidgets.QInputDialog.getItem( + self.parent, 'Customize', 'Select axes:', titles, 0, False) + if ok: + axes = allaxes[titles.index(six.text_type(item))] else: - titles = [] - for axes in allaxes: - title = axes.get_title() - ylabel = axes.get_ylabel() - label = axes.get_label() - if title: - fmt = "%(title)s" - if ylabel: - fmt += ": %(ylabel)s" - fmt += " (%(axes_repr)s)" - elif ylabel: - fmt = "%(axes_repr)s (%(ylabel)s)" - elif label: - fmt = "%(axes_repr)s (%(label)s)" - else: - fmt = "%(axes_repr)s" - titles.append(fmt % dict(title=title, - ylabel=ylabel, label=label, - axes_repr=repr(axes))) - item, ok = QtWidgets.QInputDialog.getItem( - self.parent, 'Customize', 'Select axes:', titles, 0, False) - if ok: - axes = allaxes[titles.index(six.text_type(item))] - else: - return - - figureoptions.figure_edit(axes, self) + return + + figureoptions.figure_edit(axes, self) def _update_buttons_checked(self): # sync button checkstates to match active mode From 90aeeeb777457ccc4957fdc3723d7df446ea1c89 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 17 Jun 2016 12:32:47 -0700 Subject: [PATCH 11/11] Mock the entire PyQt4.QtGui classes API. --- doc/conf.py | 107 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 93 insertions(+), 14 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 976ca29c38f8..b5cc8847a0fc 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -306,20 +306,99 @@ class Frame(object): class MyPyQt4(MagicMock): class QtGui(object): - class QToolBar(object): - pass - - class QDialog(object): - pass - - class QWidget(object): - pass - - class QMainWindow(object): - pass - - class QPushButton(object): - pass + # PyQt4.QtGui public classes. + # Generated with + # textwrap.fill([name for name in dir(PyQt4.QtGui) + # if isinstance(getattr(PyQt4.QtGui, name), type)]) + _QtGui_public_classes = """\ + Display QAbstractButton QAbstractGraphicsShapeItem + QAbstractItemDelegate QAbstractItemView QAbstractPrintDialog + QAbstractProxyModel QAbstractScrollArea QAbstractSlider + QAbstractSpinBox QAbstractTextDocumentLayout QAction QActionEvent + QActionGroup QApplication QBitmap QBoxLayout QBrush QButtonGroup + QCalendarWidget QCheckBox QClipboard QCloseEvent QColor QColorDialog + QColumnView QComboBox QCommandLinkButton QCommonStyle QCompleter + QConicalGradient QContextMenuEvent QCursor QDataWidgetMapper QDateEdit + QDateTimeEdit QDesktopServices QDesktopWidget QDial QDialog + QDialogButtonBox QDirModel QDockWidget QDoubleSpinBox QDoubleValidator + QDrag QDragEnterEvent QDragLeaveEvent QDragMoveEvent QDropEvent + QErrorMessage QFileDialog QFileIconProvider QFileOpenEvent + QFileSystemModel QFocusEvent QFocusFrame QFont QFontComboBox + QFontDatabase QFontDialog QFontInfo QFontMetrics QFontMetricsF + QFormLayout QFrame QGesture QGestureEvent QGestureRecognizer QGlyphRun + QGradient QGraphicsAnchor QGraphicsAnchorLayout QGraphicsBlurEffect + QGraphicsColorizeEffect QGraphicsDropShadowEffect QGraphicsEffect + QGraphicsEllipseItem QGraphicsGridLayout QGraphicsItem + QGraphicsItemAnimation QGraphicsItemGroup QGraphicsLayout + QGraphicsLayoutItem QGraphicsLineItem QGraphicsLinearLayout + QGraphicsObject QGraphicsOpacityEffect QGraphicsPathItem + QGraphicsPixmapItem QGraphicsPolygonItem QGraphicsProxyWidget + QGraphicsRectItem QGraphicsRotation QGraphicsScale QGraphicsScene + QGraphicsSceneContextMenuEvent QGraphicsSceneDragDropEvent + QGraphicsSceneEvent QGraphicsSceneHelpEvent QGraphicsSceneHoverEvent + QGraphicsSceneMouseEvent QGraphicsSceneMoveEvent + QGraphicsSceneResizeEvent QGraphicsSceneWheelEvent + QGraphicsSimpleTextItem QGraphicsTextItem QGraphicsTransform + QGraphicsView QGraphicsWidget QGridLayout QGroupBox QHBoxLayout + QHeaderView QHelpEvent QHideEvent QHoverEvent QIcon QIconDragEvent + QIconEngine QIconEngineV2 QIdentityProxyModel QImage QImageIOHandler + QImageReader QImageWriter QInputContext QInputContextFactory + QInputDialog QInputEvent QInputMethodEvent QIntValidator QItemDelegate + QItemEditorCreatorBase QItemEditorFactory QItemSelection + QItemSelectionModel QItemSelectionRange QKeyEvent QKeyEventTransition + QKeySequence QLCDNumber QLabel QLayout QLayoutItem QLineEdit + QLinearGradient QListView QListWidget QListWidgetItem QMainWindow + QMatrix QMatrix2x2 QMatrix2x3 QMatrix2x4 QMatrix3x2 QMatrix3x3 + QMatrix3x4 QMatrix4x2 QMatrix4x3 QMatrix4x4 QMdiArea QMdiSubWindow + QMenu QMenuBar QMessageBox QMimeSource QMouseEvent + QMouseEventTransition QMoveEvent QMovie QPageSetupDialog QPaintDevice + QPaintEngine QPaintEngineState QPaintEvent QPainter QPainterPath + QPainterPathStroker QPalette QPanGesture QPen QPicture QPictureIO + QPinchGesture QPixmap QPixmapCache QPlainTextDocumentLayout + QPlainTextEdit QPolygon QPolygonF QPrintDialog QPrintEngine + QPrintPreviewDialog QPrintPreviewWidget QPrinter QPrinterInfo + QProgressBar QProgressDialog QProxyModel QPushButton QPyTextObject + QQuaternion QRadialGradient QRadioButton QRawFont QRegExpValidator + QRegion QResizeEvent QRubberBand QScrollArea QScrollBar + QSessionManager QShortcut QShortcutEvent QShowEvent QSizeGrip + QSizePolicy QSlider QSortFilterProxyModel QSound QSpacerItem QSpinBox + QSplashScreen QSplitter QSplitterHandle QStackedLayout QStackedWidget + QStandardItem QStandardItemModel QStaticText QStatusBar + QStatusTipEvent QStringListModel QStyle QStyleFactory QStyleHintReturn + QStyleHintReturnMask QStyleHintReturnVariant QStyleOption + QStyleOptionButton QStyleOptionComboBox QStyleOptionComplex + QStyleOptionDockWidget QStyleOptionDockWidgetV2 QStyleOptionFocusRect + QStyleOptionFrame QStyleOptionFrameV2 QStyleOptionFrameV3 + QStyleOptionGraphicsItem QStyleOptionGroupBox QStyleOptionHeader + QStyleOptionMenuItem QStyleOptionProgressBar QStyleOptionProgressBarV2 + QStyleOptionRubberBand QStyleOptionSizeGrip QStyleOptionSlider + QStyleOptionSpinBox QStyleOptionTab QStyleOptionTabBarBase + QStyleOptionTabBarBaseV2 QStyleOptionTabV2 QStyleOptionTabV3 + QStyleOptionTabWidgetFrame QStyleOptionTabWidgetFrameV2 + QStyleOptionTitleBar QStyleOptionToolBar QStyleOptionToolBox + QStyleOptionToolBoxV2 QStyleOptionToolButton QStyleOptionViewItem + QStyleOptionViewItemV2 QStyleOptionViewItemV3 QStyleOptionViewItemV4 + QStylePainter QStyledItemDelegate QSwipeGesture QSyntaxHighlighter + QSystemTrayIcon QTabBar QTabWidget QTableView QTableWidget + QTableWidgetItem QTableWidgetSelectionRange QTabletEvent + QTapAndHoldGesture QTapGesture QTextBlock QTextBlockFormat + QTextBlockGroup QTextBlockUserData QTextBrowser QTextCharFormat + QTextCursor QTextDocument QTextDocumentFragment QTextDocumentWriter + QTextEdit QTextFormat QTextFragment QTextFrame QTextFrameFormat + QTextImageFormat QTextInlineObject QTextItem QTextLayout QTextLength + QTextLine QTextList QTextListFormat QTextObject QTextObjectInterface + QTextOption QTextTable QTextTableCell QTextTableCellFormat + QTextTableFormat QTimeEdit QToolBar QToolBox QToolButton QToolTip + QTouchEvent QTransform QTreeView QTreeWidget QTreeWidgetItem + QTreeWidgetItemIterator QUndoCommand QUndoGroup QUndoStack QUndoView + QVBoxLayout QValidator QVector2D QVector3D QVector4D QWhatsThis + QWhatsThisClickedEvent QWheelEvent QWidget QWidgetAction QWidgetItem + QWindowStateChangeEvent QWizard QWizardPage QWorkspace + QX11EmbedContainer QX11EmbedWidget QX11Info + """ + for _name in _QtGui_public_classes.split(): + locals()[_name] = type(_name, (), {}) + del _name class MySip(MagicMock):