From f85906460e88c8498b74d3e8f6261422750a5bc5 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 26 Nov 2018 10:08:25 +0100 Subject: [PATCH] Deprecate public access to the vendored formlayout module. --- .flake8 | 2 +- doc/api/next_api_changes/2018-11-26-AL.rst | 7 + .../backends/qt_editor/_formlayout.py | 547 +++++++++++++++++ .../backends/qt_editor/figureoptions.py | 8 +- .../backends/qt_editor/formlayout.py | 550 +----------------- lib/matplotlib/tests/test_backend_qt.py | 2 +- 6 files changed, 565 insertions(+), 551 deletions(-) create mode 100644 doc/api/next_api_changes/2018-11-26-AL.rst create mode 100644 lib/matplotlib/backends/qt_editor/_formlayout.py 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()