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/Dockerfile b/Dockerfile
index 0ba5c5605dc81669c396b2cc9f78f6538410168d..d4420c91c03b1d3436a69ceeb553db22250e6a8a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -41,3 +41,11 @@ ENV LC_ALL en_US.UTF-8
 # From https://it.i88.ca/2016/03/how-to-fix-importerror-libgssapikrb5so2.html
 # python3.7 -m unittest fails without this
 RUN apt-get -y install libgssapi-krb5-2
+
+# Necessary for import of PySide2.QtWebEngineWidgets to work
+RUN apt-get -y install libnss3 \
+                       libxcomposite-dev \
+                       libxcursor-dev \
+                       libxi-dev \
+                       libxtst-dev \
+                       libxrandr-dev
diff --git a/README.md b/README.md
index 5abde24ededc26891550f89aa9664f7ee9f19f30..1798acb96dcfa16bf291854cd340a0410431b769 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ deactivate
 
 Sjekk av kodekvalitet gjøres med scriptet "CodeQualityCheck" i script-mappa, og er skrevet i Powershell(.ps1) og Bash(.sh). Scriptet kjører Pylint og Flake8 med tilhørende konfigurasjonsfiler, Pydocstyle med numpy-konvensjon og Bandit på Python-filer. 
 
-Scriptet godtar kommandolinjeargumenter: .py-fil(er), mappe(r ) eller en blanding av disse. Uten argumenter vil scriptet sjekke alle .py-filer.
+Scriptet godtar kommandolinjeargumenter: .py-fil(er), mappe(r) eller en blanding av disse. Uten argumenter vil scriptet sjekke alle .py-filer.
 
 Terminal kjøres fra root. Sjekk:
 
@@ -39,7 +39,7 @@ Terminal kjøres fra root. Sjekk:
 
 * Spesifikk(e) .py: `.\scripts\CodeQualityCheck.ps1 filEn.py filTo.py`
 
-* Alle .py i mappe(r ): `.\scripts\CodeQualityCheck.ps1 mappeEn mappeTo`
+* Alle .py i mappe(r): `.\scripts\CodeQualityCheck.ps1 mappeEn mappeTo`
 
 * Blanding: `.\scripts\CodeQualityCheck.ps1 mappeEn mappeTo\fil.py`
 
@@ -59,6 +59,8 @@ Gjøres med pdoc3:
 
 * Uten kildekode: `pdoc3 --html --config show_source_code=False --output-dir .\docs\ .\soitool\main.py`
 
+Autogenerert docs: http://bachelor-paa-bittet.pages.stud.idi.ntnu.no/soitool/
+
 ## Om `Dockerfile`
 
 Docker image som brukes i `.gitlab-ci.yml` er bygget med filen `Dockerfile` og er lastet opp som `morkolai/soitool-ci`. Docker image inneholder alle avhengigheter til prosjektet. Følgende prosedyre brukes for å oppdatere image. Dette må gjøres når `requirements.txt` endrer seg.
diff --git a/scripts/.flake8 b/scripts/.flake8
index de75a936c7acec77595ab58ce170803ae7205cd2..f6c974480c4192afdbffa9c58c1b72a51d49339e 100644
--- a/scripts/.flake8
+++ b/scripts/.flake8
@@ -2,4 +2,4 @@
 ; ignorerer sjekk for lange linjer, ettersom dette gjøres i pylint
 ; vi vil tillate lange linjer som avsluttes med link, og dette er mulig i
 ; pylint, men såvidt vi vet ikke mulig i flake8
