diff --git a/.flake8 b/.flake8
index 39e197ee77c2..76c3063f41c2 100644
--- a/.flake8
+++ b/.flake8
@@ -35,7 +35,7 @@ per-file-ignores =
matplotlib/backends/backend_cairo.py: E203, E221, E402
matplotlib/backends/backend_gtk3.py: E203, E221, E222, E225, E251, E501
matplotlib/backends/backend_pgf.py: E731
- matplotlib/backends/qt_editor/formlayout.py: E501
+ matplotlib/backends/qt_editor/_formlayout.py: E501
matplotlib/font_manager.py: E203, E221, E251, E501
matplotlib/fontconfig_pattern.py: E201, E203, E221, E222, E225
matplotlib/mathtext.py: E201, E202, E203, E211, E221, E222, E225, E251, E301, E402
diff --git a/doc/api/next_api_changes/2018-11-26-AL.rst b/doc/api/next_api_changes/2018-11-26-AL.rst
new file mode 100644
index 000000000000..a4a46d3b4878
--- /dev/null
+++ b/doc/api/next_api_changes/2018-11-26-AL.rst
@@ -0,0 +1,7 @@
+Public use of the matplotlib.backends.qt_editor.formlayout module is deprecated
+```````````````````````````````````````````````````````````````````````````````
+
+This module is a vendored, modified version of the official formlayout_ module
+available on PyPI. Install that module separately if you need it.
+
+.. _formlayout: https://pypi.org/project/formlayout/
diff --git a/lib/matplotlib/backends/qt_editor/_formlayout.py b/lib/matplotlib/backends/qt_editor/_formlayout.py
new file mode 100644
index 000000000000..3c0c96bf68cc
--- /dev/null
+++ b/lib/matplotlib/backends/qt_editor/_formlayout.py
@@ -0,0 +1,547 @@
+"""
+formlayout
+==========
+
+Module creating Qt form dialogs/layouts to edit various type of parameters
+
+
+formlayout License Agreement (MIT License)
+------------------------------------------
+
+Copyright (c) 2009 Pierre Raybaut
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+"""
+
+# 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
+
+__version__ = '1.0.10'
+__license__ = __doc__
+
+import copy
+import datetime
+import logging
+from numbers import Integral, Real
+
+from matplotlib import cbook, colors as mcolors
+from matplotlib.backends.qt_compat import QtGui, QtWidgets, QtCore
+
+_log = logging.getLogger(__name__)
+
+BLACKLIST = {"title", "label"}
+
+
+class ColorButton(QtWidgets.QPushButton):
+ """
+ Color choosing push button
+ """
+ colorChanged = QtCore.Signal(QtGui.QColor)
+
+ def __init__(self, parent=None):
+ QtWidgets.QPushButton.__init__(self, parent)
+ self.setFixedSize(20, 20)
+ self.setIconSize(QtCore.QSize(12, 12))
+ self.clicked.connect(self.choose_color)
+ self._color = QtGui.QColor()
+
+ def choose_color(self):
+ color = QtWidgets.QColorDialog.getColor(
+ self._color, self.parentWidget(), "",
+ QtWidgets.QColorDialog.ShowAlphaChannel)
+ if color.isValid():
+ self.set_color(color)
+
+ def get_color(self):
+ return self._color
+
+ @QtCore.Slot(QtGui.QColor)
+ def set_color(self, color):
+ if color != self._color:
+ self._color = color
+ self.colorChanged.emit(self._color)
+ pixmap = QtGui.QPixmap(self.iconSize())
+ pixmap.fill(color)
+ self.setIcon(QtGui.QIcon(pixmap))
+
+ color = QtCore.Property(QtGui.QColor, get_color, set_color)
+
+
+def to_qcolor(color):
+ """Create a QColor from a matplotlib color"""
+ qcolor = QtGui.QColor()
+ try:
+ rgba = mcolors.to_rgba(color)
+ except ValueError:
+ cbook._warn_external('Ignoring invalid color %r' % color)
+ return qcolor # return invalid QColor
+ qcolor.setRgbF(*rgba)
+ return qcolor
+
+
+class ColorLayout(QtWidgets.QHBoxLayout):
+ """Color-specialized QLineEdit layout"""
+ def __init__(self, color, parent=None):
+ QtWidgets.QHBoxLayout.__init__(self)
+ assert isinstance(color, QtGui.QColor)
+ 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)
+ self.colorbtn.color = color
+ self.colorbtn.colorChanged.connect(self.update_text)
+ self.addWidget(self.colorbtn)
+
+ def update_color(self):
+ color = self.text()
+ qcolor = to_qcolor(color)
+ self.colorbtn.color = qcolor # defaults to black if not qcolor.isValid()
+
+ def update_text(self, color):
+ self.lineedit.setText(mcolors.to_hex(color.getRgbF(), keep_alpha=True))
+
+ def text(self):
+ return self.lineedit.text()
+
+
+def font_is_installed(font):
+ """Check if font is installed"""
+ return [fam for fam in QtGui.QFontDatabase().families()
+ if str(fam) == font]
+
+
+def tuple_to_qfont(tup):
+ """
+ Create a QFont from tuple:
+ (family [string], size [int], italic [bool], bold [bool])
+ """
+ if not (isinstance(tup, tuple) and len(tup) == 4
+ and font_is_installed(tup[0])
+ and isinstance(tup[1], Integral)
+ and isinstance(tup[2], bool)
+ and isinstance(tup[3], bool)):
+ return None
+ font = QtGui.QFont()
+ family, size, italic, bold = tup
+ font.setFamily(family)
+ font.setPointSize(size)
+ font.setItalic(italic)
+ font.setBold(bold)
+ return font
+
+
+def qfont_to_tuple(font):
+ return (str(font.family()), int(font.pointSize()),
+ font.italic(), font.bold())
+
+
+class FontLayout(QtWidgets.QGridLayout):
+ """Font selection"""
+ def __init__(self, value, parent=None):
+ QtWidgets.QGridLayout.__init__(self)
+ font = tuple_to_qfont(value)
+ assert font is not None
+
+ # Font family
+ self.family = QtWidgets.QFontComboBox(parent)
+ self.family.setCurrentFont(font)
+ self.addWidget(self.family, 0, 0, 1, -1)
+
+ # Font size
+ self.size = QtWidgets.QComboBox(parent)
+ self.size.setEditable(True)
+ sizelist = [*range(6, 12), *range(12, 30, 2), 36, 48, 72]
+ size = font.pointSize()
+ if size not in sizelist:
+ sizelist.append(size)
+ sizelist.sort()
+ self.size.addItems([str(s) for s in sizelist])
+ self.size.setCurrentIndex(sizelist.index(size))
+ self.addWidget(self.size, 1, 0)
+
+ # Italic or not
+ self.italic = QtWidgets.QCheckBox(self.tr("Italic"), parent)
+ self.italic.setChecked(font.italic())
+ self.addWidget(self.italic, 1, 1)
+
+ # Bold or not
+ self.bold = QtWidgets.QCheckBox(self.tr("Bold"), parent)
+ self.bold.setChecked(font.bold())
+ self.addWidget(self.bold, 1, 2)
+
+ def get_font(self):
+ font = self.family.currentFont()
+ font.setItalic(self.italic.isChecked())
+ font.setBold(self.bold.isChecked())
+ font.setPointSize(int(self.size.currentText()))
+ return qfont_to_tuple(font)
+
+
+def is_edit_valid(edit):
+ text = edit.text()
+ state = edit.validator().validate(text, 0)[0]
+
+ return state == QtGui.QDoubleValidator.Acceptable
+
+
+class FormWidget(QtWidgets.QWidget):
+ update_buttons = QtCore.Signal()
+
+ def __init__(self, data, comment="", parent=None):
+ QtWidgets.QWidget.__init__(self, parent)
+ self.data = copy.deepcopy(data)
+ self.widgets = []
+ self.formlayout = QtWidgets.QFormLayout(self)
+ if comment:
+ self.formlayout.addRow(QtWidgets.QLabel(comment))
+ self.formlayout.addRow(QtWidgets.QLabel(" "))
+
+ def get_dialog(self):
+ """Return FormDialog instance"""
+ dialog = self.parent()
+ while not isinstance(dialog, QtWidgets.QDialog):
+ dialog = dialog.parent()
+ return dialog
+
+ def setup(self):
+ for label, value in self.data:
+ if label is None and value is None:
+ # Separator: (None, None)
+ self.formlayout.addRow(QtWidgets.QLabel(" "),
+ QtWidgets.QLabel(" "))
+ self.widgets.append(None)
+ continue
+ elif label is None:
+ # Comment
+ self.formlayout.addRow(QtWidgets.QLabel(value))
+ self.widgets.append(None)
+ continue
+ elif tuple_to_qfont(value) is not None:
+ field = FontLayout(value, self)
+ elif (label.lower() not in BLACKLIST
+ and mcolors.is_color_like(value)):
+ field = ColorLayout(to_qcolor(value), self)
+ elif isinstance(value, str):
+ field = QtWidgets.QLineEdit(value, self)
+ elif isinstance(value, (list, tuple)):
+ if isinstance(value, tuple):
+ value = list(value)
+ # Note: get() below checks the type of value[0] in self.data so
+ # it is essential that value gets modified in-place.
+ # This means that the code is actually broken in the case where
+ # value is a tuple, but fortunately we always pass a list...
+ selindex = value.pop(0)
+ field = QtWidgets.QComboBox(self)
+ if isinstance(value[0], (list, tuple)):
+ keys = [key for key, _val in value]
+ value = [val for _key, val in value]
+ else:
+ keys = value
+ field.addItems(value)
+ if selindex in value:
+ selindex = value.index(selindex)
+ elif selindex in keys:
+ selindex = keys.index(selindex)
+ elif not isinstance(selindex, Integral):
+ _log.warning(
+ "index '%s' is invalid (label: %s, value: %s)",
+ selindex, label, value)
+ selindex = 0
+ field.setCurrentIndex(selindex)
+ elif isinstance(value, bool):
+ field = QtWidgets.QCheckBox(self)
+ if value:
+ field.setCheckState(QtCore.Qt.Checked)
+ else:
+ field.setCheckState(QtCore.Qt.Unchecked)
+ elif isinstance(value, Integral):
+ field = QtWidgets.QSpinBox(self)
+ field.setRange(-1e9, 1e9)
+ field.setValue(value)
+ elif isinstance(value, Real):
+ field = QtWidgets.QLineEdit(repr(value), self)
+ field.setCursorPosition(0)
+ field.setValidator(QtGui.QDoubleValidator(field))
+ field.validator().setLocale(QtCore.QLocale("C"))
+ dialog = self.get_dialog()
+ dialog.register_float_field(field)
+ field.textChanged.connect(lambda text: dialog.update_buttons())
+ elif isinstance(value, datetime.datetime):
+ field = QtWidgets.QDateTimeEdit(self)
+ field.setDateTime(value)
+ elif isinstance(value, datetime.date):
+ field = QtWidgets.QDateEdit(self)
+ field.setDate(value)
+ else:
+ field = QtWidgets.QLineEdit(repr(value), self)
+ self.formlayout.addRow(label, field)
+ self.widgets.append(field)
+
+ def get(self):
+ valuelist = []
+ for index, (label, value) in enumerate(self.data):
+ field = self.widgets[index]
+ if label is None:
+ # Separator / Comment
+ continue
+ elif tuple_to_qfont(value) is not None:
+ value = field.get_font()
+ elif isinstance(value, str) or mcolors.is_color_like(value):
+ value = str(field.text())
+ elif isinstance(value, (list, tuple)):
+ index = int(field.currentIndex())
+ if isinstance(value[0], (list, tuple)):
+ value = value[index][0]
+ else:
+ value = value[index]
+ elif isinstance(value, bool):
+ value = field.checkState() == QtCore.Qt.Checked
+ elif isinstance(value, Integral):
+ value = int(field.value())
+ elif isinstance(value, Real):
+ value = float(str(field.text()))
+ elif isinstance(value, datetime.datetime):
+ value = field.dateTime().toPyDateTime()
+ elif isinstance(value, datetime.date):
+ value = field.date().toPyDate()
+ else:
+ value = eval(str(field.text()))
+ valuelist.append(value)
+ return valuelist
+
+
+class FormComboWidget(QtWidgets.QWidget):
+ update_buttons = QtCore.Signal()
+
+ def __init__(self, datalist, comment="", parent=None):
+ QtWidgets.QWidget.__init__(self, parent)
+ layout = QtWidgets.QVBoxLayout()
+ self.setLayout(layout)
+ self.combobox = QtWidgets.QComboBox()
+ layout.addWidget(self.combobox)
+
+ self.stackwidget = QtWidgets.QStackedWidget(self)
+ layout.addWidget(self.stackwidget)
+ self.combobox.currentIndexChanged.connect(
+ self.stackwidget.setCurrentIndex)
+
+ self.widgetlist = []
+ for data, title, comment in datalist:
+ self.combobox.addItem(title)
+ widget = FormWidget(data, comment=comment, parent=self)
+ self.stackwidget.addWidget(widget)
+ self.widgetlist.append(widget)
+
+ def setup(self):
+ for widget in self.widgetlist:
+ widget.setup()
+
+ def get(self):
+ return [widget.get() for widget in self.widgetlist]
+
+
+class FormTabWidget(QtWidgets.QWidget):
+ update_buttons = QtCore.Signal()
+
+ def __init__(self, datalist, comment="", parent=None):
+ QtWidgets.QWidget.__init__(self, parent)
+ layout = QtWidgets.QVBoxLayout()
+ self.tabwidget = QtWidgets.QTabWidget()
+ layout.addWidget(self.tabwidget)
+ self.setLayout(layout)
+ self.widgetlist = []
+ for data, title, comment in datalist:
+ if len(data[0]) == 3:
+ widget = FormComboWidget(data, comment=comment, parent=self)
+ else:
+ widget = FormWidget(data, comment=comment, parent=self)
+ index = self.tabwidget.addTab(widget, title)
+ self.tabwidget.setTabToolTip(index, comment)
+ self.widgetlist.append(widget)
+
+ def setup(self):
+ for widget in self.widgetlist:
+ widget.setup()
+
+ def get(self):
+ return [widget.get() for widget in self.widgetlist]
+
+
+class FormDialog(QtWidgets.QDialog):
+ """Form Dialog"""
+ def __init__(self, data, title="", comment="",
+ icon=None, parent=None, apply=None):
+ QtWidgets.QDialog.__init__(self, parent)
+
+ self.apply_callback = apply
+
+ # Form
+ if isinstance(data[0][0], (list, tuple)):
+ self.formwidget = FormTabWidget(data, comment=comment,
+ parent=self)
+ elif len(data[0]) == 3:
+ self.formwidget = FormComboWidget(data, comment=comment,
+ parent=self)
+ else:
+ self.formwidget = FormWidget(data, comment=comment,
+ parent=self)
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.formwidget)
+
+ self.float_fields = []
+ self.formwidget.setup()
+
+ # Button box
+ 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)
+ apply_btn.clicked.connect(self.apply)
+
+ bbox.accepted.connect(self.accept)
+ bbox.rejected.connect(self.reject)
+ layout.addWidget(bbox)
+
+ self.setLayout(layout)
+
+ self.setWindowTitle(title)
+ if not isinstance(icon, QtGui.QIcon):
+ icon = QtWidgets.QWidget().style().standardIcon(
+ QtWidgets.QStyle.SP_MessageBoxQuestion)
+ self.setWindowIcon(icon)
+
+ def register_float_field(self, field):
+ self.float_fields.append(field)
+
+ def update_buttons(self):
+ valid = True
+ for field in self.float_fields:
+ if not is_edit_valid(field):
+ valid = False
+ for btn_type in (QtWidgets.QDialogButtonBox.Ok,
+ QtWidgets.QDialogButtonBox.Apply):
+ btn = self.bbox.button(btn_type)
+ if btn is not None:
+ btn.setEnabled(valid)
+
+ def accept(self):
+ self.data = self.formwidget.get()
+ QtWidgets.QDialog.accept(self)
+
+ def reject(self):
+ self.data = None
+ QtWidgets.QDialog.reject(self)
+
+ def apply(self):
+ self.apply_callback(self.formwidget.get())
+
+ def get(self):
+ """Return form result"""
+ return self.data
+
+
+def fedit(data, title="", comment="", icon=None, parent=None, apply=None):
+ """
+ Create form dialog and return result
+ (if Cancel button is pressed, return None)
+
+ data: datalist, datagroup
+ title: string
+ comment: string
+ icon: QIcon instance
+ parent: parent QWidget
+ apply: apply callback (function)
+
+ datalist: list/tuple of (field_name, field_value)
+ datagroup: list/tuple of (datalist *or* datagroup, title, comment)
+
+ -> one field for each member of a datalist
+ -> one tab for each member of a top-level datagroup
+ -> one page (of a multipage widget, each page can be selected with a combo
+ box) for each member of a datagroup inside a datagroup
+
+ Supported types for field_value:
+ - int, float, str, unicode, bool
+ - colors: in Qt-compatible text form, i.e. in hex format or name (red,...)
+ (automatically detected from a string)
+ - list/tuple:
+ * the first element will be the selected index (or value)
+ * the other elements can be couples (key, value) or only values
+ """
+
+ # Create a QApplication instance if no instance currently exists
+ # (e.g., if the module is used directly from the interpreter)
+ if QtWidgets.QApplication.startingUp():
+ _app = QtWidgets.QApplication([])
+ dialog = FormDialog(data, title, comment, icon, parent, apply)
+ if dialog.exec_():
+ return dialog.get()
+
+
+if __name__ == "__main__":
+
+ def create_datalist_example():
+ return [('str', 'this is a string'),
+ ('list', [0, '1', '3', '4']),
+ ('list2', ['--', ('none', 'None'), ('--', 'Dashed'),
+ ('-.', 'DashDot'), ('-', 'Solid'),
+ ('steps', 'Steps'), (':', 'Dotted')]),
+ ('float', 1.2),
+ (None, 'Other:'),
+ ('int', 12),
+ ('font', ('Arial', 10, False, True)),
+ ('color', '#123409'),
+ ('bool', True),
+ ('date', datetime.date(2010, 10, 10)),
+ ('datetime', datetime.datetime(2010, 10, 10)),
+ ]
+
+ def create_datagroup_example():
+ datalist = create_datalist_example()
+ return ((datalist, "Category 1", "Category 1 comment"),
+ (datalist, "Category 2", "Category 2 comment"),
+ (datalist, "Category 3", "Category 3 comment"))
+
+ #--------- datalist example
+ datalist = create_datalist_example()
+
+ def apply_test(data):
+ print("data:", data)
+ print("result:", fedit(datalist, title="Example",
+ comment="This is just an example.",
+ apply=apply_test))
+
+ #--------- datagroup example
+ datagroup = create_datagroup_example()
+ print("result:", fedit(datagroup, "Global title"))
+
+ #--------- datagroup inside a datagroup example
+ datalist = create_datalist_example()
+ datagroup = create_datagroup_example()
+ print("result:", fedit(((datagroup, "Title 1", "Tab 1 comment"),
+ (datalist, "Title 2", "Tab 2 comment"),
+ (datalist, "Title 3", "Tab 3 comment")),
+ "Global title"))
diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py
index 6620f5870920..8178161f9293 100644
--- a/lib/matplotlib/backends/qt_editor/figureoptions.py
+++ b/lib/matplotlib/backends/qt_editor/figureoptions.py
@@ -10,8 +10,8 @@
import matplotlib
from matplotlib import cm, colors as mcolors, markers, image as mimage
-import matplotlib.backends.qt_editor.formlayout as formlayout
from matplotlib.backends.qt_compat import QtGui
+from matplotlib.backends.qt_editor import _formlayout
def get_icon(name):
@@ -250,8 +250,8 @@ def apply_callback(data):
if not (axes.get_xlim() == orig_xlim and axes.get_ylim() == orig_ylim):
figure.canvas.toolbar.push_current()
- data = formlayout.fedit(datalist, title="Figure options", parent=parent,
- icon=get_icon('qt4_editor_options.svg'),
- apply=apply_callback)
+ data = _formlayout.fedit(datalist, title="Figure options", parent=parent,
+ icon=get_icon('qt4_editor_options.svg'),
+ apply=apply_callback)
if data is not None:
apply_callback(data)
diff --git a/lib/matplotlib/backends/qt_editor/formlayout.py b/lib/matplotlib/backends/qt_editor/formlayout.py
index 3c0c96bf68cc..a6c3f75ee167 100644
--- a/lib/matplotlib/backends/qt_editor/formlayout.py
+++ b/lib/matplotlib/backends/qt_editor/formlayout.py
@@ -1,547 +1,7 @@
-"""
-formlayout
-==========
+from .. import cbook
+from ._formlayout import *
-Module creating Qt form dialogs/layouts to edit various type of parameters
-
-formlayout License Agreement (MIT License)
-------------------------------------------
-
-Copyright (c) 2009 Pierre Raybaut
-
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
-"""
-
-# 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
-
-__version__ = '1.0.10'
-__license__ = __doc__
-
-import copy
-import datetime
-import logging
-from numbers import Integral, Real
-
-from matplotlib import cbook, colors as mcolors
-from matplotlib.backends.qt_compat import QtGui, QtWidgets, QtCore
-
-_log = logging.getLogger(__name__)
-
-BLACKLIST = {"title", "label"}
-
-
-class ColorButton(QtWidgets.QPushButton):
- """
- Color choosing push button
- """
- colorChanged = QtCore.Signal(QtGui.QColor)
-
- def __init__(self, parent=None):
- QtWidgets.QPushButton.__init__(self, parent)
- self.setFixedSize(20, 20)
- self.setIconSize(QtCore.QSize(12, 12))
- self.clicked.connect(self.choose_color)
- self._color = QtGui.QColor()
-
- def choose_color(self):
- color = QtWidgets.QColorDialog.getColor(
- self._color, self.parentWidget(), "",
- QtWidgets.QColorDialog.ShowAlphaChannel)
- if color.isValid():
- self.set_color(color)
-
- def get_color(self):
- return self._color
-
- @QtCore.Slot(QtGui.QColor)
- def set_color(self, color):
- if color != self._color:
- self._color = color
- self.colorChanged.emit(self._color)
- pixmap = QtGui.QPixmap(self.iconSize())
- pixmap.fill(color)
- self.setIcon(QtGui.QIcon(pixmap))
-
- color = QtCore.Property(QtGui.QColor, get_color, set_color)
-
-
-def to_qcolor(color):
- """Create a QColor from a matplotlib color"""
- qcolor = QtGui.QColor()
- try:
- rgba = mcolors.to_rgba(color)
- except ValueError:
- cbook._warn_external('Ignoring invalid color %r' % color)
- return qcolor # return invalid QColor
- qcolor.setRgbF(*rgba)
- return qcolor
-
-
-class ColorLayout(QtWidgets.QHBoxLayout):
- """Color-specialized QLineEdit layout"""
- def __init__(self, color, parent=None):
- QtWidgets.QHBoxLayout.__init__(self)
- assert isinstance(color, QtGui.QColor)
- 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)
- self.colorbtn.color = color
- self.colorbtn.colorChanged.connect(self.update_text)
- self.addWidget(self.colorbtn)
-
- def update_color(self):
- color = self.text()
- qcolor = to_qcolor(color)
- self.colorbtn.color = qcolor # defaults to black if not qcolor.isValid()
-
- def update_text(self, color):
- self.lineedit.setText(mcolors.to_hex(color.getRgbF(), keep_alpha=True))
-
- def text(self):
- return self.lineedit.text()
-
-
-def font_is_installed(font):
- """Check if font is installed"""
- return [fam for fam in QtGui.QFontDatabase().families()
- if str(fam) == font]
-
-
-def tuple_to_qfont(tup):
- """
- Create a QFont from tuple:
- (family [string], size [int], italic [bool], bold [bool])
- """
- if not (isinstance(tup, tuple) and len(tup) == 4
- and font_is_installed(tup[0])
- and isinstance(tup[1], Integral)
- and isinstance(tup[2], bool)
- and isinstance(tup[3], bool)):
- return None
- font = QtGui.QFont()
- family, size, italic, bold = tup
- font.setFamily(family)
- font.setPointSize(size)
- font.setItalic(italic)
- font.setBold(bold)
- return font
-
-
-def qfont_to_tuple(font):
- return (str(font.family()), int(font.pointSize()),
- font.italic(), font.bold())
-
-
-class FontLayout(QtWidgets.QGridLayout):
- """Font selection"""
- def __init__(self, value, parent=None):
- QtWidgets.QGridLayout.__init__(self)
- font = tuple_to_qfont(value)
- assert font is not None
-
- # Font family
- self.family = QtWidgets.QFontComboBox(parent)
- self.family.setCurrentFont(font)
- self.addWidget(self.family, 0, 0, 1, -1)
-
- # Font size
- self.size = QtWidgets.QComboBox(parent)
- self.size.setEditable(True)
- sizelist = [*range(6, 12), *range(12, 30, 2), 36, 48, 72]
- size = font.pointSize()
- if size not in sizelist:
- sizelist.append(size)
- sizelist.sort()
- self.size.addItems([str(s) for s in sizelist])
- self.size.setCurrentIndex(sizelist.index(size))
- self.addWidget(self.size, 1, 0)
-
- # Italic or not
- self.italic = QtWidgets.QCheckBox(self.tr("Italic"), parent)
- self.italic.setChecked(font.italic())
- self.addWidget(self.italic, 1, 1)
-
- # Bold or not
- self.bold = QtWidgets.QCheckBox(self.tr("Bold"), parent)
- self.bold.setChecked(font.bold())
- self.addWidget(self.bold, 1, 2)
-
- def get_font(self):
- font = self.family.currentFont()
- font.setItalic(self.italic.isChecked())
- font.setBold(self.bold.isChecked())
- font.setPointSize(int(self.size.currentText()))
- return qfont_to_tuple(font)
-
-
-def is_edit_valid(edit):
- text = edit.text()
- state = edit.validator().validate(text, 0)[0]
-
- return state == QtGui.QDoubleValidator.Acceptable
-
-
-class FormWidget(QtWidgets.QWidget):
- update_buttons = QtCore.Signal()
-
- def __init__(self, data, comment="", parent=None):
- QtWidgets.QWidget.__init__(self, parent)
- self.data = copy.deepcopy(data)
- self.widgets = []
- self.formlayout = QtWidgets.QFormLayout(self)
- if comment:
- self.formlayout.addRow(QtWidgets.QLabel(comment))
- self.formlayout.addRow(QtWidgets.QLabel(" "))
-
- def get_dialog(self):
- """Return FormDialog instance"""
- dialog = self.parent()
- while not isinstance(dialog, QtWidgets.QDialog):
- dialog = dialog.parent()
- return dialog
-
- def setup(self):
- for label, value in self.data:
- if label is None and value is None:
- # Separator: (None, None)
- self.formlayout.addRow(QtWidgets.QLabel(" "),
- QtWidgets.QLabel(" "))
- self.widgets.append(None)
- continue
- elif label is None:
- # Comment
- self.formlayout.addRow(QtWidgets.QLabel(value))
- self.widgets.append(None)
- continue
- elif tuple_to_qfont(value) is not None:
- field = FontLayout(value, self)
- elif (label.lower() not in BLACKLIST
- and mcolors.is_color_like(value)):
- field = ColorLayout(to_qcolor(value), self)
- elif isinstance(value, str):
- field = QtWidgets.QLineEdit(value, self)
- elif isinstance(value, (list, tuple)):
- if isinstance(value, tuple):
- value = list(value)
- # Note: get() below checks the type of value[0] in self.data so
- # it is essential that value gets modified in-place.
- # This means that the code is actually broken in the case where
- # value is a tuple, but fortunately we always pass a list...
- selindex = value.pop(0)
- field = QtWidgets.QComboBox(self)
- if isinstance(value[0], (list, tuple)):
- keys = [key for key, _val in value]
- value = [val for _key, val in value]
- else:
- keys = value
- field.addItems(value)
- if selindex in value:
- selindex = value.index(selindex)
- elif selindex in keys:
- selindex = keys.index(selindex)
- elif not isinstance(selindex, Integral):
- _log.warning(
- "index '%s' is invalid (label: %s, value: %s)",
- selindex, label, value)
- selindex = 0
- field.setCurrentIndex(selindex)
- elif isinstance(value, bool):
- field = QtWidgets.QCheckBox(self)
- if value:
- field.setCheckState(QtCore.Qt.Checked)
- else:
- field.setCheckState(QtCore.Qt.Unchecked)
- elif isinstance(value, Integral):
- field = QtWidgets.QSpinBox(self)
- field.setRange(-1e9, 1e9)
- field.setValue(value)
- elif isinstance(value, Real):
- field = QtWidgets.QLineEdit(repr(value), self)
- field.setCursorPosition(0)
- field.setValidator(QtGui.QDoubleValidator(field))
- field.validator().setLocale(QtCore.QLocale("C"))
- dialog = self.get_dialog()
- dialog.register_float_field(field)
- field.textChanged.connect(lambda text: dialog.update_buttons())
- elif isinstance(value, datetime.datetime):
- field = QtWidgets.QDateTimeEdit(self)
- field.setDateTime(value)
- elif isinstance(value, datetime.date):
- field = QtWidgets.QDateEdit(self)
- field.setDate(value)
- else:
- field = QtWidgets.QLineEdit(repr(value), self)
- self.formlayout.addRow(label, field)
- self.widgets.append(field)
-
- def get(self):
- valuelist = []
- for index, (label, value) in enumerate(self.data):
- field = self.widgets[index]
- if label is None:
- # Separator / Comment
- continue
- elif tuple_to_qfont(value) is not None:
- value = field.get_font()
- elif isinstance(value, str) or mcolors.is_color_like(value):
- value = str(field.text())
- elif isinstance(value, (list, tuple)):
- index = int(field.currentIndex())
- if isinstance(value[0], (list, tuple)):
- value = value[index][0]
- else:
- value = value[index]
- elif isinstance(value, bool):
- value = field.checkState() == QtCore.Qt.Checked
- elif isinstance(value, Integral):
- value = int(field.value())
- elif isinstance(value, Real):
- value = float(str(field.text()))
- elif isinstance(value, datetime.datetime):
- value = field.dateTime().toPyDateTime()
- elif isinstance(value, datetime.date):
- value = field.date().toPyDate()
- else:
- value = eval(str(field.text()))
- valuelist.append(value)
- return valuelist
-
-
-class FormComboWidget(QtWidgets.QWidget):
- update_buttons = QtCore.Signal()
-
- def __init__(self, datalist, comment="", parent=None):
- QtWidgets.QWidget.__init__(self, parent)
- layout = QtWidgets.QVBoxLayout()
- self.setLayout(layout)
- self.combobox = QtWidgets.QComboBox()
- layout.addWidget(self.combobox)
-
- self.stackwidget = QtWidgets.QStackedWidget(self)
- layout.addWidget(self.stackwidget)
- self.combobox.currentIndexChanged.connect(
- self.stackwidget.setCurrentIndex)
-
- self.widgetlist = []
- for data, title, comment in datalist:
- self.combobox.addItem(title)
- widget = FormWidget(data, comment=comment, parent=self)
- self.stackwidget.addWidget(widget)
- self.widgetlist.append(widget)
-
- def setup(self):
- for widget in self.widgetlist:
- widget.setup()
-
- def get(self):
- return [widget.get() for widget in self.widgetlist]
-
-
-class FormTabWidget(QtWidgets.QWidget):
- update_buttons = QtCore.Signal()
-
- def __init__(self, datalist, comment="", parent=None):
- QtWidgets.QWidget.__init__(self, parent)
- layout = QtWidgets.QVBoxLayout()
- self.tabwidget = QtWidgets.QTabWidget()
- layout.addWidget(self.tabwidget)
- self.setLayout(layout)
- self.widgetlist = []
- for data, title, comment in datalist:
- if len(data[0]) == 3:
- widget = FormComboWidget(data, comment=comment, parent=self)
- else:
- widget = FormWidget(data, comment=comment, parent=self)
- index = self.tabwidget.addTab(widget, title)
- self.tabwidget.setTabToolTip(index, comment)
- self.widgetlist.append(widget)
-
- def setup(self):
- for widget in self.widgetlist:
- widget.setup()
-
- def get(self):
- return [widget.get() for widget in self.widgetlist]
-
-
-class FormDialog(QtWidgets.QDialog):
- """Form Dialog"""
- def __init__(self, data, title="", comment="",
- icon=None, parent=None, apply=None):
- QtWidgets.QDialog.__init__(self, parent)
-
- self.apply_callback = apply
-
- # Form
- if isinstance(data[0][0], (list, tuple)):
- self.formwidget = FormTabWidget(data, comment=comment,
- parent=self)
- elif len(data[0]) == 3:
- self.formwidget = FormComboWidget(data, comment=comment,
- parent=self)
- else:
- self.formwidget = FormWidget(data, comment=comment,
- parent=self)
- layout = QtWidgets.QVBoxLayout()
- layout.addWidget(self.formwidget)
-
- self.float_fields = []
- self.formwidget.setup()
-
- # Button box
- 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)
- apply_btn.clicked.connect(self.apply)
-
- bbox.accepted.connect(self.accept)
- bbox.rejected.connect(self.reject)
- layout.addWidget(bbox)
-
- self.setLayout(layout)
-
- self.setWindowTitle(title)
- if not isinstance(icon, QtGui.QIcon):
- icon = QtWidgets.QWidget().style().standardIcon(
- QtWidgets.QStyle.SP_MessageBoxQuestion)
- self.setWindowIcon(icon)
-
- def register_float_field(self, field):
- self.float_fields.append(field)
-
- def update_buttons(self):
- valid = True
- for field in self.float_fields:
- if not is_edit_valid(field):
- valid = False
- for btn_type in (QtWidgets.QDialogButtonBox.Ok,
- QtWidgets.QDialogButtonBox.Apply):
- btn = self.bbox.button(btn_type)
- if btn is not None:
- btn.setEnabled(valid)
-
- def accept(self):
- self.data = self.formwidget.get()
- QtWidgets.QDialog.accept(self)
-
- def reject(self):
- self.data = None
- QtWidgets.QDialog.reject(self)
-
- def apply(self):
- self.apply_callback(self.formwidget.get())
-
- def get(self):
- """Return form result"""
- return self.data
-
-
-def fedit(data, title="", comment="", icon=None, parent=None, apply=None):
- """
- Create form dialog and return result
- (if Cancel button is pressed, return None)
-
- data: datalist, datagroup
- title: string
- comment: string
- icon: QIcon instance
- parent: parent QWidget
- apply: apply callback (function)
-
- datalist: list/tuple of (field_name, field_value)
- datagroup: list/tuple of (datalist *or* datagroup, title, comment)
-
- -> one field for each member of a datalist
- -> one tab for each member of a top-level datagroup
- -> one page (of a multipage widget, each page can be selected with a combo
- box) for each member of a datagroup inside a datagroup
-
- Supported types for field_value:
- - int, float, str, unicode, bool
- - colors: in Qt-compatible text form, i.e. in hex format or name (red,...)
- (automatically detected from a string)
- - list/tuple:
- * the first element will be the selected index (or value)
- * the other elements can be couples (key, value) or only values
- """
-
- # Create a QApplication instance if no instance currently exists
- # (e.g., if the module is used directly from the interpreter)
- if QtWidgets.QApplication.startingUp():
- _app = QtWidgets.QApplication([])
- dialog = FormDialog(data, title, comment, icon, parent, apply)
- if dialog.exec_():
- return dialog.get()
-
-
-if __name__ == "__main__":
-
- def create_datalist_example():
- return [('str', 'this is a string'),
- ('list', [0, '1', '3', '4']),
- ('list2', ['--', ('none', 'None'), ('--', 'Dashed'),
- ('-.', 'DashDot'), ('-', 'Solid'),
- ('steps', 'Steps'), (':', 'Dotted')]),
- ('float', 1.2),
- (None, 'Other:'),
- ('int', 12),
- ('font', ('Arial', 10, False, True)),
- ('color', '#123409'),
- ('bool', True),
- ('date', datetime.date(2010, 10, 10)),
- ('datetime', datetime.datetime(2010, 10, 10)),
- ]
-
- def create_datagroup_example():
- datalist = create_datalist_example()
- return ((datalist, "Category 1", "Category 1 comment"),
- (datalist, "Category 2", "Category 2 comment"),
- (datalist, "Category 3", "Category 3 comment"))
-
- #--------- datalist example
- datalist = create_datalist_example()
-
- def apply_test(data):
- print("data:", data)
- print("result:", fedit(datalist, title="Example",
- comment="This is just an example.",
- apply=apply_test))
-
- #--------- datagroup example
- datagroup = create_datagroup_example()
- print("result:", fedit(datagroup, "Global title"))
-
- #--------- datagroup inside a datagroup example
- datalist = create_datalist_example()
- datagroup = create_datagroup_example()
- print("result:", fedit(((datagroup, "Title 1", "Tab 1 comment"),
- (datalist, "Title 2", "Tab 2 comment"),
- (datalist, "Title 3", "Tab 3 comment")),
- "Global title"))
+cbook.warn_deprecated(
+ "3.1", name=__name__, obj_type="module",
+ alternative="the formlayout module available on PyPI")
diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py
index cecd7abcb175..d68674858a91 100644
--- a/lib/matplotlib/tests/test_backend_qt.py
+++ b/lib/matplotlib/tests/test_backend_qt.py
@@ -251,6 +251,6 @@ def test_figureoptions():
ax.plot([1, 2])
ax.imshow([[1]])
with mock.patch(
- "matplotlib.backends.qt_editor.formlayout.FormDialog.exec_",
+ "matplotlib.backends.qt_editor._formlayout.FormDialog.exec_",
lambda self: None):
fig.canvas.manager.toolbar.edit_parameters()