diff --git a/scripts/.pylintrc b/scripts/.pylintrc
index a4c463b20ed3dc0db7b8e1f7e3e4d233720719e9..17b783052542f58797737e105ec35f5d0669a615 100644
--- a/scripts/.pylintrc
+++ b/scripts/.pylintrc
@@ -141,7 +141,8 @@ disable=print-statement,
         comprehension-escape,
         E0611,
         I1101,
-        E1101
+        E1101,
+        R0901
 
 # Enable the message, report, category or checker with the given id(s). You can
 # either give multiple identifier separated by comma (,) or put this option
diff --git a/soitool/main_window.py b/soitool/main_window.py
index 2a50c4d2daf3953e286e9011cc3765674019500f..4f00083a241c9c34de110ed709e7266168c88b8a 100644
--- a/soitool/main_window.py
+++ b/soitool/main_window.py
@@ -1,10 +1,16 @@
-"""Hovedvinduet."""
+"""MainWindow.
+
+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 QRectF, QPoint, QTimer, Qt
-from PySide2.QtWidgets import QTabWidget, QWidget, QMainWindow, QApplication, \
-    QHBoxLayout, QVBoxLayout, QPushButton, QTreeWidget, QGraphicsScene, \
-    QGraphicsView, QAction, QScrollArea, QGraphicsRectItem
+from PySide2.QtWidgets import QTabWidget, QWidget, QMainWindow, \
+    QApplication, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, \
+    QAbstractItemView, QListWidget, QListWidgetItem, QAction, QGraphicsScene, \
+    QGraphicsView, QScrollArea, QGraphicsRectItem
 from PySide2.QtGui import QBrush, QIcon, QPalette
 
 
@@ -83,7 +89,7 @@ class MainWindow(QMainWindow):
 
         # Legger til MainWidget som en tab, kanskje flytt ut til egen funksjon
         tabs = QTabWidget()
-        tab = MainWidget()
+        tab = SOIWorkspaceWidget()
 
         tabs.addTab(tab, "MainTab")
         self.setCentralWidget(tabs)
@@ -95,30 +101,74 @@ class MainWindow(QMainWindow):
         self.setWindowIcon(QIcon(filepath))
 
 
-class MainWidget(QWidget):
-    """Hovedwidget til applikasjonen, dette er det som blir tabs."""
+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__()
-        layout1 = QHBoxLayout()
-        layout2 = QVBoxLayout()
 
-        # New module button
-        new_module = QPushButton("Ny modul")
-        new_module.setShortcut("Ctrl+m")
-        new_module.setStatusTip("Legg til en ny modul")
+        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()
+        self.widget_sidebar = QWidget()
+        self.widget_sidebar.setFixedWidth(200)
+
+        # 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.widget_sidebar.setLayout(self.layout_sidebar)
+        self.layout_wrapper.addWidget(self.widget_sidebar)
+        self.layout_wrapper.addWidget(self.view)
+
+        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)
 
-        tree_view = QTreeWidget()
-        setup = QPushButton("Oppsett")
-        layout2.addWidget(tree_view)
-        layout2.addWidget(new_module)
-        layout2.addWidget(setup)
+        # source: https://www.qtcentre.org/threads/32500-Horizontal-Scrolling-QListWidget
+        self.list_modules.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
 
-        view = ViewArea()
-        layout1.addLayout(layout2, 2)
-        layout1.addWidget(view, 8)
+    def fill_list_modules(self):
+        """Fill module list with some items to manually test with.
 