-ignore = E501
+ignore = E501,W503
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/__init__.py b/soitool/__init__.py
index b5bf2de2219cec4d93e0b5e67629b24b5c8028b1..ab3061e1810d86c58eba45869272f7d921e541d4 100644
--- a/soitool/__init__.py
+++ b/soitool/__init__.py
@@ -1 +1 @@
-"""SOI - Tool: Verktøy for å produsere SOIer."""
+"""SOI - Tool: A tool to produce SOI."""
diff --git a/soitool/inline_editable_soi_view.py b/soitool/inline_editable_soi_view.py
new file mode 100644
index 0000000000000000000000000000000000000000..e49d3dc369e522aa096701a59e58185ebbd4733d
--- /dev/null
+++ b/soitool/inline_editable_soi_view.py
@@ -0,0 +1,250 @@
+"""Includes functionality for inline editing of SOI."""
+from PySide2.QtCore import Qt, QRectF, QTimer, QPoint, QMarginsF
+from PySide2.QtWidgets import QApplication, QScrollArea, QLabel, \
+    QGraphicsScene, QGraphicsView, QGraphicsRectItem
+from PySide2.QtGui import QFont, QPixmap, QBrush, QPalette, QPainter
+from PySide2.QtPrintSupport import QPrinter
+
+
+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__()
+
+        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 produce_pdf(self, filename):
+        """Produce PDF using QGraphicsScene.
+
+        Renders the QGraphicsScene-representation of the SOI as a PDF. This
+        PDF is in theory supposed to be searchable [1], but in practice it
+        seems that each QGraphicsItem in the scene is simply dumped as an
+        image.
+
+        Sources:
+        [1]: https://doc.qt.io/qt-5/qprinter.html#OutputFormat-enum
+
+        Parameters
+        ----------
+        filename : str
+            Name of file to store PDF in. If file exists it will be
+            overwritten. Note that the filename should contain the extension
+            '.pdf' to be properly handled by operating systems.
+        """
+        printer = QPrinter(QPrinter.HighResolution)
+        printer.setOutputFormat(QPrinter.PdfFormat)
+        printer.setOutputFileName(filename)
+        printer.setPageSize(QPrinter.A4)
+        printer.setOrientation(QPrinter.Landscape)
+        printer.setPageMargins(QMarginsF(0, 0, 0, 0))
+
+        painter = QPainter()
+
+        try:
+            ok = painter.begin(printer)
+
+            if not ok:
+                raise ValueError("Not able to begin QPainter using QPrinter "
+                                 "based on argument "
+                                 "filename '{}'".format(filename))
+
+            # render each page to own PDF page
+            for i, modules in enumerate(self.pages):
+
+                x = 0
+                y = self.soi.HEIGHT * i + self.soi.PADDING * i
+
+                self.scene.render(painter, source=QRectF(x, y,
+                                                         self.soi.WIDTH,
+                                                         self.soi.HEIGHT))
+
+                # if there are more pages, newPage
+                if i + 1 < len(self.pages):
+                    printer.newPage()
+        finally:
+            painter.end()
+
+    def update_drawn_pages(self):
+        """Update drawn pages to reflect self.pages."""
+        for i, modules in enumerate(self.pages):
+
+            x = 0
+            y = self.soi.HEIGHT * i + self.soi.PADDING * i
+
+            # 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 + 1)
+
+            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
+        app = QApplication.instance()
+        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())
diff --git a/soitool/main_window.py b/soitool/main_window.py
index 042f25a93784dabad3bd5a6a6bb8dd72f517211c..275b1e903d801526c709fda2e34788a977dd6fdb 100644
--- a/soitool/main_window.py
+++ b/soitool/main_window.py
@@ -1,16 +1,21 @@
-"""MainWindow.
+"""Inclues the main window of our application.
+
+Built up by widgets implemented in other modules.
 
