From 25314fd2aed81aa5281bc3d93dbcc7919368ebe1 Mon Sep 17 00:00:00 2001 From: Bicker Date: Wed, 22 May 2019 17:45:37 +0200 Subject: Initial commit --- gui/__pycache__/helper.cpython-35.pyc | Bin 0 -> 984 bytes gui/helper.py | 23 ++++ gui/model/__pycache__/form.cpython-35.pyc | Bin 0 -> 2007 bytes gui/model/__pycache__/question.cpython-35.pyc | Bin 0 -> 3248 bytes gui/model/form.py | 44 +++++++ gui/model/question.py | 83 ++++++++++++ gui/widgets/__pycache__/calendar.cpython-35.pyc | Bin 0 -> 1142 bytes gui/widgets/__pycache__/checkbox.cpython-35.pyc | Bin 0 -> 1043 bytes .../__pycache__/double_spinbox.cpython-35.pyc | Bin 0 -> 1209 bytes gui/widgets/__pycache__/label.cpython-35.pyc | Bin 0 -> 702 bytes gui/widgets/__pycache__/line_edit.cpython-35.pyc | Bin 0 -> 1038 bytes .../__pycache__/money_spinbox.cpython-35.pyc | Bin 0 -> 1216 bytes gui/widgets/__pycache__/spinbox.cpython-35.pyc | Bin 0 -> 1133 bytes gui/widgets/calendar.py | 13 ++ gui/widgets/checkbox.py | 12 ++ gui/widgets/double_spinbox.py | 15 +++ gui/widgets/label.py | 9 ++ gui/widgets/line_edit.py | 12 ++ gui/widgets/money_spinbox.py | 15 +++ gui/widgets/spinbox.py | 14 +++ gui/windows/__pycache__/form.cpython-35.pyc | Bin 0 -> 2762 bytes gui/windows/__pycache__/main.cpython-35.pyc | Bin 0 -> 4832 bytes gui/windows/form.py | 68 ++++++++++ gui/windows/main.py | 139 +++++++++++++++++++++ 24 files changed, 447 insertions(+) create mode 100644 gui/__pycache__/helper.cpython-35.pyc create mode 100644 gui/helper.py create mode 100644 gui/model/__pycache__/form.cpython-35.pyc create mode 100644 gui/model/__pycache__/question.cpython-35.pyc create mode 100644 gui/model/form.py create mode 100644 gui/model/question.py create mode 100644 gui/widgets/__pycache__/calendar.cpython-35.pyc create mode 100644 gui/widgets/__pycache__/checkbox.cpython-35.pyc create mode 100644 gui/widgets/__pycache__/double_spinbox.cpython-35.pyc create mode 100644 gui/widgets/__pycache__/label.cpython-35.pyc create mode 100644 gui/widgets/__pycache__/line_edit.cpython-35.pyc create mode 100644 gui/widgets/__pycache__/money_spinbox.cpython-35.pyc create mode 100644 gui/widgets/__pycache__/spinbox.cpython-35.pyc create mode 100644 gui/widgets/calendar.py create mode 100644 gui/widgets/checkbox.py create mode 100644 gui/widgets/double_spinbox.py create mode 100644 gui/widgets/label.py create mode 100644 gui/widgets/line_edit.py create mode 100644 gui/widgets/money_spinbox.py create mode 100644 gui/widgets/spinbox.py create mode 100644 gui/windows/__pycache__/form.cpython-35.pyc create mode 100644 gui/windows/__pycache__/main.cpython-35.pyc create mode 100644 gui/windows/form.py create mode 100644 gui/windows/main.py (limited to 'gui') diff --git a/gui/__pycache__/helper.cpython-35.pyc b/gui/__pycache__/helper.cpython-35.pyc new file mode 100644 index 0000000..ee2c6ac Binary files /dev/null and b/gui/__pycache__/helper.cpython-35.pyc differ diff --git a/gui/helper.py b/gui/helper.py new file mode 100644 index 0000000..615c87a --- /dev/null +++ b/gui/helper.py @@ -0,0 +1,23 @@ +from PyQt5.QtWidgets import QMessageBox + + +def append_file_extension(file_name, extension): + if not file_name.endswith('.' + extension): + return '{}.{}'.format(file_name, extension) + return file_name + + +def error(errors): + message = '\n'.join(errors) + QMessageBox.critical(QMessageBox(), 'Error', message, QMessageBox.Close, QMessageBox.Escape) + + +def warning(warnings): + message = '\n'.join(warnings) + QMessageBox.warning(QMessageBox(), 'Warning', message, QMessageBox.Close, QMessageBox.Escape) + + +def check_errors(errors): + if errors: + error(errors) + raise SyntaxError diff --git a/gui/model/__pycache__/form.cpython-35.pyc b/gui/model/__pycache__/form.cpython-35.pyc new file mode 100644 index 0000000..cd48947 Binary files /dev/null and b/gui/model/__pycache__/form.cpython-35.pyc differ diff --git a/gui/model/__pycache__/question.cpython-35.pyc b/gui/model/__pycache__/question.cpython-35.pyc new file mode 100644 index 0000000..64bb4cf Binary files /dev/null and b/gui/model/__pycache__/question.cpython-35.pyc differ diff --git a/gui/model/form.py b/gui/model/form.py new file mode 100644 index 0000000..3f045ce --- /dev/null +++ b/gui/model/form.py @@ -0,0 +1,44 @@ +from json import dumps + + +class FormModel: + def __init__(self, identifier): + self.__identifier = identifier + self.__block = [] + + @property + def identifier(self): + return self.__identifier + + @property + def block(self): + return self.__block + + def to_json(self): + return dumps({question.identifier: question.answer.value.get_json_value() for question in self.block}) + + def find_question_of_widget(self, widget): + for question in self.block: + if question.widget == widget: + return question + + def update_questions_on_change(self, changed_widget): + changed_question = self.find_question_of_widget(changed_widget) + + if changed_question: + new_value = changed_question.widget.get_value(changed_question.answer_type) + changed_question.answer = changed_question.answer_type.get_literal_node(new_value) + + for question in self.block: + result = question.evaluate_visibility_condition(self) + + if result: + question.widget.show() + question.widget_label.show() + else: + question.widget.hide() + question.widget_label.hide() + + if question.computed: + answer_result = str(question.evaluate_answer(self)) + question.widget.setText(answer_result) diff --git a/gui/model/question.py b/gui/model/question.py new file mode 100644 index 0000000..a45d6e2 --- /dev/null +++ b/gui/model/question.py @@ -0,0 +1,83 @@ +from gui.widgets.label import Label +from ql.ast.visitors.expression_evaluator import ExpressionEvaluator + + +class QuestionModel: + def __init__(self, label, identifier, answer_type, answer, computed, visibility_condition): + self.__label = label + self.__identifier = identifier + self.__answer_type = answer_type + self.__answer = answer + self.__computed = computed + self.__visibility_condition = visibility_condition + self.__widget = None + self.__widget_label = None + + @property + def label(self): + return self.__label + + @property + def identifier(self): + return self.__identifier + + @property + def answer_type(self): + return self.__answer_type + + @property + def answer(self): + return self.__answer + + @answer.setter + def answer(self, value): + self.__answer = value + + @property + def computed(self): + return self.__computed + + @property + def visibility_condition(self): + return self.__visibility_condition + + @property + def widget(self): + return self.__widget + + @widget.setter + def widget(self, value): + self.__widget = value + + @property + def widget_label(self): + return self.__widget_label + + @widget_label.setter + def widget_label(self, value): + self.__widget_label = value + + def evaluate_visibility_condition(self, form): + visitor = ExpressionEvaluator(form) + visitor.visit(self.visibility_condition) + return visitor.result + + def evaluate_answer(self, form): + visitor = ExpressionEvaluator(form) + visitor.visit(self.answer) + return visitor.result + + def pyqt5_render(self, layout, form, visible=True): + self.widget_label = Label(self.label) + + if self.computed: + answer_result = str(self.evaluate_answer(form)) + self.widget = Label(answer_result) + else: + self.widget = self.answer_type.pyqt5_default_widget() + + if not visible: + self.widget.hide() + self.widget_label.hide() + + layout.addRow(self.widget_label, self.widget) diff --git a/gui/widgets/__pycache__/calendar.cpython-35.pyc b/gui/widgets/__pycache__/calendar.cpython-35.pyc new file mode 100644 index 0000000..fcd150c Binary files /dev/null and b/gui/widgets/__pycache__/calendar.cpython-35.pyc differ diff --git a/gui/widgets/__pycache__/checkbox.cpython-35.pyc b/gui/widgets/__pycache__/checkbox.cpython-35.pyc new file mode 100644 index 0000000..49f87fc Binary files /dev/null and b/gui/widgets/__pycache__/checkbox.cpython-35.pyc differ diff --git a/gui/widgets/__pycache__/double_spinbox.cpython-35.pyc b/gui/widgets/__pycache__/double_spinbox.cpython-35.pyc new file mode 100644 index 0000000..31d21df Binary files /dev/null and b/gui/widgets/__pycache__/double_spinbox.cpython-35.pyc differ diff --git a/gui/widgets/__pycache__/label.cpython-35.pyc b/gui/widgets/__pycache__/label.cpython-35.pyc new file mode 100644 index 0000000..13726bf Binary files /dev/null and b/gui/widgets/__pycache__/label.cpython-35.pyc differ diff --git a/gui/widgets/__pycache__/line_edit.cpython-35.pyc b/gui/widgets/__pycache__/line_edit.cpython-35.pyc new file mode 100644 index 0000000..8b314d1 Binary files /dev/null and b/gui/widgets/__pycache__/line_edit.cpython-35.pyc differ diff --git a/gui/widgets/__pycache__/money_spinbox.cpython-35.pyc b/gui/widgets/__pycache__/money_spinbox.cpython-35.pyc new file mode 100644 index 0000000..f55f3da Binary files /dev/null and b/gui/widgets/__pycache__/money_spinbox.cpython-35.pyc differ diff --git a/gui/widgets/__pycache__/spinbox.cpython-35.pyc b/gui/widgets/__pycache__/spinbox.cpython-35.pyc new file mode 100644 index 0000000..d23e28b Binary files /dev/null and b/gui/widgets/__pycache__/spinbox.cpython-35.pyc differ diff --git a/gui/widgets/calendar.py b/gui/widgets/calendar.py new file mode 100644 index 0000000..5f09834 --- /dev/null +++ b/gui/widgets/calendar.py @@ -0,0 +1,13 @@ +from PyQt5.QtWidgets import QCalendarWidget + + +class CalendarWidget(QCalendarWidget): + def __init__(self): + super(QCalendarWidget, self).__init__() + + def get_value(self, ql_type): + date = self.selectedDate() + return ql_type(date.day(), date.month(), date.year()) + + def on_change(self, change_event_function): + self.selectionChanged.connect(lambda: change_event_function(self)) diff --git a/gui/widgets/checkbox.py b/gui/widgets/checkbox.py new file mode 100644 index 0000000..d8e32f1 --- /dev/null +++ b/gui/widgets/checkbox.py @@ -0,0 +1,12 @@ +from PyQt5.QtWidgets import QCheckBox + + +class CheckBox(QCheckBox): + def __init__(self): + super(QCheckBox, self).__init__() + + def get_value(self, ql_type): + return ql_type(self.isChecked()) + + def on_change(self, change_event_function): + self.stateChanged.connect(lambda: change_event_function(self)) diff --git a/gui/widgets/double_spinbox.py b/gui/widgets/double_spinbox.py new file mode 100644 index 0000000..bde201d --- /dev/null +++ b/gui/widgets/double_spinbox.py @@ -0,0 +1,15 @@ +from PyQt5.QtWidgets import QDoubleSpinBox + + +class DoubleSpinBox(QDoubleSpinBox): + def __init__(self): + super(QDoubleSpinBox, self).__init__() + self.setMaximum(2**31 - 1) + self.setMinimum(-self.maximum()) + self.setDecimals(16) + + def get_value(self, ql_type): + return ql_type(self.value()) + + def on_change(self, change_event_function): + self.valueChanged.connect(lambda: change_event_function(self)) diff --git a/gui/widgets/label.py b/gui/widgets/label.py new file mode 100644 index 0000000..a0ce233 --- /dev/null +++ b/gui/widgets/label.py @@ -0,0 +1,9 @@ +from PyQt5.QtWidgets import QLabel + + +class Label(QLabel): + def __init__(self, label): + super(QLabel, self).__init__(label) + + def on_change(self, change_event_function): + pass diff --git a/gui/widgets/line_edit.py b/gui/widgets/line_edit.py new file mode 100644 index 0000000..a87a7b6 --- /dev/null +++ b/gui/widgets/line_edit.py @@ -0,0 +1,12 @@ +from PyQt5.QtWidgets import QLineEdit + + +class LineEdit(QLineEdit): + def __init__(self): + super(QLineEdit, self).__init__() + + def get_value(self, ql_type): + return ql_type(self.text()) + + def on_change(self, change_event_function): + self.textChanged.connect(lambda: change_event_function(self)) diff --git a/gui/widgets/money_spinbox.py b/gui/widgets/money_spinbox.py new file mode 100644 index 0000000..680a432 --- /dev/null +++ b/gui/widgets/money_spinbox.py @@ -0,0 +1,15 @@ +from PyQt5.QtWidgets import QDoubleSpinBox + + +class MoneySpinbox(QDoubleSpinBox): + def __init__(self): + super(QDoubleSpinBox, self).__init__() + self.setPrefix('$') + self.setMaximum(2**31 - 1) + self.setMinimum(-self.maximum()) + + def get_value(self, ql_type): + return ql_type(self.value(), self.prefix()) + + def on_change(self, change_event_function): + self.valueChanged.connect(lambda: change_event_function(self)) diff --git a/gui/widgets/spinbox.py b/gui/widgets/spinbox.py new file mode 100644 index 0000000..1abb63a --- /dev/null +++ b/gui/widgets/spinbox.py @@ -0,0 +1,14 @@ +from PyQt5.QtWidgets import QSpinBox + + +class SpinBox(QSpinBox): + def __init__(self): + super(QSpinBox, self).__init__() + self.setMaximum(2**31 - 1) + self.setMinimum(-self.maximum()) + + def get_value(self, ql_type): + return ql_type(self.value()) + + def on_change(self, change_event_function): + self.valueChanged.connect(lambda: change_event_function(self)) diff --git a/gui/windows/__pycache__/form.cpython-35.pyc b/gui/windows/__pycache__/form.cpython-35.pyc new file mode 100644 index 0000000..d04e617 Binary files /dev/null and b/gui/windows/__pycache__/form.cpython-35.pyc differ diff --git a/gui/windows/__pycache__/main.cpython-35.pyc b/gui/windows/__pycache__/main.cpython-35.pyc new file mode 100644 index 0000000..3e7dec3 Binary files /dev/null and b/gui/windows/__pycache__/main.cpython-35.pyc differ diff --git a/gui/windows/form.py b/gui/windows/form.py new file mode 100644 index 0000000..64c05f8 --- /dev/null +++ b/gui/windows/form.py @@ -0,0 +1,68 @@ +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QDialog +from PyQt5.QtWidgets import QDialogButtonBox +from PyQt5.QtWidgets import QFileDialog +from PyQt5.QtWidgets import QFormLayout +from PyQt5.QtWidgets import QGroupBox +from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtWidgets import QVBoxLayout + +from gui.helper import append_file_extension + + +class FormWindow(QDialog): + def __init__(self, form_model): + super(FormWindow, self).__init__() + self.__form_model = form_model + self.__form_group_box = QGroupBox() + + self.setWindowTitle('Form') + self.create_form() + + button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + + main_layout = QVBoxLayout() + main_layout.addWidget(self.form_group_box) + main_layout.addWidget(button_box) + self.setLayout(main_layout) + + @property + def form_model(self): + return self.__form_model + + @property + def form_group_box(self): + return self.__form_group_box + + def create_form(self): + layout = QFormLayout() + + for question in self.form_model.block: + show = question.evaluate_visibility_condition(self.form_model) + question.pyqt5_render(layout, self.form_model, show) + question.widget.on_change(self.form_model.update_questions_on_change) + + self.form_group_box.setLayout(layout) + + @pyqtSlot(name='accept') + def accept(self): + file_name, _ = QFileDialog.getSaveFileName(QFileDialog(), 'Save results', self.form_model.identifier, + 'JSON (*.json);;All Files (*)') + + if file_name: + file_name = append_file_extension(file_name, 'json') + file = open(file_name, 'w') + file.write(self.form_model.to_json()) + file.close() + self.close() + QMessageBox.information(QMessageBox(), 'Submission', 'Your answers have been submitted successfully.', + QMessageBox.Close, QMessageBox.Escape) + else: + QMessageBox.warning(QMessageBox(), 'Warning', 'Questionnaire results were not saved.', + QMessageBox.Close, QMessageBox.Escape) + + @pyqtSlot(name='reject') + def reject(self): + self.hide() diff --git a/gui/windows/main.py b/gui/windows/main.py new file mode 100644 index 0000000..4dc4ef2 --- /dev/null +++ b/gui/windows/main.py @@ -0,0 +1,139 @@ +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QAction +from PyQt5.QtWidgets import QFileDialog +from PyQt5.QtWidgets import QMainWindow +from PyQt5.QtWidgets import QTextEdit + +from gui.helper import append_file_extension +from gui.helper import check_errors +from gui.helper import warning +from gui.windows.form import FormWindow +from ql.ast.checkers.dependency_checker import DependencyChecker +from ql.ast.checkers.question_checker import QuestionChecker +from ql.ast.checkers.reference_checker import ReferenceChecker +from ql.ast.extractors.extractor import extract_gui_model +from ql.ast.extractors.extractor import extract_identifier_dependencies +from ql.ast.extractors.extractor import extract_identifier_scopes +from ql.ast.extractors.extractor import extract_identifier_types +from ql.ast.extractors.extractor import extract_questions +from ql.ast.visitors.type_visitor import TypeVisitor +from ql.parser.lexer import QLLexer +from ql.parser.parser import QLParser + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + self.setWindowTitle('PyQuest') + self.current_file = None + self.create_menu_bar() + self.text_edit = QTextEdit() + self.setCentralWidget(self.text_edit) + + self.show() + + def create_menu_bar(self): + menu_bar = self.menuBar() + + new_file = QAction(QIcon(), 'New', self) + new_file.setShortcut('Ctrl+N') + new_file.setStatusTip('New file') + new_file.triggered.connect(self.new_file) + + open_file = QAction(QIcon(), 'Open', self) + open_file.setShortcut('Ctrl+O') + open_file.setStatusTip('Open file') + open_file.triggered.connect(self.open_file) + + save = QAction(QIcon(), 'Save', self) + save.setShortcut('Ctrl+S') + save.setStatusTip('Save file') + save.triggered.connect(self.save) + + save_file = QAction(QIcon(), 'Save as', self) + save_file.setShortcut('Ctrl+Shift+S') + save_file.setStatusTip('Save file as') + save_file.triggered.connect(self.save_file) + + create_form = QAction(QIcon(), 'Create form', self) + create_form.setShortcut('Ctrl+R') + create_form.setStatusTip('Create form') + create_form.triggered.connect(self.create_form) + + file_menu = menu_bar.addMenu('&File') + file_menu.addAction(new_file) + file_menu.addAction(open_file) + file_menu.addAction(save) + file_menu.addAction(save_file) + + create_menu = menu_bar.addMenu('&Create') + create_menu.addAction(create_form) + + def new_file(self): + self.current_file = None + self.text_edit.setText('') + + def open_file(self): + self.current_file, _ = QFileDialog.getOpenFileName(caption='Open file', directory='/home', + filter="Questionnaire language files (*.ql)") + + if self.current_file: + with open(self.current_file, 'r') as file: + self.text_edit.setText(file.read()) + + def save(self): + if self.current_file: + with open(self.current_file, 'w') as file: + file.write(self.text_edit.toPlainText()) + else: + self.save_file() + + def save_file(self): + file_name, _ = QFileDialog.getSaveFileName(caption='Save file as', directory='/home', + filter='Questionnaire language files (*.ql)') + + if file_name: + self.current_file = append_file_extension(file_name, 'ql') + with open(self.current_file, 'w') as file: + file.write(self.text_edit.toPlainText()) + + def create_form(self): + textbox_value = self.text_edit.toPlainText() + ql_lexer = QLLexer() + ql_lexer.build() + ql_parser = QLParser() + ql_parser.build() + empty_form = [] + + if not textbox_value: + empty_form.append('Empty Form') + + try: + check_errors(empty_form) + ast = ql_parser.parse(textbox_value, ql_lexer.lexer) + parse_errors = ql_parser.errors + + check_errors(parse_errors) + reference_errors = ReferenceChecker(extract_identifier_scopes(ast)).errors + + check_errors(reference_errors) + dependency_errors = DependencyChecker(extract_identifier_dependencies(ast)).errors + + check_errors(dependency_errors) + question_errors = QuestionChecker(extract_questions(ast)).errors + question_warnings = QuestionChecker(extract_questions(ast)).warnings + + if question_warnings: + warning(question_warnings) + + check_errors(question_errors) + type_visitor = TypeVisitor(extract_identifier_types(ast)) + type_visitor.visit(ast) + type_errors = type_visitor.errors + + check_errors(type_errors), + dialog = FormWindow(extract_gui_model(ast)) + dialog.exec_() + except SyntaxError: + print('Consult application popup error window for information on what went wrong.') -- cgit v1.2.3