diff options
author | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2021-04-06 16:03:40 +0200 |
---|---|---|
committer | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2021-04-12 15:46:56 +0200 |
commit | 9a9f9fd2528c03df4b0e9dde48026a2181e8a410 (patch) | |
tree | 60b2c8b3e79d4f09421d10a551fd214d523f8814 /examples/widgets | |
parent | 59f92c2133ad1d43ba2f6a7a32c5b70fc6aba614 (diff) |
Rewrite the classwizard example
The classwizard created some outdated C++ header and source which is
not useful for Qt for Python.
Rewrite it to generate a Python class and add a special page allowing
for specifying properties and signals of QObjects. Add an overwrite
check and a 'Launch' checkbox to the conclusion page.
Use QFormLayout instead QGridLayout for the pages.
Task-number: PYSIDE-1112
Change-Id: Ice158553571e30ea069ceda8873bf165dc704afc
Reviewed-by: Christian Tismer <tismer@stackless.com>
Diffstat (limited to 'examples/widgets')
-rw-r--r-- | examples/widgets/dialogs/classwizard/classwizard.py | 563 | ||||
-rw-r--r-- | examples/widgets/dialogs/classwizard/classwizard.pyproject | 2 | ||||
-rw-r--r-- | examples/widgets/dialogs/classwizard/listchooser.py | 212 |
3 files changed, 501 insertions, 276 deletions
diff --git a/examples/widgets/dialogs/classwizard/classwizard.py b/examples/widgets/dialogs/classwizard/classwizard.py index ce7c02d89..7267d1d8b 100644 --- a/examples/widgets/dialogs/classwizard/classwizard.py +++ b/examples/widgets/dialogs/classwizard/classwizard.py @@ -1,7 +1,7 @@ ############################################################################# ## ## Copyright (C) 2013 Riverbank Computing Limited. -## Copyright (C) 2016 The Qt Company Ltd. +## Copyright (C) 2021 The Qt Company Ltd. ## Contact: http://www.qt.io/licensing/ ## ## This file is part of the Qt for Python examples of the Qt Toolkit. @@ -39,368 +39,381 @@ ## ############################################################################# -from PySide6 import QtCore, QtGui, QtWidgets +import os +from pathlib import Path +import sys + +from PySide6.QtCore import (QByteArray, QDir, QFile, QFileInfo, + QRegularExpression, Qt, QUrl, Slot) +from PySide6.QtGui import QDesktopServices, QPixmap +from PySide6.QtWidgets import (QApplication, QComboBox, QCheckBox, QFormLayout, + QFileDialog, QGroupBox, QGridLayout, + QHBoxLayout, QLabel, QLineEdit, QMessageBox, + QPushButton, QRadioButton, QToolButton, + QVBoxLayout, QWizard, QWizardPage) + +from listchooser import ListChooser, PropertyChooser, SignalChooser import classwizard_rc -class ClassWizard(QtWidgets.QWizard): - def __init__(self, parent=None): - super(ClassWizard, self).__init__(parent) +BASE_CLASSES = ['<None>', 'PySide6.QtCore.QObject', + 'PySide6.QtWidgets.QDialog', 'PySide6.QtWidgets.QMainWindow' + 'PySide6.QtWidgets.QWidget'] - self.addPage(IntroPage()) - self.addPage(ClassInfoPage()) - self.addPage(CodeStylePage()) - self.addPage(OutputFilesPage()) - self.addPage(ConclusionPage()) - self.setPixmap(QtWidgets.QWizard.BannerPixmap, - QtGui.QPixmap(':/images/banner.png')) - self.setPixmap(QtWidgets.QWizard.BackgroundPixmap, - QtGui.QPixmap(':/images/background.png')) +PYTHON_TYPES = ['int', 'list', 'str'] - self.setWindowTitle("Class Wizard") - def accept(self): - class_name = self.field('className') - base_class = self.field('baseClass') - macro_name = self.field('macroName') - base_include = self.field('baseInclude') +INTRODUCTION = """This wizard will generate a skeleton Python class definition,\ + including a few functions. You simply need to specify the class name and set\ + a few options to produce a Python file.""" - output_dir = self.field('outputDir') - header = self.field('header') - implementation = self.field('implementation') - block = '' +def property_accessors(property_type, name): + """Generate the property accessor functions.""" + return (f' @Property({property_type})\n' + f' def {name}(self):\n' + f' return self._{name}\n\n' + f' @{name}.setter\n' + f' def {name}(self, value):\n' + f' self._{name} = value\n') - if self.field('comment'): - block += '/*\n' - block += ' ' + header + '\n' - block += '*/\n' - block += '\n' - if self.field('protect'): - block += '#ifndef ' + macro_name + '\n' - block += '#define ' + macro_name + '\n' - block += '\n' +def property_initialization(property_type, name): + """Generate the property initialization for __init__().""" + return f' self._{name} = {property_type}()\n' - if self.field('includeBase'): - block += '#include ' + base_include + '\n' - block += '\n' - block += 'class ' + class_name - if base_class: - block += ' : public ' + base_class +def signal_initialization(signature): + """Generate the Signal initialization from the function signature.""" + paren_pos = signature.find('(') + name = signature[:paren_pos] + parameters = signature[paren_pos:] + return f' {name} = Signal{parameters}\n' - block += '\n' - block += '{\n' - if self.field('qobjectMacro'): - block += ' Q_OBJECT\n' - block += '\n' +class ClassWizard(QWizard): + def __init__(self, parent=None): + super(ClassWizard, self).__init__(parent) - block += 'public:\n' + self.addPage(IntroPage()) + self._class_info_index = self.addPage(ClassInfoPage()) + self._qobject_index = self.addPage(QObjectPage()) + self._output_index = self.addPage(OutputFilesPage()) + self.addPage(ConclusionPage()) - if self.field('qobjectCtor'): - block += ' ' + class_name + '(QObject *parent = 0);\n' - elif self.field('qwidgetCtor'): - block += ' ' + class_name + '(QWidget *parent = 0);\n' - elif self.field('defaultCtor'): - block += ' ' + class_name + '();\n' + self.setPixmap(QWizard.BannerPixmap, + QPixmap(':/images/banner.png')) + self.setPixmap(QWizard.BackgroundPixmap, + QPixmap(':/images/background.png')) - if self.field('copyCtor'): - block += ' ' + class_name + '(const ' + class_name + ' &other);\n' - block += '\n' - block += ' ' + class_name + ' &operator=' + '(const ' + class_name + ' &other);\n' + self.setWindowTitle("Class Wizard") - block += '};\n' + def nextId(self): + """Overrides QWizard.nextId() to insert the property/signal + page in case the class is a QObject.""" + id = self.currentId() + if self.currentId() == self._class_info_index: + qobject = self.field('qobject') + return self._qobject_index if qobject else self._output_index + return super(ClassWizard, self).nextId() + + def generate_code(self): + imports = [] # Classes to be imported + module_imports = {} # Module->class list + + def add_import(class_str): + """Add a class to the import list or module hash depending on + whether it is 'class' or 'module.class'. Returns the + class name.""" + dot = class_str.rfind('.') + if dot < 0: + imports.append(class_str) + return class_str + module = class_str[0:dot] + class_name = class_str[dot + 1:] + class_list = module_imports.get(module) + if class_list: + if class_name not in class_list: + class_list.append(class_name) + else: + module_imports[module] = [class_name] + return class_name + + class_name = self.field('className') + qobject = self.field('qobject') + base_class = self.field('baseClass') + if base_class.startswith('<'): # <None> + base_class = '' + if qobject and not base_class: + base_class = 'PySide6.QtCore.QObject' + + if base_class: + base_class = add_import(base_class) + + signals = self.field('signals') + if signals: + add_import('PySide6.QtCore.Signal') + + property_types = [] + property_names = [] + for p in self.field('properties'): + property_type, property_name = str(p).split(' ') + if property_type not in PYTHON_TYPES: + property_type = add_import(property_type) + property_types.append(property_type) + property_names.append(property_name) + + if property_names: + add_import('PySide6.QtCore.Property') + + signals = self.field('signals') + if signals: + add_import('PySide6.QtCore.Signal') + + property_types = [] + property_names = [] + for p in self.field('properties'): + property_type, property_name = str(p).split(' ') + if property_type not in PYTHON_TYPES: + property_type = add_import(property_type) + property_types.append(property_type) + property_names.append(property_name) + + if property_names: + add_import('PySide6.QtCore.Property') + + # Generate imports + block = '# This Python file uses the following encoding: utf-8\n\n' + for module, class_list in module_imports.items(): + class_list.sort() + class_list_str = ', '.join(class_list) + block += f'from {module} import {class_list_str}\n' + for klass in imports: + block += f'import {klass}\n' + + # Generate class definition + block += f'\n\nclass {class_name}' + if base_class: + block += f'({base_class})' + block += ':\n' + description = self.field('description') + if description: + block += f' """{description}"""\n' - if self.field('protect'): + if signals: block += '\n' - block += '#endif\n' + for s in signals: + block += signal_initialization(str(s)) - header_file = QtCore.QFile(output_dir + '/' + header) + # Generate __init__ function + block += '\n def __init__(self' + if qobject: + block += ', parent=None' + block += '):\n' - if not header_file.open(QtCore.QFile.WriteOnly | QtCore.QFile.Text): - name = header_file.fileName() - reason = header_file.errorString() - QtWidgets.QMessageBox.warning(None, "Class Wizard", - f"Cannot write file {name}:\n{reason}") - return + if base_class: + block += f' super({class_name}, self).__init__(' + if qobject: + block += 'parent' + block += ')\n' - header_file.write(QtCore.QByteArray(block.encode("utf-8"))) + for i, name in enumerate(property_names): + block += property_initialization(property_types[i], name) - block = '' + if not base_class and not property_names: + block += ' pass\n' - if self.field('comment'): - block += '/*\n' - block += ' ' + implementation + '\n' - block += '*/\n' - block += '\n' + # Generate property accessors + for i, name in enumerate(property_names): + block += '\n' + property_accessors(property_types[i], name) + + return block - block += '#include "' + header + '"\n' - block += '\n' - - if self.field('qobjectCtor'): - block += class_name + '::' + class_name + '(QObject *parent)\n' - block += ' : ' + base_class + '(parent)\n' - block += '{\n' - block += '}\n' - elif self.field('qwidgetCtor'): - block += class_name + '::' + class_name + '(QWidget *parent)\n' - block += ' : ' + base_class + '(parent)\n' - block += '{\n' - block += '}\n' - elif self.field('defaultCtor'): - block += class_name + '::' + class_name + '()\n' - block += '{\n' - block += ' // missing code\n' - block += '}\n' - - if self.field('copyCtor'): - block += '\n' - block += class_name + '::' + class_name + '(const ' + class_name + ' &other)\n' - block += '{\n' - block += ' *this = other;\n' - block += '}\n' - block += '\n' - block += class_name + ' &' + class_name + '::operator=(const ' + class_name + ' &other)\n' - block += '{\n' - - if base_class: - block += ' ' + base_class + '::operator=(other);\n' - - block += ' // missing code\n' - block += ' return *this;\n' - block += '}\n' - - implementation_file = QtCore.QFile(output_dir + '/' + implementation) - - if not implementation_file.open(QtCore.QFile.WriteOnly | QtCore.QFile.Text): - name = implementation_file.fileName() - reason = implementation_file.errorString() - QtWidgets.QMessageBox.warning(None, "Class Wizard", - f"Cannot write file {name}:\n{reason}") + def accept(self): + file_name = self.field('file') + output_dir = self.field('outputDir') + python_file = Path(output_dir) / file_name + name = os.fspath(python_file) + try: + python_file.write_text(self.generate_code()) + except (OSError, PermissionError) as e: + reason = str(e) + QMessageBox.warning(None, "Class Wizard", + f"Cannot write file {name}:\n{reason}") return - implementation_file.write(QtCore.QByteArray(block.encode("utf-8"))) + if self.field('launch'): + url = QUrl.fromLocalFile(QDir.fromNativeSeparators(name)) + QDesktopServices.openUrl(url) super(ClassWizard, self).accept() -class IntroPage(QtWidgets.QWizardPage): +class IntroPage(QWizardPage): def __init__(self, parent=None): super(IntroPage, self).__init__(parent) self.setTitle("Introduction") - self.setPixmap(QtWidgets.QWizard.WatermarkPixmap, - QtGui.QPixmap(':/images/watermark1.png')) - - label = QtWidgets.QLabel("This wizard will generate a skeleton C++ class " - "definition, including a few functions. You simply need to " - "specify the class name and set a few options to produce a " - "header file and an implementation file for your new C++ " - "class.") + self.setPixmap(QWizard.WatermarkPixmap, + QPixmap(':/images/watermark1.png')) + + label = QLabel(INTRODUCTION) label.setWordWrap(True) - layout = QtWidgets.QVBoxLayout() + layout = QVBoxLayout(self) layout.addWidget(label) - self.setLayout(layout) -class ClassInfoPage(QtWidgets.QWizardPage): +class ClassInfoPage(QWizardPage): def __init__(self, parent=None): super(ClassInfoPage, self).__init__(parent) self.setTitle("Class Information") self.setSubTitle("Specify basic information about the class for " - "which you want to generate skeleton source code files.") - self.setPixmap(QtWidgets.QWizard.LogoPixmap, - QtGui.QPixmap(':/images/logo1.png')) + "which you want to generate a skeleton source code file.") + self.setPixmap(QWizard.LogoPixmap, + QPixmap(':/images/logo1.png')) - class_name_label = QtWidgets.QLabel("&Class name:") - class_name_line_edit = QtWidgets.QLineEdit() - class_name_label.setBuddy(class_name_line_edit) + class_name_line_edit = QLineEdit() + class_name_line_edit.setClearButtonEnabled(True) - base_class_label = QtWidgets.QLabel("B&ase class:") - base_class_line_edit = QtWidgets.QLineEdit() - base_class_label.setBuddy(base_class_line_edit) + self._base_class_combo = QComboBox() + self._base_class_combo.addItems(BASE_CLASSES) + self._base_class_combo.setEditable(True) - qobject_macro_check_box = QtWidgets.QCheckBox("Generate Q_OBJECT ¯o") + base_class_line_edit = self._base_class_combo.lineEdit() + base_class_line_edit.setPlaceholderText('Module.Class') + self._base_class_combo.currentTextChanged.connect(self._base_class_changed) - group_box = QtWidgets.QGroupBox("C&onstructor") + description_line_edit = QLineEdit() + description_line_edit.setClearButtonEnabled(True) - qobject_ctor_radio_button = QtWidgets.QRadioButton("&QObject-style constructor") - qwidget_ctor_radio_button = QtWidgets.QRadioButton("Q&Widget-style constructor") - default_ctor_radio_button = QtWidgets.QRadioButton("&Default constructor") - copy_ctor_check_box = QtWidgets.QCheckBox("&Generate copy constructor and operator=") - - default_ctor_radio_button.setChecked(True) - - default_ctor_radio_button.toggled.connect(copy_ctor_check_box.setEnabled) + self._qobject_check_box = QCheckBox("Inherits QObject") self.registerField('className*', class_name_line_edit) self.registerField('baseClass', base_class_line_edit) - self.registerField('qobjectMacro', qobject_macro_check_box) - self.registerField('qobjectCtor', qobject_ctor_radio_button) - self.registerField('qwidgetCtor', qwidget_ctor_radio_button) - self.registerField('defaultCtor', default_ctor_radio_button) - self.registerField('copyCtor', copy_ctor_check_box) - - group_box_layout = QtWidgets.QVBoxLayout() - group_box_layout.addWidget(qobject_ctor_radio_button) - group_box_layout.addWidget(qwidget_ctor_radio_button) - group_box_layout.addWidget(default_ctor_radio_button) - group_box_layout.addWidget(copy_ctor_check_box) - group_box.setLayout(group_box_layout) - - layout = QtWidgets.QGridLayout() - layout.addWidget(class_name_label, 0, 0) - layout.addWidget(class_name_line_edit, 0, 1) - layout.addWidget(base_class_label, 1, 0) - layout.addWidget(base_class_line_edit, 1, 1) - layout.addWidget(qobject_macro_check_box, 2, 0, 1, 2) - layout.addWidget(group_box, 3, 0, 1, 2) - self.setLayout(layout) - - -class CodeStylePage(QtWidgets.QWizardPage): - def __init__(self, parent=None): - super(CodeStylePage, self).__init__(parent) - - self.setTitle("Code Style Options") - self.setSubTitle("Choose the formatting of the generated code.") - self.setPixmap(QtWidgets.QWizard.LogoPixmap, - QtGui.QPixmap(':/images/logo2.png')) - - comment_check_box = QtWidgets.QCheckBox("&Start generated files with a " - "comment") - comment_check_box.setChecked(True) - - protect_check_box = QtWidgets.QCheckBox("&Protect header file against " - "multiple inclusions") - protect_check_box.setChecked(True) - - macro_name_label = QtWidgets.QLabel("&Macro name:") - self._macro_name_line_edit = QtWidgets.QLineEdit() - macro_name_label.setBuddy(self._macro_name_line_edit) - - self._include_base_check_box = QtWidgets.QCheckBox("&Include base class " - "definition") - self._base_include_label = QtWidgets.QLabel("Base class include:") - self._base_include_line_edit = QtWidgets.QLineEdit() - self._base_include_label.setBuddy(self._base_include_line_edit) - - protect_check_box.toggled.connect(macro_name_label.setEnabled) - protect_check_box.toggled.connect(self._macro_name_line_edit.setEnabled) - self._include_base_check_box.toggled.connect(self._base_include_label.setEnabled) - self._include_base_check_box.toggled.connect(self._base_include_line_edit.setEnabled) - - self.registerField('comment', comment_check_box) - self.registerField('protect', protect_check_box) - self.registerField('macroName', self._macro_name_line_edit) - self.registerField('includeBase', self._include_base_check_box) - self.registerField('baseInclude', self._base_include_line_edit) - - layout = QtWidgets.QGridLayout() - layout.setColumnMinimumWidth(0, 20) - layout.addWidget(comment_check_box, 0, 0, 1, 3) - layout.addWidget(protect_check_box, 1, 0, 1, 3) - layout.addWidget(macro_name_label, 2, 1) - layout.addWidget(self._macro_name_line_edit, 2, 2) - layout.addWidget(self._include_base_check_box, 3, 0, 1, 3) - layout.addWidget(self._base_include_label, 4, 1) - layout.addWidget(self._base_include_line_edit, 4, 2) - self.setLayout(layout) + self.registerField('description', description_line_edit) + self.registerField('qobject', self._qobject_check_box) - def initializePage(self): - class_name = self.field('className') - self._macro_name_line_edit.setText(class_name.upper() + "_H") - - base_class = self.field('baseClass') - is_baseClass = bool(base_class) + layout = QFormLayout(self) + layout.addRow("&Class name:", class_name_line_edit) + layout.addRow("B&ase class:", self._base_class_combo) + layout.addRow("&Description:", description_line_edit) + layout.addRow(self._qobject_check_box) - self._include_base_check_box.setChecked(is_baseClass) - self._include_base_check_box.setEnabled(is_baseClass) - self._base_include_label.setEnabled(is_baseClass) - self._base_include_line_edit.setEnabled(is_baseClass) + @Slot(str) + def _base_class_changed(self, text): + is_qobject = text.startswith('PySide') + self._qobject_check_box.setChecked(is_qobject) - if not is_baseClass: - self._base_include_line_edit.clear() - elif QtCore.QRegularExpression('^Q[A-Z].*$').match(base_class).hasMatch(): - self._base_include_line_edit.setText('<' + base_class + '>') - else: - self._base_include_line_edit.setText('"' + base_class.lower() + '.h"') - -class OutputFilesPage(QtWidgets.QWizardPage): +class QObjectPage(QWizardPage): + """Allows for adding properties and signals to a QObject.""" + def __init__(self, parent=None): + super(QObjectPage, self).__init__(parent) + + self.setTitle("QObject parameters") + self.setSubTitle("Specify the signals, slots and properties.") + self.setPixmap(QWizard.LogoPixmap, + QPixmap(':/images/logo2.png')) + layout = QVBoxLayout(self) + self._properties_chooser = PropertyChooser() + self.registerField('properties', self._properties_chooser, 'items') + layout.addWidget(self._properties_chooser) + self._signals_chooser = SignalChooser() + self.registerField('signals', self._signals_chooser, 'items') + layout.addWidget(self._signals_chooser) + + +class OutputFilesPage(QWizardPage): def __init__(self, parent=None): super(OutputFilesPage, self).__init__(parent) self.setTitle("Output Files") self.setSubTitle("Specify where you want the wizard to put the " - "generated skeleton code.") - self.setPixmap(QtWidgets.QWizard.LogoPixmap, - QtGui.QPixmap(':/images/logo3.png')) - - output_dir_label = QtWidgets.QLabel("&Output directory:") - self._output_dir_line_edit = QtWidgets.QLineEdit() + "generated skeleton code.") + self.setPixmap(QWizard.LogoPixmap, + QPixmap(':/images/logo3.png')) + + output_dir_label = QLabel("&Output directory:") + output_dir_layout = QHBoxLayout() + self._output_dir_line_edit = QLineEdit() + output_dir_layout.addWidget(self._output_dir_line_edit) output_dir_label.setBuddy(self._output_dir_line_edit) + output_dir_button = QToolButton() + output_dir_button.setText('...') + output_dir_button.clicked.connect(self._choose_output_dir) + output_dir_layout.addWidget(output_dir_button) - header_label = QtWidgets.QLabel("&Header file name:") - self._header_line_edit = QtWidgets.QLineEdit() - header_label.setBuddy(self._header_line_edit) - - implementation_label = QtWidgets.QLabel("&Implementation file name:") - self._implementation_line_edit = QtWidgets.QLineEdit() - implementation_label.setBuddy(self._implementation_line_edit) + self._file_line_edit = QLineEdit() self.registerField('outputDir*', self._output_dir_line_edit) - self.registerField('header*', self._header_line_edit) - self.registerField('implementation*', self._implementation_line_edit) - - layout = QtWidgets.QGridLayout() - layout.addWidget(output_dir_label, 0, 0) - layout.addWidget(self._output_dir_line_edit, 0, 1) - layout.addWidget(header_label, 1, 0) - layout.addWidget(self._header_line_edit, 1, 1) - layout.addWidget(implementation_label, 2, 0) - layout.addWidget(self._implementation_line_edit, 2, 1) - self.setLayout(layout) + self.registerField('file*', self._file_line_edit) + + layout = QFormLayout(self) + layout.addRow(output_dir_label, output_dir_layout) + layout.addRow("&File name:", self._file_line_edit) def initializePage(self): class_name = self.field('className') - self._header_line_edit.setText(class_name.lower() + '.h') - self._implementation_line_edit.setText(class_name.lower() + '.cpp') - self._output_dir_line_edit.setText(QtCore.QDir.toNativeSeparators(QtCore.QDir.tempPath())) + self._file_line_edit.setText(class_name.lower() + '.py') + self.set_output_dir(QDir.tempPath()) + + def set_output_dir(self, dir): + self._output_dir_line_edit.setText(QDir.toNativeSeparators(dir)) + + def output_dir(self): + return QDir.fromNativeSeparators(self._output_dir_line_edit.text()) + def file_name(self): + return self.output_dir() + '/' + self._file_line_edit.text() -class ConclusionPage(QtWidgets.QWizardPage): + def _choose_output_dir(self): + dir = QFileDialog.getExistingDirectory(self, "Output Directory", + self.output_dir()) + if dir: + self.set_output_dir(dir) + + def validatePage(self): + """Ensure we do not overwrite existing files.""" + name = self.file_name() + if QFileInfo.exists(name): + question = f'{name} already exists. Would you like to overwrite it?' + r = QMessageBox.question(self, 'File Exists', question) + if r != QMessageBox.Yes: + return False + return True + + +class ConclusionPage(QWizardPage): def __init__(self, parent=None): super(ConclusionPage, self).__init__(parent) self.setTitle("Conclusion") - self.setPixmap(QtWidgets.QWizard.WatermarkPixmap, - QtGui.QPixmap(':/images/watermark2.png')) + self.setPixmap(QWizard.WatermarkPixmap, + QPixmap(':/images/watermark2.png')) - self.label = QtWidgets.QLabel() + self.label = QLabel() self.label.setWordWrap(True) - layout = QtWidgets.QVBoxLayout() + self._launch_check_box = QCheckBox("Launch") + self.registerField('launch', self._launch_check_box) + + layout = QVBoxLayout(self) layout.addWidget(self.label) - self.setLayout(layout) + layout.addWidget(self._launch_check_box) def initializePage(self): - finish_text = self.wizard().buttonText(QtWidgets.QWizard.FinishButton) - finish_text.replace('&', '') + finish_text = self.wizard().buttonText(QWizard.FinishButton) + finish_text = finish_text.replace('&', '') self.label.setText(f"Click {finish_text} to generate the class skeleton.") + self._launch_check_box.setChecked(True) if __name__ == '__main__': - - import sys - - app = QtWidgets.QApplication(sys.argv) + app = QApplication(sys.argv) wizard = ClassWizard() wizard.show() sys.exit(app.exec_()) diff --git a/examples/widgets/dialogs/classwizard/classwizard.pyproject b/examples/widgets/dialogs/classwizard/classwizard.pyproject index 1c1fe9998..6086099b8 100644 --- a/examples/widgets/dialogs/classwizard/classwizard.pyproject +++ b/examples/widgets/dialogs/classwizard/classwizard.pyproject @@ -1,4 +1,4 @@ { "files": ["classwizard.qrc", "classwizard.py", "classwizard_rc.py", - "classwizard_rc.pyc"] + "listchooser.py", "classwizard_rc.pyc"] } diff --git a/examples/widgets/dialogs/classwizard/listchooser.py b/examples/widgets/dialogs/classwizard/listchooser.py new file mode 100644 index 000000000..8b6f0d020 --- /dev/null +++ b/examples/widgets/dialogs/classwizard/listchooser.py @@ -0,0 +1,212 @@ +############################################################################# +## +## Copyright (C) 2021 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide6.QtCore import (QCoreApplication, QDir, QRegularExpression, Qt, + Property, Slot) +from PySide6.QtGui import QRegularExpressionValidator +from PySide6.QtWidgets import (QComboBox, QDialog, QDialogButtonBox, + QFormLayout, QGroupBox, QHBoxLayout, + QInputDialog, QLineEdit, QListWidget, + QListWidgetItem, QPushButton, QVBoxLayout, + QWidget) + + +DEFAULT_TYPES = ['int', 'str', 'PySide6.QtCore.QPoint', 'PySide6.QtCore.QRect', + 'PySide6.QtCore.QSize', 'PySide6.QtGui.QColor'] + + +FUNCTION_PATTERN = r'^\w+\([\w ,]*\)$' + + +class ValidatingInputDialog(QDialog): + """A dialog for text input with a regular expression validation.""" + def __init__(self, label, pattern, parent=None): + super(ValidatingInputDialog, self).__init__(parent) + layout = QVBoxLayout(self) + + self._form_layout = QFormLayout() + self._lineedit = QLineEdit() + self._lineedit.setClearButtonEnabled(True) + re = QRegularExpression(pattern) + assert(re.isValid()) + self._validator = QRegularExpressionValidator(re, self) + self._lineedit.setValidator(self._validator) + self._form_layout.addRow(label, self._lineedit) + layout.addLayout(self._form_layout) + + bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + layout.addWidget(bb) + bb.rejected.connect(self.reject) + bb.accepted.connect(self.accept) + + @Property(str) + def text(self): + return self._lineedit.text() + + @text.setter + def text(self, t): + self._lineedit.setText(t) + + @Property(str) + def placeholder_text(self): + return self._lineedit.placeholderText() + + @placeholder_text.setter + def placeholder_text(self, t): + self._lineedit.setPlaceholderText(t) + + @Property(int) + def cursor_position(self): + return self._lineedit.cursorPosition() + + @cursor_position.setter + def cursor_position(self, p): + self._lineedit.setCursorPosition(p) + + def is_valid(self): + return self.text + + def accept(self): + if self.is_valid(): + super(ValidatingInputDialog, self).accept() + + +class FunctionSignatureDialog(ValidatingInputDialog): + """A dialog for input of function signatures.""" + def __init__(self, name, parent=None): + super(FunctionSignatureDialog, self).__init__(name, FUNCTION_PATTERN, + parent) + self.text = '()' + self.cursor_position = 0 + + +class PropertyDialog(ValidatingInputDialog): + """A dialog for input of a property name and type.""" + def __init__(self, parent=None): + super(PropertyDialog, self).__init__('&Name:', r'^\w+$', parent) + self.setWindowTitle('Add a Property') + self._type_combo = QComboBox() + self._type_combo.addItems(DEFAULT_TYPES) + self._form_layout.insertRow(0, '&Type:', self._type_combo) + + def property_type(self): + return self._type_combo.currentText() + + +class ListChooser(QGroupBox): + """A widget for editing a list of strings with a customization point + for creating the strings.""" + def __init__(self, title, parent=None): + super(ListChooser, self).__init__(title, parent) + main_layout = QHBoxLayout(self) + self._list = QListWidget(self) + self._list.currentItemChanged.connect(self._current_item_changed) + main_layout.addWidget(self._list) + + vbox_layout = QVBoxLayout() + main_layout.addLayout(vbox_layout) + self._addButton = QPushButton("Add...") + vbox_layout.addWidget(self._addButton) + self._addButton.clicked.connect(self._add) + self._removeButton = QPushButton("Remove") + self._removeButton.setEnabled(False) + self._removeButton.clicked.connect(self._remove_current) + vbox_layout.addWidget(self._removeButton) + vbox_layout.addStretch() + + @Property(list) + def items(self): + result = [] + for i in range(self._list.count()): + result.append(self._list.item(i).text()) + return result + + @items.setter + def items(self, item_list): + self._list.clear() + for i in item_list: + self._list.append(i) + + @Slot(QListWidgetItem, QListWidgetItem) + def _current_item_changed(self, current, previous): + self._removeButton.setEnabled(current is not None) + + @Slot() + def _add(self): + new_item = self._create_new_item() + if new_item: + self._list.addItem(new_item) + + def _create_new_item(self): + """Overwrite to return a new item.""" + return 'new_item' + + @Slot() + def _remove_current(self): + row = self._list.row(self._list.currentItem()) + if row >= 0: + self._list.takeItem(row) + + +class SignalChooser(ListChooser): + """A widget for editing a list of signal function signatures.""" + def __init__(self, parent=None): + super(SignalChooser, self).__init__('Signals', parent) + + def _create_new_item(self): + dialog = FunctionSignatureDialog('&Signal signature:', self) + dialog.setWindowTitle('Enter Signal') + if dialog.exec_() != QDialog.Accepted: + return '' + return dialog.text + + +class PropertyChooser(ListChooser): + """A widget for editing a list of properties as a string of 'type name'.""" + def __init__(self, parent=None): + super(PropertyChooser, self).__init__('Properties', parent) + + def _create_new_item(self): + dialog = PropertyDialog(self) + if dialog.exec_() != QDialog.Accepted: + return '' + name = dialog.text + property_type = dialog.property_type() + return f'{property_type} {name}' |