aboutsummaryrefslogtreecommitdiff
path: root/gui
diff options
context:
space:
mode:
Diffstat (limited to 'gui')
-rw-r--r--gui/__pycache__/helper.cpython-35.pycbin0 -> 984 bytes
-rw-r--r--gui/helper.py23
-rw-r--r--gui/model/__pycache__/form.cpython-35.pycbin0 -> 2007 bytes
-rw-r--r--gui/model/__pycache__/question.cpython-35.pycbin0 -> 3248 bytes
-rw-r--r--gui/model/form.py44
-rw-r--r--gui/model/question.py83
-rw-r--r--gui/widgets/__pycache__/calendar.cpython-35.pycbin0 -> 1142 bytes
-rw-r--r--gui/widgets/__pycache__/checkbox.cpython-35.pycbin0 -> 1043 bytes
-rw-r--r--gui/widgets/__pycache__/double_spinbox.cpython-35.pycbin0 -> 1209 bytes
-rw-r--r--gui/widgets/__pycache__/label.cpython-35.pycbin0 -> 702 bytes
-rw-r--r--gui/widgets/__pycache__/line_edit.cpython-35.pycbin0 -> 1038 bytes
-rw-r--r--gui/widgets/__pycache__/money_spinbox.cpython-35.pycbin0 -> 1216 bytes
-rw-r--r--gui/widgets/__pycache__/spinbox.cpython-35.pycbin0 -> 1133 bytes
-rw-r--r--gui/widgets/calendar.py13
-rw-r--r--gui/widgets/checkbox.py12
-rw-r--r--gui/widgets/double_spinbox.py15
-rw-r--r--gui/widgets/label.py9
-rw-r--r--gui/widgets/line_edit.py12
-rw-r--r--gui/widgets/money_spinbox.py15
-rw-r--r--gui/widgets/spinbox.py14
-rw-r--r--gui/windows/__pycache__/form.cpython-35.pycbin0 -> 2762 bytes
-rw-r--r--gui/windows/__pycache__/main.cpython-35.pycbin0 -> 4832 bytes
-rw-r--r--gui/windows/form.py68
-rw-r--r--gui/windows/main.py139
24 files changed, 447 insertions, 0 deletions
diff --git a/gui/__pycache__/helper.cpython-35.pyc b/gui/__pycache__/helper.cpython-35.pyc
new file mode 100644
index 0000000..ee2c6ac
--- /dev/null
+++ b/gui/__pycache__/helper.cpython-35.pyc
Binary files 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
--- /dev/null
+++ b/gui/model/__pycache__/form.cpython-35.pyc
Binary files 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
--- /dev/null
+++ b/gui/model/__pycache__/question.cpython-35.pyc
Binary files 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
--- /dev/null
+++ b/gui/widgets/__pycache__/calendar.cpython-35.pyc
Binary files 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
--- /dev/null
+++ b/gui/widgets/__pycache__/checkbox.cpython-35.pyc
Binary files 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
--- /dev/null
+++ b/gui/widgets/__pycache__/double_spinbox.cpython-35.pyc
Binary files 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
--- /dev/null
+++ b/gui/widgets/__pycache__/label.cpython-35.pyc
Binary files 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
--- /dev/null
+++ b/gui/widgets/__pycache__/line_edit.cpython-35.pyc
Binary files 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
--- /dev/null
+++ b/gui/widgets/__pycache__/money_spinbox.cpython-35.pyc
Binary files 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
--- /dev/null
+++ b/gui/widgets/__pycache__/spinbox.cpython-35.pyc
Binary files 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
--- /dev/null
+++ b/gui/windows/__pycache__/form.cpython-35.pyc
Binary files 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
--- /dev/null
+++ b/gui/windows/__pycache__/main.cpython-35.pyc
Binary files 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.')