-During initial development this module will include most of the code necessary
-to get our project up-and-running. When our project matures we will move code
-out to separate modules.
 """
 import sys
 import os
-from PySide2.QtCore import Qt
-from PySide2.QtWidgets import QTabWidget, QWidget, QMainWindow, \
-    QApplication, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, \
-    QAbstractItemView, QListWidget, QListWidgetItem, QAction
+from enum import Enum
+from PySide2.QtWidgets import QMainWindow, QApplication, QTabWidget, QAction
 from PySide2.QtGui import QIcon
+from soitool.soi_workspace_widget import SOIWorkspaceWidget
+
+
+class ModuleType(Enum):
+    """Enumerate with types of modules."""
+
+    MAIN_MODULE = 0
+    ATTACHMENT_MODULE = 1
 
 
 class MainWindow(QMainWindow):
@@ -87,11 +92,11 @@ class MainWindow(QMainWindow):
         help_menu.addAction(easy_use)
 
         # Legger til MainWidget som en tab, kanskje flytt ut til egen funksjon
-        tabs = QTabWidget()
+        self.tabs = QTabWidget()
         tab = SOIWorkspaceWidget()
 
-        tabs.addTab(tab, "MainTab")
-        self.setCentralWidget(tabs)
+        self.tabs.addTab(tab, "MainTab")
+        self.setCentralWidget(self.tabs)
 
         # Add HV logo
         filename = 'media/HVlogo.PNG'
@@ -100,87 +105,8 @@ class MainWindow(QMainWindow):
         self.setWindowIcon(QIcon(filepath))
 
 
-class SOIWorkspaceWidget(QWidget):
-    """Contains the working area for a single SOI.
-
-    The widget is used inside tabs in our application, and contains a sidebar
-    with a module list along with a view of the SOI.
-    """
-
-    def __init__(self):
-        super().__init__()
-
-        self.layout_wrapper = QHBoxLayout()
-        self.layout_sidebar = QVBoxLayout()
-
-        # all widgets
-        self.button_new_module = QPushButton("Ny modul")
-        self.button_new_module.setShortcut("Ctrl+m")
-        self.button_new_module.setStatusTip("Legg til en ny modul")
-        self.button_setup = QPushButton("Oppsett")
-        self.list_modules = QListWidget()
-        self.view = ViewArea()
-
-        # prepare module list
-        self.setup_list_modules()
-        self.fill_list_modules()
-
-        # build layouts
-        self.layout_sidebar.addWidget(QLabel("Moduler:"))
-        self.layout_sidebar.addWidget(self.list_modules, 5)
-        self.layout_sidebar.addWidget(QLabel("Vedlegg:"))
-        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.layout_wrapper.addWidget(self.view)
-
-        self.setFixedWidth(200)
-        self.setLayout(self.layout_wrapper)
-
-    def setup_list_modules(self):
-        """Prepare module list.
-
-        The list contains modules that are drag-and-droppable.
-        """
-        # enable drag-and-drop
-        self.list_modules.setDragEnabled(True)
-        self.list_modules.viewport().setAcceptDrops(True)
-        self.list_modules.viewport().setAcceptDrops(True)
-        self.list_modules.setDragDropMode(QAbstractItemView.InternalMove)
-
-        # source: https://www.qtcentre.org/threads/32500-Horizontal-Scrolling-QListWidget
-        self.list_modules.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-
-    def fill_list_modules(self):
-        """Fill module list with some items to manually test with.
-
-        This function will be removed when we can fill the list properly.
-        """
-        # hardcode some items
-        items = [
-            QListWidgetItem("Frekvenstabell"),
-            QListWidgetItem("Sambandsdiagram"),
-            QListWidgetItem("Autentifiseringstavle"),
-            QListWidgetItem("Subtraktorkoder"),
-        ]
-        for i, item in enumerate(items):
-            self.list_modules.insertItem(i, item)
-
-
-class ViewArea(QWidget):
-    """Widget som kan byttes ut med view, edit etc."""
-
-    def __init__(self):
-        super().__init__()
-        test = QLabel("Test")
-        layout = QHBoxLayout()
-        layout.addWidget(test)
-        self.setLayout(layout)
-
-
 if __name__ == "__main__":
     app = QApplication(sys.argv)
     WINDOW = MainWindow()
-    WINDOW.show()
+    WINDOW.showMaximized()
     app.exec_()
diff --git a/soitool/module_list.py b/soitool/module_list.py
new file mode 100644
index 0000000000000000000000000000000000000000..d8c81252b5a56cb53f6db3f5cf9337c5c2d27270
--- /dev/null
+++ b/soitool/module_list.py
@@ -0,0 +1,152 @@
+"""Includes functionality for displaying a prioritized list of modules."""
+from PySide2.QtCore import Qt
+from PySide2.QtWidgets import QListWidget, QListWidgetItem, QAbstractItemView
+from soitool.soi import ModuleType
+import soitool
+
+
+class ModuleList(QListWidget):
+    """Contains module-names from soitool.SOI.
+
+    List elements are names of modules or attachment-modules from soitool.SOI.
+    List elements are editable, drag-and-droppable and unique (no duplicates).
+    Makes changes in soitool.SOI lists through its parent-variable.
+
+    Parameters
+    ----------
+    module_type : int
+        Determines whether names are from modules or attachment-modules.
+        Is used with the Enum 'ModuleType' to know which list in parent
+        to update when changes are made.
+    parent : soitool.SOIWorkspaceWidget
+        Reference to parent.
+    """
+
+    def __init__(self, module_type, parent):
+        super().__init__()
+
+        # full import path below to avoid circular dependency
+        if not isinstance(parent,
+                          soitool.soi_workspace_widget.SOIWorkspaceWidget):
+            raise RuntimeError('Only soitool.SOIWorkspaceWidget is '
+                               'acceptable type for parent-variable '
+                               'in class Module_list.')
+        self.type = module_type
+        self.parent = parent
+        self.original_element_name = None
+        self.original_element_index = None
+
+        self.setup_list()
+        self.fill_list()
+
+    def setup_list(self):
+        """Prepare list.
+
+        Make list drag-and-droppable and remove horizontal scrollbar.
+        """
+        # Enable drag-and-drop
+        self.setDragEnabled(True)
+        self.viewport().setAcceptDrops(True)
+        self.setDragDropMode(QAbstractItemView.InternalMove)
+
+        # Remove horizontal scrollbar
+        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+
+    def fill_list(self):
+        """Fill list with elements."""
+        # Get names of modules/attachments:
+        if ModuleType(self.type) == ModuleType.MAIN_MODULE:
+            names = [module["meta"]["name"] for
+                     module in self.parent.soi.modules]
+        elif ModuleType(self.type) == ModuleType.ATTACHMENT_MODULE:
+            names = [attachment["meta"]["name"] for
+                     attachment in self.parent.soi.attachments]
+
+        for i, name in enumerate(names):
+            item = QListWidgetItem(name)
+            item.setFlags(item.flags() | Qt.ItemIsEditable)
+            self.insertItem(i, item)
+
+    def currentChanged(self, current, previous):
+        """Save name and index of an element when it is selected.
+
+        Function is needed to remember original:
+        1: index of an element in case it's' drag-and-dropped (notify parent).
+        2: name of an element in case it's name changes (avoid duplicates).
+
+        https://doc.qt.io/qt-5/qlistview.html#currentChanged.
+
+        Parameters
+        ----------
+        current : QModelIndex
+            Used to get index of element, is sent to super().
+        previous : QModelIndex
+            Is sent to super().
+        """
+        super().currentChanged(current, previous)
+        self.original_element_name = self.item(current.row()).text()
+        self.original_element_index = current.row()
+
+    def dataChanged(self, index1, index2, roles):
+        """Check for duplicate name and notify parent when an element changes.
+
+        https://doc.qt.io/qt-5/qabstractitemview.html#dataChanged.
+
+        Parameters
+        ----------
+        index1 : QModelIndex
+            Used to get index of changed element, is sent to super().
+        index2 : QModelIndex
+            Is sent to super().
+        roles : QVector
+            Is sent to super().
+        """
+        index = index1.row()
+        # Get all names and new name
+        names = [self.item(i).text() for i in range(self.count())]
+        new_name = names[index]
+
+        # Count occurrences of new name
+        occurrences = names.count(new_name)
+
+        # If new name already exists, replace with original name
+        if occurrences > 1:
+            self.item(index).setText(self.original_element_name)
+        # Name does not already exist, update list in soi
+        else:
+            name = self.item(index).text()
+
+            if ModuleType(self.type) == ModuleType.MAIN_MODULE:
+                self.parent.soi.modules[index]["meta"]["name"] = name
+            elif ModuleType(self.type) == ModuleType.ATTACHMENT_MODULE:
+                self.parent.soi.attachments[index]["meta"]["name"] = name
+
+        super().dataChanged(index1, index2, roles)
+
+    def dropEvent(self, event):
+        """Notify parent when an element is drag-and-dropped.
+
+        https://doc.qt.io/qt-5/qabstractitemview.html#dropEvent.
+
+        Parameters
+        ----------
+        event : QDropEvent
+            Is sent to super().
+        """
+        super().dropEvent(event)
+
+        # Get origin and destination index of module/attachment
+        origin = self.original_element_index
+        destination = self.currentRow()
+
+        # Update module/attachment priority (order in list):
+        if ModuleType(self.type) == ModuleType.MAIN_MODULE:
+            moving_module = self.parent.soi.modules.pop(origin)
+            self.parent.soi.modules.insert(destination, moving_module)
+        elif ModuleType(self.type) == ModuleType.ATTACHMENT_MODULE:
+            moving_module = self.parent.soi.attachments.pop(origin)
+            self.parent.soi.attachments.insert(destination, moving_module)
+
+        self.parent.soi.reorganize()
+
+        self.original_element_index = self.currentRow()
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..29adbce0881c19645afa7c8e0435eefc6748a197 100644
--- a/soitool/modules/module_table.py
+++ b/soitool/modules/module_table.py
@@ -1,5 +1,5 @@
 """Module containing subclassed SOIModule (QTableWidget, ModuleBase)."""
-from PySide2.QtWidgets import QTableWidget, QTableWidgetItem, QShortcut
+from PySide2.QtWidgets import QTableWidget, QTableWidgetItem
 from PySide2 import QtGui, QtCore
 from soitool.modules.module_base import ModuleBase
 
@@ -27,6 +27,16 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
     Has shortcuts for adding and removing rows and columns.
     """
 