-        self.setLayout(layout1)
+        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):
diff --git a/soitool/modules/module_base.py b/soitool/modules/module_base.py
new file mode 100644
index 0000000000000000000000000000000000000000..92e7e6bc6145a1a1921cb5580c7b8384d77cad51
--- /dev/null
+++ b/soitool/modules/module_base.py
@@ -0,0 +1,14 @@
+"""Base/interface of each module."""
+from abc import ABC
+
+
+class ModuleBase(ABC):
+    """Interface for SOI-modules."""
+
+    def get_size(self):
+        """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/table_module.py b/soitool/modules/table_module.py
new file mode 100644
index 0000000000000000000000000000000000000000..50a68289d164a6f0a6ed99d02250ca0a561c6413
--- /dev/null
+++ b/soitool/modules/table_module.py
@@ -0,0 +1,128 @@
+"""Module containing subclassed SOIModule (QTableWidget, ModuleBase)."""
+from PySide2.QtWidgets import QTableWidget, QTableWidgetItem, QShortcut
+from PySide2 import QtGui
+from module_base import ModuleBase
+
+HEADER_FONT = QtGui.QFont()
+HEADER_FONT.setFamily('Arial')
+HEADER_FONT.setPointSize(12)
+HEADER_FONT.setWeight(100)
+
+
+class Meta(type(ModuleBase), type(QTableWidget)):
+    """Used as a metaclass to enable multiple inheritance."""
+
+
+class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
+    """Modified QTableWidget.
+
+    Has shortcuts for adding and removing rows and columns.
+    Also inherits functions from ModuleBase, which is implemented by this class.
+    """
+
+    def __init__(self):
+        """Initialize QTableWidget."""
+        QTableWidget.__init__(self)
+        super(QTableWidget)
+
+        self.horizontalHeader().hide()
+        self.verticalHeader().hide()
+
+        self.setColumnCount(2)
+        self.setRowCount(2)
+
+        self.set_header_item(0, "")
+        self.set_header_item(1, "")
+
+        self.resizeRowsToContents()
+        self.resizeColumnsToContents()
+
+        self.cellChanged.connect(self.cell_changed)
+
+        self.set_shortcuts()
+
+    def set_header_item(self, column, text):
+        """Insert item with header-style.
+
+        Item will always be set on header row (top row).
+
+        Parameters
+        ----------
+        column : int
+            What column index for inserting item.
+        text : String
+            What text the item should contain.
+        """
+        item = QTableWidgetItem(text)
+        item.setBackground(QtGui.QBrush(QtGui.QColor(220, 220, 220)))
+        item.setFont(HEADER_FONT)
+        self.setItem(0, column, item)
+
+    def set_shortcuts(self):
+        """Set shortcuts for adding and removing rows and columns."""
+        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)
+
+        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.columnCount())
+
+        # From selected column, move all columns one step to the right
+        to_pos = self.columnCount() - 2  # to_pos is target index for columns
+        while to_pos > self.currentColumn():
+            # Loop through all rows
+            for j in range(self.rowCount()):
+                item = self.takeItem(j, to_pos)
+                self.setItem(j, to_pos + 1, item)
+            to_pos -= 1
+
+        self.set_header_item(self.currentColumn() + 1, "")
+        self.resizeColumnsToContents()
+
+    def remove_column(self):
+        """Remove selected column if two or more columns exist."""
+        if self.columnCount() > 1:
+            self.removeColumn(self.currentColumn())
+
+    def add_row(self):
+        """Add row below selected row."""
+        self.insertRow(self.currentRow() + 1)
+        self.resizeRowsToContents()
+
+    def remove_row(self):
+        """Remove selected row if two or more rows exist (including header)."""
+        if self.rowCount() > 2:
+            self.removeRow(self.currentRow())
+
+    def cell_changed(self):
+        """Resize rows and columns to contents when a cell changes."""
+        self.resizeColumnsToContents()
+        self.resizeRowsToContents()
+
+    def get_size(self):
+        """Get size of widget.
+
+        Returns
+        -------
+        Tuple
+            (width, height) (total)
+        """
+        width = 0
+        for i in range(self.columnCount()):
+            width += self.columnWidth(i)
+
+        height = 0
+        for i in range(self.columnCount()):
+            height += self.rowHeight(i)
+
+        return width, height
+
+    def render_onto_pdf(self):
+        """Render onto pdf."""