diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a44111ecf75ba7e489250f88e1343f8794983603..cc4d80dde01a0addf65b1df34876b52c93cf913f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,6 +5,8 @@ stages: job_lint_flake8: stage: lint + tags: + - ci-ubuntu-executor-docker image: morkolai/soitool-ci script: - flake8 --version @@ -12,6 +14,8 @@ job_lint_flake8: job_lint_pylint: stage: lint + tags: + - ci-ubuntu-executor-docker image: morkolai/soitool-ci script: - pylint --version @@ -19,6 +23,8 @@ job_lint_pylint: job_lint_bandit: stage: lint + tags: + - ci-ubuntu-executor-docker image: morkolai/soitool-ci script: - bandit --version @@ -26,6 +32,8 @@ job_lint_bandit: job_lint_pydocstyle: stage: lint + tags: + - ci-ubuntu-executor-docker image: morkolai/soitool-ci script: - pydocstyle --version @@ -33,6 +41,8 @@ job_lint_pydocstyle: job_test_gui_ubuntu_vnc: stage: test + tags: + - ci-ubuntu-executor-docker image: morkolai/soitool-ci script: # -platform because running with a screen is not supported @@ -57,6 +67,8 @@ job_test_gui_ubuntu: job_pages_smoke_test: stage: deploy + tags: + - ci-ubuntu-executor-docker image: morkolai/soitool-ci script: - mkdir public @@ -70,6 +82,8 @@ job_pages_smoke_test: # name has to be pages pages: stage: deploy + tags: + - ci-ubuntu-executor-docker image: morkolai/soitool-ci script: - mkdir public diff --git a/requirements.txt b/requirements.txt index a3389fd958e487a8943bc582d8ebdc57329a4bf3..45f849a7607dc162721fd3fa1218599d16f66819 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/scripts/.pylintrc b/scripts/.pylintrc index 01e79b9dda28b3eefb8acdaa22c29477f31ae2b2..6262e5ddd5bbbd19ecdd7bd226c74675c1477091 100644 --- a/scripts/.pylintrc +++ b/scripts/.pylintrc @@ -286,7 +286,7 @@ indent-after-paren=4 indent-string=' ' # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=79 # Maximum number of lines in a module. max-module-lines=1000 @@ -404,6 +404,8 @@ function-naming-style=snake_case # Good variable names which should always be accepted, separated by a comma. good-names=app, + x, + y, ok, i, j, @@ -503,7 +505,7 @@ valid-metaclass-classmethod-first-arg=cls max-args=5 # Maximum number of attributes for a class (see R0902). -max-attributes=7 +max-attributes=15 # Maximum number of boolean expressions in an if statement. max-bool-expr=5 diff --git a/soitool/main_window.py b/soitool/main_window.py index 042f25a93784dabad3bd5a6a6bb8dd72f517211c..1adeebd03878548824c404683c4d025bd41f8c33 100644 --- a/soitool/main_window.py +++ b/soitool/main_window.py @@ -6,11 +6,13 @@ out to separate modules. """ import sys import os -from PySide2.QtCore import Qt +from PySide2.QtCore import QRectF, QPoint, QTimer, Qt from PySide2.QtWidgets import QTabWidget, QWidget, QMainWindow, \ QApplication, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, \ - QAbstractItemView, QListWidget, QListWidgetItem, QAction -from PySide2.QtGui import QIcon + QAbstractItemView, QListWidget, QListWidgetItem, QAction, QGraphicsScene, \ + QGraphicsView, QScrollArea, QGraphicsRectItem +from PySide2.QtGui import QBrush, QIcon, QPalette, QFont, QPixmap +from soitool.modules.module_table import TableModule class MainWindow(QMainWindow): @@ -110,6 +112,8 @@ class SOIWorkspaceWidget(QWidget): def __init__(self): super().__init__() + self.soi = SOI() + self.layout_wrapper = QHBoxLayout() self.layout_sidebar = QVBoxLayout() @@ -119,7 +123,9 @@ class SOIWorkspaceWidget(QWidget): self.button_new_module.setStatusTip("Legg til en ny modul") self.button_setup = QPushButton("Oppsett") self.list_modules = QListWidget() - self.view = ViewArea() + self.view = InlineEditableSOIView(self.soi) + self.widget_sidebar = QWidget() + self.widget_sidebar.setFixedWidth(200) # prepare module list self.setup_list_modules() @@ -132,10 +138,10 @@ class SOIWorkspaceWidget(QWidget): self.layout_sidebar.addWidget(QListWidget(), 1) self.layout_sidebar.addWidget(self.button_new_module) self.layout_sidebar.addWidget(self.button_setup) - self.layout_wrapper.addLayout(self.layout_sidebar) + self.widget_sidebar.setLayout(self.layout_sidebar) + self.layout_wrapper.addWidget(self.widget_sidebar) self.layout_wrapper.addWidget(self.view) - self.setFixedWidth(200) self.setLayout(self.layout_wrapper) def setup_list_modules(self): @@ -168,19 +174,315 @@ class SOIWorkspaceWidget(QWidget): self.list_modules.insertItem(i, item) -class ViewArea(QWidget): - """Widget som kan byttes ut med view, edit etc.""" +class SOI(): + """Temporary representation of SOI. + + Holds all info about an SOI necessary to view and edit it. + """ + + A4_RATIO = 1.414 + + # height must be adjusted to something that will look right when real + # wigets are placed inside the pages + HEIGHT = 1700 + WIDTH = HEIGHT * A4_RATIO + PADDING = 100 + CONTENT_WIDTH = WIDTH - PADDING * 2 + CONTENT_HEIGHT = HEIGHT - PADDING * 2 + HEADER_HEIGHT = 100 + MODULE_PADDING = 10 def __init__(self): + + # NOTE + # * test modules, just to have something show up on screen + # * not valid until reorganize has been run, as the positions must be + # updated + self.modules = [ + { + "widget": TableModule(), + "meta": { + "x": 0, + "y": 0, + "page": 1 + } + }, + { + "widget": TableModule(), + "meta": { + "x": 0, + "y": 0, + "page": 1 + } + }, + { + "widget": TableModule(), + "meta": { + "x": 0, + "y": 0, + "page": 2 + } + } + ] + + self.reorganize() + + def update_module_widget_position(self, module): + """Update position of module widget based on meta position. + + This function is very much WIP.. + + Parameters + ---------- + module : see description + should be dict of fields "meta" and "widget", where "meta" is + itself a dict with fields "x", "y" and "page", and "widget" is a + widget based on "ModuleBase" + """ + distance_to_start_of_next_soi_content_y = self.CONTENT_HEIGHT + \ + self.PADDING * 2 + self.HEADER_HEIGHT + + scene_skip_distance_page_height = \ + distance_to_start_of_next_soi_content_y * \ + (module["meta"]["page"] - 1) + + new_x = module["meta"]["x"] + self.PADDING + new_y = module["meta"]["y"] + self.PADDING + self.HEADER_HEIGHT + \ + scene_skip_distance_page_height + + module["widget"].set_pos(QPoint(new_x, new_y)) + + def reorganize(self): + """Update x,y positions of modules. + + In the future this is where rectpack will do it's magic, for now it is + a WIP just to get some kind of positioning working. The positioning + scheme for now is as follows: for each page 1 and 2, position widgets + next to each other from left to right + """ + x = self.MODULE_PADDING + first_page_modules = [module for module in self.modules + if module["meta"]["page"] == 1] + + for module in first_page_modules: + module["meta"]["x"] = x + module["meta"]["y"] = self.MODULE_PADDING + + self.update_module_widget_position(module) + + widget_width, _ = module["widget"].get_size() + x = x + self.MODULE_PADDING + widget_width + + # NOTE the following is simply duplicated.. left like this to KISS + # will be replaced by rectpack anyways + x = self.MODULE_PADDING + second_page_modules = [module for module in self.modules + if module["meta"]["page"] == 2] + for module in second_page_modules: + module["meta"]["x"] = x + module["meta"]["y"] = self.MODULE_PADDING + + self.update_module_widget_position(module) + + widget_width, _ = module["widget"].get_size() + x = x + self.MODULE_PADDING + widget_width + + +class InlineEditableSOIView(QScrollArea): + """Widget som kan byttes ut med view, edit etc.""" + + def is_widget_in_scene(self, widget): + """Indicate wether given widget already has a proxy in the scene.""" + for page in self.pages: + for proxy in page: + if proxy.widget() == widget: + return True + return False + + def setup_proxies(self): + """Set up proxies for the widgets of SOI modules.""" + for module in self.soi.modules: + if not self.is_widget_in_scene(module["widget"]): + proxy = self.scene.addWidget(module["widget"]) + + while len(self.pages) < module["meta"]["page"]: + self.pages.append(set()) + + self.pages[module["meta"]["page"] - 1].add(proxy) + + def mousePressEvent(self, _): + """Reorganize modules when pressed. + + This is a temporary way to activate reorganization of widgets. Note + that will not be triggered by clicks on modules in the scene + """ + self.soi.reorganize() + + def __init__(self, soi): super().__init__() - test = QLabel("Test") - layout = QHBoxLayout() - layout.addWidget(test) - self.setLayout(layout) + + self.soi = soi + + self.pages = [] + + # necessary to make the scroll area fill the space it's given + self.setWidgetResizable(True) + + # scene and view widgets + self.scene = QGraphicsScene() + self.setup_scene() + self.view = QGraphicsView(self.scene) + + self.setWidget(self.view) + + self.setup_proxies() + + self.update_drawn_pages() + + # self.launch_auto_zoom() + + def update_drawn_pages(self): + """Update drawn pages to reflect self.pages.""" + for i, modules in enumerate(self.pages, start=1): + + x = 0 + y = self.soi.HEIGHT * max(i - 1, 0) + \ + self.soi.PADDING * max(i - 1, 0) + + # adjust page size + full_scene_height = y + self.soi.HEIGHT + self.scene.setSceneRect(QRectF(0, 0, self.soi.WIDTH, + full_scene_height)) + + self.draw_page(x, y) + self.draw_header(x + self.soi.PADDING, y + self.soi.PADDING, i) + + for module in modules: + # redraw of pages requires modules to be moved to front again + module.setZValue(1) + + def draw_header(self, x, y, page_number): + """Draw header staring at given position. + + Parameters + ---------- + x : int + y : int + """ + # title + label_title = QLabel("SOI TITLE HERE") + label_title.move(x, y) + label_title.setStyleSheet("background-color: rgba(0,0,0,0%)") + label_title.setFont(QFont("Times New Roman", 50)) + self.scene.addWidget(label_title) + + # page numbering + page_number = QLabel("{} av {}".format(page_number, len(self.pages))) + page_number.setStyleSheet("background-color: rgba(0,0,0,0%)") + page_number.setFont(QFont("Times New Roman", 50)) + # source: https://stackoverflow.com/a/8638114/3545896 + # CAUTION: does not work if font is set through stylesheet + label_width = \ + page_number.fontMetrics().boundingRect(page_number.text()).width() + page_number.move(x + (self.soi.CONTENT_WIDTH - label_width) / 2, y) + self.scene.addWidget(page_number) + + # grading + grading = QLabel("UGRADERT") + grading.setStyleSheet("background-color: rgba(0,0,0,0%); color: red") + grading.setFont(QFont("Times New Roman", 50)) + # source: https://stackoverflow.com/a/8638114/3545896 + # CAUTION: does not work if font is set through stylesheet + label_width = \ + grading.fontMetrics().boundingRect(grading.text()).width() + x_pos = x + self.soi.CONTENT_WIDTH - label_width - \ + self.soi.HEADER_HEIGHT + grading.move(x_pos, y) + self.scene.addWidget(grading) + + # patch + pixmap = QPixmap("soitool/media/HVlogo.png") + patch = QLabel() + patch.setPixmap(pixmap.scaled(self.soi.HEADER_HEIGHT, + self.soi.HEADER_HEIGHT)) + patch.move(x + self.soi.CONTENT_WIDTH - self.soi.HEADER_HEIGHT, y) + self.scene.addWidget(patch) + + def draw_page(self, x, y): + """Draw page starting at given position. + + Parameters + ---------- + x : int + y : int + """ + # color the page white + page_background = QGraphicsRectItem(x, y, self.soi.WIDTH, + self.soi.HEIGHT) + page_background.setBrush(QBrush(Qt.white)) + self.scene.addItem(page_background) + + # draw borders + self.scene.addRect(x, y, self.soi.WIDTH, self.soi.HEIGHT) + self.scene.addRect(x + self.soi.PADDING, y + self.soi.PADDING, + self.soi.CONTENT_WIDTH, self.soi.CONTENT_HEIGHT) + self.scene.addRect(x + self.soi.PADDING, y + self.soi.PADDING, + self.soi.CONTENT_WIDTH, self.soi.CONTENT_HEIGHT) + self.scene.addRect(x + self.soi.PADDING, y + self.soi.PADDING, + self.soi.CONTENT_WIDTH, self.soi.HEADER_HEIGHT) + + def setup_scene(self): + """Prepare scene for use. + + Draws borders, background, etc. + """ + # sets the background color of the scene to be the appropriate + # system-specific background color + # source: https://stackoverflow.com/a/23880531/3545896 + # source: https://stackoverflow.com/questions/15519749/how-to-get-widget-background-qcolor + self.scene.setBackgroundBrush(app.palette().color(QPalette.Window)) + + self.update_drawn_pages() + + def launch_auto_zoom(self): + """Zoom in a regular interval. + + Used to demonstrate zooming only, should be removed once the project + matures. + """ + def do_on_timeout(): + self.zoom(1 / 1.00005) + + timer = QTimer(self) + timer.timeout.connect(do_on_timeout) + timer.start(10) + + def zoom(self, zoom_factor): + """Zoom GraphicsView by zoom_factor. + + source: https://stackoverflow.com/a/29026916/3545896 + + Parameters + ---------- + zoom_factor : float + > 1 to zoom in and < 1 to zoom out + """ + # Set Anchors + self.view.setTransformationAnchor(QGraphicsView.NoAnchor) + self.view.setResizeAnchor(QGraphicsView.NoAnchor) + + # Zoom + self.view.scale(zoom_factor, zoom_factor) + + pos_old = self.view.mapToScene(QPoint(self.soi.WIDTH / 2, + self.soi.HEIGHT / 2)) + pos_new = self.view.mapToScene(self.soi.WIDTH / 2, self.soi.HEIGHT / 2) + delta = pos_new - pos_old + self.view.translate(delta.x(), delta.y()) if __name__ == "__main__": app = QApplication(sys.argv) WINDOW = MainWindow() - WINDOW.show() + WINDOW.showMaximized() app.exec_() diff --git a/soitool/modules/module_base.py b/soitool/modules/module_base.py index 92e7e6bc6145a1a1921cb5580c7b8384d77cad51..630cdaeb4c5e7b982b52b49413cbde406abedb74 100644 --- a/soitool/modules/module_base.py +++ b/soitool/modules/module_base.py @@ -9,6 +9,10 @@ class ModuleBase(ABC): """Abstract method, should be implemented by derived class.""" raise NotImplementedError + def set_pos(self, pos): + """Abstract method, should be implemented by derived class.""" + raise NotImplementedError + def render_onto_pdf(self): """Abstract method, should be implemented by derived class.""" raise NotImplementedError diff --git a/soitool/modules/module_table.py b/soitool/modules/module_table.py index 6ddf18b4645cd7e18294adb1bc9d518676b0e39b..e9d06a7502c15d4d859c0173eb39ccc7de5dd481 100644 --- a/soitool/modules/module_table.py +++ b/soitool/modules/module_table.py @@ -27,6 +27,10 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta): Has shortcuts for adding and removing rows and columns. """ + def set_pos(self, pos): + """Set position of widget.""" + self.move(pos) + def __init__(self): QTableWidget.__init__(self) super(QTableWidget) diff --git a/test/test_main.py b/test/test_main.py index beadccfe5991936ceba67b155d69db315ddf1414..4f70f155c05aa9d0baf3e7f0143110aa4e3e7981 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -59,13 +59,19 @@ class TestMain(unittest.TestCase): """Test at endring av tekst funker.""" def change_text_and_ok(): - while self.widget.dlg_input is None: - app.processEvents() - child_line_edit = self.widget.dlg_input.findChild(QtWidgets.QLineEdit) + # in PySide2 we need to store a reference to this. If we don't the + # widget is garbage collected somehow before we get to use + # child_line_edit (a child of the active widget) + active_widget = app.activeModalWidget() + + child_line_edit = active_widget.findChild( + QtWidgets.QLineEdit + ) + QtTest.QTest.keyClicks(child_line_edit, self.test_text2) QtTest.QTest.keyClick(child_line_edit, QtCore.Qt.Key_Enter) - QtCore.QTimer.singleShot(100, change_text_and_ok) + QtCore.QTimer.singleShot(0, change_text_and_ok) QtTest.QTest.mouseClick(self.widget.button, QtCore.Qt.LeftButton) self.assertEqual(