+    def set_pos(self, pos):
+        """Set position of widget.
+
+        Parameters
+        ----------
+        pos : QPoint
+            Position (x, y).
+        """
+        self.move(pos)
+
     def __init__(self):
         QTableWidget.__init__(self)
         super(QTableWidget)
@@ -54,7 +64,31 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
 
         self.cellChanged.connect(self.resize)
 
-        self.set_shortcuts()
+    def keyPressEvent(self, event):
+        """Launch actions when specific combinations of keys are pressed.
+
+        If the keys pressed are not related to a shortcut on this custom widget
+        the event is sent on to be handled by the superclass (for navigation
+        with arrow-keys for.eg.)
+
+        Parameters
+        ----------
+        event : QKeyEvent
+            event sent by Qt for us to handle
+        """
+        if event.key() == QtCore.Qt.Key_Question:
+            self.add_column()
+        elif (event.modifiers() == QtCore.Qt.ShiftModifier
+              and event.key() == QtCore.Qt.Key_Underscore):
+            self.remove_column()
+        elif (event.modifiers() == QtCore.Qt.ControlModifier
+              and event.key() == QtCore.Qt.Key_Plus):
+            self.add_row()
+        elif (event.modifiers() == QtCore.Qt.ControlModifier
+              and event.key() == QtCore.Qt.Key_Underscore):
+            self.remove_row()
+        else:
+            super(TableModule, self).keyPressEvent(event)
 
     def set_header_item(self, column, text):
         """Insert item with header-style.
@@ -73,20 +107,6 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
         item.setFont(HEADER_FONT)
         self.setItem(0, column, item)
 
-    def set_shortcuts(self):
-        """Set shortcuts for adding and removing rows and columns."""
-        # Create shortcuts
-        shortcut_add_col = QShortcut(QtGui.QKeySequence("Shift++"), self)
-        shortcut_rem_col = QShortcut(QtGui.QKeySequence("Shift+-"), self)
-        shortcut_add_row = QShortcut(QtGui.QKeySequence("Ctrl++"), self)
-        shortcut_rem_row = QShortcut(QtGui.QKeySequence("Ctrl+-"), self)
-
-        # Connect shortcuts to functions
-        shortcut_add_col.activated.connect(self.add_column)
-        shortcut_rem_col.activated.connect(self.remove_column)
-        shortcut_add_row.activated.connect(self.add_row)
-        shortcut_rem_row.activated.connect(self.remove_row)
-
     def add_column(self):
         """Add column to the right of selected column."""
         self.insertColumn(self.currentColumn() + 1)
diff --git a/soitool/pdf_preview_widget.py b/soitool/pdf_preview_widget.py
new file mode 100644
index 0000000000000000000000000000000000000000..f241cb20cb087efdffcf52ae9057a1d99f47d143
--- /dev/null
+++ b/soitool/pdf_preview_widget.py
@@ -0,0 +1,33 @@
+"""Includes functionality for PDF preview."""
+from PySide2.QtCore import QUrl
+from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
+
+
+# pylint: disable=r0903
+class PDFPreviewWidget(QWebEngineView):
+    """Widget to preview PDFs using QWebEngineView with PDF plugin.
+
+    Inspired by the following sources:
+    * https://doc.qt.io/qt-5/qtwebengine-features.html#pdf-file-viewing
+    * https://doc.qt.io/qt-5/qtwebengine-webenginewidgets-simplebrowser-example.html
+
+    The load method interited from QWebEngineView's can be used to load a new
+    URL, and the inherited reload method can be used to reload the currently
+    opened URL.
+
+    Parameters
+    ----------
+    initial_url : QUrl
+        URL to load by default. Used for QWebEngineView's .load method
+        https://doc.qt.io/qtforpython/PySide2/QtWebEngineWidgets/QWebEngineView.html#detailed-description
+    """
+
+    def __init__(self, initial_url):
+        super().__init__()
+        self.page().settings().setAttribute(QWebEngineSettings.PluginsEnabled,
+                                            True)
+        # the following setting is the default, but explicitly enabling to be
+        # explicit
+        self.page().settings().setAttribute(QWebEngineSettings.
+                                            PdfViewerEnabled, True)
+        self.load(QUrl(initial_url))
diff --git a/soitool/setup_settings.py b/soitool/setup_settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..3fef0c0c7d804eeff898b060f91a7e56ea68fa95
--- /dev/null
+++ b/soitool/setup_settings.py
@@ -0,0 +1,81 @@
+"""Includes the functionality to edit settings for SOI.
+
+This dialog is called when a button in soi_workspace_widget is pressed
+
+"""
+
+from PySide2.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, \
+    QLabel, QLineEdit, QRadioButton, QPushButton
+
+
+class Setup(QDialog):  # pylint: disable = R0902
+    """Contains the settings for the SOI."""
+
+    def __init__(self):  # pylint: disable = R0915
+        super().__init__()
+        self.layout_setup = QVBoxLayout()
+        self.layout_buttons = QHBoxLayout()
+        self.layout_header = QFormLayout()
+        self.layout_paper_orientation = QFormLayout()
+        self.layout_algorithm = QFormLayout()
+        self.layout_module_placement = QFormLayout()
+
+        # Labels for headings
+        self.label_setup = QLabel("Oppsett")
+        # setup.setStyleSheet("font: 12pt")
+        self.label_header = QLabel("Headerdata")
+        self.label_paper_orientation = QLabel("Papirretning")
+        self.label_algorithm = QLabel("Plasseringsalgoritme")
+        self.label_module_placement = QLabel("Modulplassering")
+
+        self.layout_setup.addWidget(self.label_setup)
+
+        # Headerdata
+        self.layout_setup.addWidget(self.label_header)
+        self.head1 = QLabel("Header1")  # Change variablename later
+        self.head2 = QLabel("Header2")  # Change variablename later
+        self.hline1 = QLineEdit()    # Change variablename later
+        self.hline2 = QLineEdit()    # Change variablename later
+        self.layout_header.addRow(self.head1, self.hline1)
+        self.layout_header.addRow(self.head2, self.hline2)
+        self.layout_setup.addLayout(self.layout_header)
+
+        # Paperorientation
+        self.layout_setup.addWidget(self.label_paper_orientation)
+        self.pob1 = QRadioButton()  # Change variablename later
+        self.pob2 = QRadioButton()  # Change variablename later
+        self.layout_paper_orientation.addRow(self.pob1, QLabel("Horisontal"))
+        self.layout_paper_orientation.addRow(self.pob2, QLabel("Vertikal"))
+        self.layout_setup.addLayout(self.layout_paper_orientation)
+
+        # Placement algorithm
+        self.layout_setup.addWidget(self.label_algorithm)
+        self.pb1 = QRadioButton()  # Change variablename later
+        self.pb2 = QRadioButton()  # Change variablename later
+        self.pb3 = QRadioButton()  # Change variablename later
+        self.pb4 = QRadioButton()  # Change variablename later
+        self.layout_algorithm.addRow(self.pb1, QLabel("Alg1"))
+        self.layout_algorithm.addRow(self.pb2, QLabel("Alg2"))
+        self.layout_algorithm.addRow(self.pb3, QLabel("Alg3"))
+        self.layout_algorithm.addRow(self.pb4, QLabel("Alg4"))
+        self.layout_setup.addLayout(self.layout_algorithm)
+
+        # Moduleplacement
+        self.layout_setup.addWidget(self.label_module_placement)
+        self.mb1 = QRadioButton()  # Change variablename later
+        self.mb2 = QRadioButton()  # Change variablename later
+        self.layout_module_placement.addRow(self.mb1, QLabel("Automatisk"))
+        self.layout_module_placement.addRow(self.mb2, QLabel("Manuelt"))
+        self.layout_setup.addLayout(self.layout_module_placement)
+
+        # Exit-buttons
+        self.button_cancel = QPushButton("Avbryt")
+        self.button_save = QPushButton("Lagre")
+        self.layout_buttons.addWidget(self.button_cancel)
+        self.layout_buttons.addWidget(self.button_save)
+        self.layout_setup.addLayout(self.layout_buttons)
+
+        self.setLayout(self.layout_setup)
+
+        self.button_cancel.clicked.connect(self.reject)  # esc-key (default)
+        self.button_save.clicked.connect(self.accept)  # enter-key (default)
diff --git a/soitool/soi.py b/soitool/soi.py
new file mode 100644
index 0000000000000000000000000000000000000000..485185411fa1806a97262ded70972105e3459e9f
--- /dev/null
+++ b/soitool/soi.py
@@ -0,0 +1,142 @@
+"""Includes datastructure used to represent a SOI."""
+from enum import Enum
+from PySide2.QtCore import QPoint
+from soitool.modules.module_table import TableModule
+
+
+class ModuleType(Enum):
+    """Enumerate with types of modules."""
+
+    MAIN_MODULE = 0
+    ATTACHMENT_MODULE = 1
+
+
+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,
+                    "name": 'Tabell1'
+                }
+            },
+            {
+                "widget": TableModule(),
+                "meta": {
+                    "x": 0,
+                    "y": 0,
+                    "page": 1,
+                    "name": 'Tabell2'
+                }
+            },
+            {
+                "widget": TableModule(),
+                "meta": {
+                    "x": 0,
+                    "y": 0,
+                    "page": 2,
+                    "name": 'Tabell3'
+                }
+            }
+        ]
+
+        # NOTE
+        # test attachments, just to have something show up on screen
+        self.attachments = [
+            {
+                "widget": TableModule(),
+                "meta": {
+                    "x": 0,
+                    "y": 0,
+                    "page": 2,
+                    "name": 'Tabell1'
+                }
+            }
+        ]
+
+        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
diff --git a/soitool/soi_workspace_widget.py b/soitool/soi_workspace_widget.py
new file mode 100644
index 0000000000000000000000000000000000000000..76b5531b35690ce19984de8cdd938edd232b8bb6
--- /dev/null
+++ b/soitool/soi_workspace_widget.py
@@ -0,0 +1,61 @@
+"""Includes functionality to edit and view a single SOI.
+
+Meant for use inside of tabs in our program.
+
+"""
+from PySide2.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QPushButton, \
+    QLabel
+from soitool.soi import SOI, ModuleType
+from soitool.module_list import ModuleList
+from soitool.inline_editable_soi_view import InlineEditableSOIView
+from soitool.setup_settings import Setup
+
+
+class SOIWorkspaceWidget(QWidget):
+    """Contains the working area for a single SOI.
+
+    The widget is used inside tabs in our application, and contains a sidebar
+    with a module list along with a view of the SOI.
+    """
+
+    def __init__(self):
+        super().__init__()
+
+        self.soi = SOI()
+
+        self.layout_wrapper = QHBoxLayout()
+        self.layout_sidebar = QVBoxLayout()
+
+        # all widgets
+        self.button_new_module = QPushButton("Ny modul")
+        self.button_new_module.setShortcut("Ctrl+m")
+        self.button_new_module.setStatusTip("Legg til en ny modul")
+        self.button_setup = QPushButton("Oppsett")
+
+        self.list_modules = ModuleList(ModuleType.MAIN_MODULE, self)
+        self.list_attachments = ModuleList(ModuleType.ATTACHMENT_MODULE, self)
+
+        self.view = InlineEditableSOIView(self.soi)
+        self.widget_sidebar = QWidget()
+        self.widget_sidebar.setFixedWidth(200)
+        self.popup = Setup()
+
+        # build layouts
+        self.layout_sidebar.addWidget(QLabel("Moduler:"))
+        self.layout_sidebar.addWidget(self.list_modules, 5)
+        self.layout_sidebar.addWidget(QLabel("Vedlegg:"))
+        self.layout_sidebar.addWidget(self.list_attachments, 1)
+        self.layout_sidebar.addWidget(self.button_new_module)
+        self.layout_sidebar.addWidget(self.button_setup)
+        self.widget_sidebar.setLayout(self.layout_sidebar)
+        self.layout_wrapper.addWidget(self.widget_sidebar)
+        self.layout_wrapper.addWidget(self.view)
+
+        self.setLayout(self.layout_wrapper)
+
+        self.button_setup.clicked.connect(self.open_setup)
+
+    def open_setup(self):
+        """Open setup_settings."""
+        self.popup.setGeometry(150, 150, 200, 200)
+        self.popup.exec()  # exec = modal dialog, show = modeless dialog
diff --git a/test/test_main.py b/test/test_main.py
index beadccfe5991936ceba67b155d69db315ddf1414..3255793a528443b4115c6ffccb8e029aeec7f9ab 100644
--- a/test/test_main.py
+++ b/test/test_main.py
@@ -1,9 +1,8 @@
 """Test CoolWidget."""
 
 import unittest
-import sys
 from datetime import datetime as datetime_, timedelta
-from PySide2 import QtWidgets, QtTest, QtCore
+from PySide2 import QtWidgets, QtTest, QtCore, QtGui
 from soitool import main
 
 # references:
@@ -18,8 +17,10 @@ from soitool import main
 # * keyclick: https://programtalk.com/python-examples/PyQt4.QtTest.QTest.keyClick/
 # * isVisible: https://stackoverflow.com/questions/13850240/pyqt-how-to-check-is-qdialog-is-visible
 
-# moved here from setUp to avoid annoying startup messages
-app = QtWidgets.QApplication(sys.argv)
+if isinstance(QtGui.qApp, type(None)):
+    app = QtWidgets.QApplication([])
+else:
+    app = QtGui.qApp
 
 
 def wait(msec):
@@ -59,13 +60,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(
diff --git a/test/test_main_window.py b/test/test_main_window.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e6290c7ac066f54a4cf05b4192380b956f4393f
--- /dev/null
+++ b/test/test_main_window.py
@@ -0,0 +1,35 @@
+"""Test main_window.py."""
+
+import unittest
+from PySide2 import QtWidgets, QtGui
+from soitool import main_window
+
+if isinstance(QtGui.qApp, type(None)):
+    app = QtWidgets.QApplication([])
+else:
+    app = QtGui.qApp
+
+
+class TestMainWindow(unittest.TestCase):
+    """Test class for main_window.py."""
+
+    def setUp(self):
+        """Set up main_window_widget for testing."""
+        self.test_mw = main_window.MainWindow()
+
+    def test_window_title(self):
+        """Test window title is correct."""
+        expected = "SOI-tool"
+        actual = self.test_mw.windowTitle()
+        self.assertEqual(expected, actual)
+
+    # To smoke test SOIWorkspaceWidget
+    def test_new_module_btn(self):
+        """Test correct label on new module button."""
+        expected = "Ny modul"
+        actual = self.test_mw.tabs.currentWidget().button_new_module.text()
+        self.assertEqual(expected, actual)
+
+
+if __name__ == '__main__':
+    unittest.main()