diff --git a/src/simudator/gui/cpu_graphics_scene.py b/src/simudator/gui/cpu_graphics_scene.py
index 47292c4ec7552e6f75e0243cbb44009a5bdee77b..a64e417b1dfa6a4b9d834bb031373a2f9364a3f1 100644
--- a/src/simudator/gui/cpu_graphics_scene.py
+++ b/src/simudator/gui/cpu_graphics_scene.py
@@ -1,8 +1,22 @@
-from qtpy.QtWidgets import QGraphicsItem, QGraphicsScene
+import json
+from json.decoder import JSONDecodeError
+
+from qtpy.QtCore import QPointF
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtWidgets import (
+    QErrorMessage,
+    QFileDialog,
+    QGraphicsItem,
+    QGraphicsScene,
+    QMessageBox,
+)
 
 from simudator.core.processor import Processor
 from simudator.gui.color_scheme import ColorScheme
 from simudator.gui.module_graphics_item.module_graphics_item import ModuleGraphicsItem
+from simudator.gui.orientation import Orientation
+from simudator.gui.port_graphics_item import PortGraphicsItem
 from simudator.gui.signal_graphics_item import SignalGraphicsItem
 
 
@@ -10,69 +24,78 @@ class CpuGraphicsScene(QGraphicsScene):
     """
     This class creates the graphics scene for visualising the processor.
     It takes each module representation as a ModuleGraphicsItem
-    and handels mouse inputs for interacting with these graphicsitems.
+    and handles mouse inputs for interacting with these graphicsitems.
     Can create a default layout on creation and can save/load new layouts.
     """
 
     MODULE_SPACEING = 100
 
-    def __init__(self, cpu: Processor):
+    def __init__(self):
         super().__init__()
-        self.cpu = cpu
-        self.module_graphics_items = dict()
-        self.signal_graphics_items = []
+        self._module_graphics_items = dict()
+        self._signal_graphics_items = []
+        self._error_msg_box = QErrorMessage()
         self.setBackgroundBrush(ColorScheme.Window)
 
-    def resetSignals(self) -> None:
+    def reset_signals(self) -> None:
         """
-        Resets all graphical signals to their default visual representation
+        Reset all graphical signals to their default visual representation
         when initialised.
         """
-        for graphics_Signal in self.signal_graphics_items:
-            graphics_Signal.reset()
+        for graphics_signal in self._signal_graphics_items:
+            graphics_signal.reset()
 
-    def addModuleGraphicsItem(self, graphics_item: ModuleGraphicsItem) -> None:
-        """
-        Takes a ModuleGraphicsItem and adds it to the scene.
-        Will give it a new position according to the deafult layout.
+    def add_module(self, item: ModuleGraphicsItem) -> None:
         """
-        self.module_graphics_items[graphics_item.name] = graphics_item
-        self.placeModuleGraphicsItemDefault(graphics_item)
+        Add a graphical module item to the processor scene at a position
+        according to the default layout.
 
-    def placeModuleGraphicsItemDefault(self, graphics_item: ModuleGraphicsItem) -> None:
-        """
-        Places a module graphics items at a position where the x value
-        is equal to the y value. Used to place all graphics items in a
-        diagonal.
+        Parameters
+        ----------
+        item : ModuleGraphicsItem
+            Graphical module item to add to the scene.
         """
-        placement = len(self.module_graphics_items) * self.MODULE_SPACEING
-        graphics_item.setPos(placement, placement)
-        self.addItem(graphics_item)
+        self._module_graphics_items[item.name] = item
+        self._place_module_default(item)
 
-    def replaceModuleGraphicsItem(
-        self, graphics_item: ModuleGraphicsItem, pos_x: int, pos_y: int
-    ) -> None:
+    def _place_module_default(self, item: ModuleGraphicsItem) -> None:
         """
-        Changes the postions of an existing modules graphics item.
+        Place a graphical module item at a position according to the default
+        layout, i.e. placing all module items along a diagonal.
+
+        Parameters
+        ----------
+        item : ModuleGraphicsItem
         """
-        graphics_item.setPos(pos_x * self.MODULE_SPACEING, pos_y * self.MODULE_SPACEING)
+        placement = len(self._module_graphics_items) * self.MODULE_SPACEING
+        item.setPos(placement, placement)
+        self.addItem(item)
 
-    def updateGraphicsItems(self):
+    def move_module_to(self, item: ModuleGraphicsItem, pos_x: int, pos_y: int) -> None:
         """
-        Used to update graphicsitems when modules in the processor has chnaged values.
+        Place an existing graphical module item at some position.
+
+        Parameters
+        ----------
+        item : ModuleGraphicsItem
+           Graphical module item to place at a position.
+        pos_x : int
+            Position on x-axis to place the module item at.
+        pos_y : int
+            Position on y-axis to place the module item at.
         """
-        for graphics_item in self.module_graphics_items.values():
-            graphics_item.update()
+        item.setPos(pos_x * self.MODULE_SPACEING, pos_y * self.MODULE_SPACEING)
 
-    def addAllSignals(self) -> None:
+    def add_all_signals(self) -> None:
         """
-        Instantiates signals between all matching ports of all modules.
+        Instantiate graphical signals between all matching ports of all
+        graphical modules.
         """
         # Map which ports that are connected using their signals' names
         # (Every port keeps a reference to a simulation signal which has
         # a name)
         signal_to_ports = {}
-        for module_w in self.module_graphics_items.values():
+        for module_w in self._module_graphics_items.values():
             ports = module_w.getPorts()
             for port in ports:
                 s_name = port.getSignalName()
@@ -88,96 +111,317 @@ class CpuGraphicsScene(QGraphicsScene):
                 port_1 = module_graphics_items[0]
                 port_2 = module_graphics_items[1]
                 signal_w = SignalGraphicsItem(port_1, port_2)
-                self.signal_graphics_items.append(signal_w)
+                self._signal_graphics_items.append(signal_w)
                 self.addItem(signal_w)
                 port_1.moved.connect(signal_w.move)
                 port_2.moved.connect(signal_w.move)
                 port_1.toggled.connect(signal_w.toggleVisibility)
                 port_2.toggled.connect(signal_w.toggleVisibility)
 
-    def getModulesGraphicsItems(self) -> list[ModuleGraphicsItem]:
-        return list(self.module_graphics_items.values())
+    def get_modules(self) -> list[ModuleGraphicsItem]:
+        """
+        Get a list of all graphical module items in the scene.
 
-    def moduleGraphicsItemsDict(self) -> dict[str, ModuleGraphicsItem]:
-        return self.module_graphics_items
+        Returns
+        -------
+        list[ModuleGraphicsItem]
+            List of graphical module items in the scene.
+        """
+        return list(self._module_graphics_items.values())
 
-    def getSignalGraphicsItems(self) -> list[SignalGraphicsItem]:
-        return self.signal_graphics_items
+    def get_signals(self) -> list[SignalGraphicsItem]:
+        """
+        Get a list of all graphical signal items in the scene.
+
+        Returns
+        -------
+        list[SignalGraphicsItem]
+            List of graphical signal items in the scene.
+        """
+        return self._signal_graphics_items
 
     def mousePressEvent(self, event):
         super().mousePressEvent(event)
 
-    def load_layout_from_file(self, file_path: str) -> None:
+    @Slot()
+    def load_layout(self) -> None:
         """
-        Loads a layout for a processor from a saved filed.
+        Prompt the user for a layout file and load the layout from the file.
         """
-        file = open(file_path)
-
-        graphics_item_name = None
-        graphics_item_str = ""
 
-        # Go through each line in the file
-        for line in file.readlines():
-            # If no name currently saved then get name from current line
-            if graphics_item_name is None:
-                graphics_item_name = line.partition(":")[0]
+        # Prompt the user for a file
+        dialog = QFileDialog()
+        dialog.setFileMode(QFileDialog.AnyFile)
+        dialog.setAcceptMode(QFileDialog.AcceptOpen)
+        dialog.setDirectory("~/simudator")  # TODO: does this work when exported?
+        path = dialog.getOpenFileName()[0]
 
-            # if already has name get line content
-            else:
-                # if not empty line, then save content
-                if line.strip():
-                    graphics_item_str += line
+        # If no file was selected, do nothing
+        if path == '':
+            return
 
-                # if line is empty then give saved content to name file
-                else:
-                    graphics_item = self.get_module(graphics_item_name)
-                    try:
-                        graphics_item.load_state_from_str(graphics_item_str)
-                    except Exception as exc:
-                        raise exc
-
-                    # set back to empty
-                    graphics_item_str = ""
-                    graphics_item_name = None
+        try:
+            self.load_layout_from_file(path)
 
-        # give module anything that is left
-        if graphics_item_name and graphics_item_str:
-            graphics_item = self.get_module(graphics_item_name)
-            graphics_item.load_state_from_str(graphics_item_str)
+        # Anything goes wrong with loading the selected one,
+        # we dont care about what went wrong
+        except (OSError, JSONDecodeError):
+            self.load_default_layout()
+            self._error_msg_box.showMessage(
+                "Unable to load given file.", "load_layout_err"
+            )
+        else:
+            QMessageBox.information(
+                self.parent(), "SimuDator", "File loaded succesfully."
+            )
 
-    def save_layout_to_file(self, file_path: str) -> None:
+    def _save_layout_to_file(self, file_path: str) -> None:
         """
-        Saves the positions and visibility of graphicsitems in the scene (this
-        includes module and signal representations) to a file.
+        Save the layout of the scene to file.
+
+        Parameters
+        ----------
+        file_path : str
+            Path to the file to save to layout to.
         """
         layout_str = ""
-        for graphics_item in self.module_graphics_items.values():
+        for graphics_item in self._module_graphics_items.values():
             layout_str += graphics_item.save_state_as_str() + "\n"
 
         file = open(file_path, "w")
         file.write(layout_str)
         file.close()
 
-    def setAllSignalsVisibility(self, is_signals_visible: bool) -> None:
-        for item in self.signal_graphics_items:
-            item.setVisible(is_signals_visible)
+    @Slot(bool)
+    def show_all_signals(self, value: bool) -> None:
+        """
+        Set the visibility of all graphical signals in the scene.
 
-    def setPortNamesVisibility(self, is_ports_visible: bool) -> None:
-        for item in self.module_graphics_items.values():
+        Parameters
+        ----------
+        value : bool
+            ``True`` to show all graphical signals, ``False`` to hide them.
+        """
+        for item in self._signal_graphics_items:
+            item.setVisible(value)
+
+    @Slot(bool)
+    def show_port_names(self, value: bool) -> None:
+        """
+        Set the visibility of the names of all ports of graphical modules
+        in the scene.
+
+        Parameters
+        ----------
+        value : bool
+            ``True`` to show the port names, ``False`` to hide them.
+        """
+        for item in self._module_graphics_items.values():
             for port in item.ports:
-                port.setNameVisibility(is_ports_visible)
+                port.setNameVisibility(value)
 
-    def setLayoutLock(self, is_layout_locked: bool) -> None:
+    def toggle_layout_lock(self, value: bool) -> None:
         """
-        Toggles the layout lock making it so items in the scene can not be moved.
+        Toggle the layout between locked and unlocked. Locked means that
+        nothing can be moved around in the layout.
+
+        Parameters
+        ----------
+        value : bool
+            ``True`` to lock the layout, ``False`` to unlock it.
         """
 
-        for item in self.module_graphics_items.values():
-            item.setFlag(QGraphicsItem.ItemIsMovable, not is_layout_locked)
-            item.setLocked(is_layout_locked)
+        for item in self._module_graphics_items.values():
+            item.setFlag(QGraphicsItem.ItemIsMovable, not value)
+            item.setLocked(value)
 
-        for item in self.signal_graphics_items:
-            item.setFlag(QGraphicsItem.ItemIsMovable, not is_layout_locked)
+        for item in self._signal_graphics_items:
+            item.setFlag(QGraphicsItem.ItemIsMovable, not value)
 
             # We use this value so lines in the signal can not be moved or edited
-            item.is_locked = is_layout_locked
+            item.is_locked = value
+
+    @Slot()
+    def save_layout(self) -> None:
+        """
+        Prompt the user for a file and save the scene layout to the file.
+
+        This overwrites the previous content of the file.
+        """
+        # Prompt the user for a file
+        dialog = QFileDialog()
+        dialog.setFileMode(QFileDialog.AnyFile)
+        dialog.setAcceptMode(QFileDialog.AcceptOpen)
+        path = dialog.getSaveFileName()[0]
+
+        # Open the given file erasing the previous content
+        with open(path, "w") as fp:
+            graphics_modules_data = {}
+            ports_data = {}
+            graphics_signals_data = {}
+
+            graphics_modules = self.get_modules()
+            for graphics_module in graphics_modules:
+                pos = (graphics_module.x(), graphics_module.y())
+                graphics_modules_data[graphics_module.getName()] = pos
+
+                for port in graphics_module.getPorts():
+                    visibility = port.isVisible()
+                    orientation = int(port.getOrientation())
+                    data = (port.x(), port.y(), orientation, visibility)
+                    ports_data[port.getID()] = data
+
+            for graphics_signal in self.get_signals():
+                visibility = graphics_signal.isVisible()
+                points = []
+                for point in graphics_signal.getPoints():
+                    points.append((point.x(), point.y()))
+                data = (points, visibility)
+                graphics_signals_data[graphics_signal.getID()] = data
+
+            data = (graphics_modules_data, ports_data, graphics_signals_data)
+            json.dump(data, fp)
+
+        fp.close()
+
+    @Slot()
+    def load_default_layout(self) -> None:
+        """
+        Place all graphical modules in the scene according to the default
+        layout, i.e. along a diagonal.
+        """
+        counter = 0
+        for key in self._module_graphics_items:
+            module_graphic = self._module_graphics_items[key]
+            module_graphic.showPorts()
+            counter += 1
+            self.move_module_to(module_graphic, counter, counter)
+        self.reset_signals()
+
+    def load_layout_from_file(self, file_path: str) -> None:
+        """
+        Load a layout from a file.
+
+        If at anypoint this function would error, the default layout
+        is loaded instead.
+
+        Parameters
+        ----------
+        file_path : str
+            Path to a file containing a layout for the processor scene.
+        """
+        graphics_modules = self._module_graphics_items
+        ports = {}
+        graphics_signals = {}
+
+        for graphics_module in graphics_modules.values():
+            for port in graphics_module.getPorts():
+                ports[port.getID()] = port
+
+        for graphics_signal in self.get_signals():
+            graphics_signals[graphics_signal.getID()] = graphics_signal
+
+        # Open the file in 'read-only'
+        with open(file_path, 'rb') as fp:
+            data = json.load(fp)
+            graphics_modules_data = data[0]
+            ports_data = data[1]
+            graphics_signals_data = data[2]
+
+            for g_module_name, g_module_data in graphics_modules_data.items():
+                g_module = graphics_modules[g_module_name]
+                x = g_module_data[0]
+                y = g_module_data[1]
+                self._load_module(g_module, x, y)
+
+            for port_id, port_data in ports_data.items():
+                port = ports[int(port_id)]
+                self._load_port(port, *port_data)
+
+            for g_signal_id, g_signal_data in graphics_signals_data.items():
+                g_signal = graphics_signals[int(g_signal_id)]
+                self._load_signal(g_signal, *g_signal_data)
+
+        fp.close()
+
+    def _load_signal(
+        self,
+        signal: SignalGraphicsItem,
+        signal_points: list[tuple[float, float]],
+        visibility: bool,
+    ) -> None:
+        """
+        Set the positions and visibility of a graphical signal. Helper method
+        for loading a layout from file.
+
+        Parameters
+        ----------
+        signal : SignalGraphicsItem
+            Graphical signal to modify.
+        signal_points : list[tuple[float, float]]
+            List of points for visually drawing the signal as line.
+        visibility : bool
+            Visibility of the signal. ``True`` to show it, ``False`` to hide it.
+        """
+        qpoints = []
+        # Turn points -> QPointF
+        # list[int, int] -> QPointF object
+        for point in signal_points:
+            qpoints.append(QPointF(point[0], point[1]))
+
+        # Set the new points
+        signal.setPoints(qpoints)
+        signal.setVisible(visibility)
+
+    def _load_module(
+        self,
+        module: ModuleGraphicsItem,
+        pos_x: float,
+        pos_y: float,
+    ) -> None:
+        """
+        Set the position of a graphical module in the scene. Helper method
+        for loading a layout from file.
+
+        Parameters
+        ----------
+        module : ModuleGraphicsItem
+            Graphical module of which to set the position.
+        pos_x : float
+            Position on the x-axis in the scene.
+        pos_y : float
+            Position on the y-axis in the scene.
+        """
+        module.setX(pos_x)
+        module.setY(pos_y)
+
+    def _load_port(
+        self,
+        port: PortGraphicsItem,
+        pos_x: float,
+        pos_y: float,
+        orientation: Orientation,
+        visibility: bool,
+    ) -> None:
+        """
+        Set position, orientation and visibility of a port of a graphical
+        module. Helper method for loading a layout from file.
+
+        Parameters
+        ----------
+        port : PortGraphicsItem
+            Port to modify.
+        pos_x : float
+            Position on the x-axis in the scene.
+        pos_y : float
+            Position on the y-axis in the scene.
+        orientation : Orientation
+            Orientation of the port.
+        visibility : bool
+            Visibility of the port. ``True`` to show the port, ``False`` to
+            hide it.
+        """
+        port.setOrientation(orientation)
+        port.setX(pos_x)
+        port.setY(pos_y)
+        port.setVisible(visibility)
diff --git a/src/simudator/gui/custom_toolbar.py b/src/simudator/gui/custom_toolbar.py
deleted file mode 100644
index b55049f2cb0848b4ca1b8b294e734d06fd9a429b..0000000000000000000000000000000000000000
--- a/src/simudator/gui/custom_toolbar.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from qtpy.QtWidgets import QToolBar
-
-
-class CustomToolBar(QToolBar):
-    """
-    A custom implementation of QToolBar that reimplemented contextMenuEvent
-    so that the toolbar is no longer removable.
-    """
-
-    def __init__(self, text):
-        super().__init__(text)
-
-    def contextMenuEvent(self, event):
-        pass
diff --git a/src/simudator/gui/gui.py b/src/simudator/gui/gui.py
index 48297b3901f484398cbe607b8ae512c22b96b188..2f2aa64626b65082ff9a321e61de017495bfa6fc 100644
--- a/src/simudator/gui/gui.py
+++ b/src/simudator/gui/gui.py
@@ -1,137 +1,63 @@
 import ast
-import json
 import sys
-from json import JSONDecodeError
-from threading import Thread
 
 from qtpy import QtCore, QtWidgets
-from qtpy.QtCore import QPointF
 from qtpy.QtCore import Signal as pyqtSignal
-from qtpy.QtCore import Slot
+from qtpy.QtCore import Slot, ws
 from qtpy.QtWidgets import (
     QAction,
     QApplication,
     QErrorMessage,
-    QFileDialog,
-    QGraphicsView,
-    QInputDialog,
-    QLabel,
     QMainWindow,
     QMessageBox,
-    QSpinBox,
-    QStyle,
-    QWidget,
 )
 
 from simudator.core.processor import Processor
 from simudator.gui.breakpoint_window import BreakpointWindow
 from simudator.gui.cpu_graphics_scene import CpuGraphicsScene
-from simudator.gui.custom_toolbar import CustomToolBar
 from simudator.gui.dialogs.lambda_breakpoint_dialog import LambdaBreakpointDialog
+from simudator.gui.menu_bar import MainMenuBar
 from simudator.gui.module_graphics_item.module_graphics_item import ModuleGraphicsItem
-from simudator.gui.orientation import Orientation
-from simudator.gui.port_graphics_item import PortGraphicsItem
-from simudator.gui.run_continuously_thread import RunThread
-from simudator.gui.signal_graphics_item import SignalGraphicsItem
 from simudator.gui.pipeline import PipeLine
-
-
-class View(QGraphicsView):
-    """
-    This class views all QGraphicsItems for the user. It takes the
-    QGraphicsScene as input and inherits all funcitonality from the
-    QGraphicsView and overrides the wheelEvent function.
-    This allows the users to navigate the view with their trackpads
-    and zoom in/out with their trackpads + ctrl.
-    """
-
-    def __init__(self, QGraphicsScene):
-        super().__init__(QGraphicsScene)
-        self.scene = QGraphicsScene
-
-    def wheelEvent(self, event):
-        """
-        Default behaviour if ctrl is not pressed, otherwise zoom in/out.
-        """
-        modifiers = QtWidgets.QApplication.keyboardModifiers()
-        if modifiers == QtCore.Qt.ControlModifier:
-            # Factor above 1 zooms in, below zooms out
-            factor = 1.03
-            if event.angleDelta().y() < 0:
-                factor = 0.97
-
-            # If event got triggered due to the x axis, do nothing
-            if event.pixelDelta().x() != 0:
-                return
-
-            view_pos = event.globalPosition()
-            scene_pos = self.mapToScene(int(view_pos.x()), int(view_pos.y()))
-
-            self.centerOn(scene_pos)
-            self.scale(factor, factor)
-
-            old_mapToScene = self.mapToScene(int(view_pos.x()), int(view_pos.y()))
-            new_mapToScene = self.mapToScene(self.viewport().rect().center())
-
-            delta = old_mapToScene - new_mapToScene
-
-            self.centerOn(scene_pos - delta)
-
-        else:
-            # Default behaviour
-            super().wheelEvent(event)
+from simudator.gui.processor_handler import ProcessorHandler
+from simudator.gui.simulation_toolbar import SimulationToolBar
+from simudator.gui.view import View
 
 
 class GUI(QMainWindow):
     """
-    Main gui class. Handles gui windows, toolbar and visualizes modules.
+    Main gui class for visualizing the SimuDator processor simulator.
 
-    This is the main class for the GUI. It handles creating the window for
-    the gui, aswell as the toolbar for controlling the simultaion.
-    It takes a processor and visualizes its modules as boxes with the signals as lines
-    between them. Graphics items for the modules and signals need to be created
-    and added individually.
-    """
+    Graphics items for modules need to be created and added individually using
+    the provided methods. Signals are also not added automatically but need
+    not be created.
 
-    cpu_tick_signal = pyqtSignal(int)
-    HALT_MESSAGE_THRESHOLD = 100
+    Parameters
+    ----------
+    processor : Processor
+        The simulated processor to visualize and control.
+    """
 
-    def __init__(self, cpu: Processor):
+    def __init__(self, processor: Processor):
         super().__init__()
 
-        self.cpu = cpu
+        self._processor = processor
+        self._processor_handler = ProcessorHandler(processor, parent=self)
 
         self.setWindowTitle("SimuDator")
-        self.cpu_graphics_scene = CpuGraphicsScene(cpu)
-        self.graphics_view = View(self.cpu_graphics_scene)
+        self._graphics_scene = CpuGraphicsScene()
+        self._graphics_view = View(self._graphics_scene)
         # self.graphics_view.setDragMode(True)
-        self.moduleActions: dict[str, QAction] = {}
-
-        self.setCentralWidget(self.graphics_view)
-
-        self.errorMessageWidget = QErrorMessage(self)
-
-        self.createToolBar()
+        self._module_actions: dict[str, QAction] = {}
 
-        # Threadpool for running cpu and gui concurently
-        self.threadpool = QtCore.QThreadPool()
+        self.setCentralWidget(self._graphics_view)
 
-        # Signal to tell gui when cpu has halted
-        self.cpu_tick_signal.connect(self.handleCpuTick)
+        self._error_msg_widget = QErrorMessage(self)
 
-        # Used to set if all values in the gui should be updated each tick
-        # or only the clock counter
-        self.update_all_values = False
+        self._create_menu_bar()
+        self._create_tool_bar()
 
-        # Used to set the update delay
-        # Useful when watching the values being updated while running
-        self.update_delay = 0.00
-
-        # Used to lock some actions in ui when cpu is running in another thread
-        # Using the cpu's internal status directly could case problems
-        self.cpu_running = False
-
-        self.pipeline = PipeLine()
+        self._pipeline = PipeLine()
         self.init_pipeline()
 
         # Set Style, THESE ARE TEST AND DONT WORK
@@ -140,196 +66,58 @@ class GUI(QMainWindow):
         self.setAttribute(QtCore.Qt.WA_StyledBackground)
 
         # Add this so we can save the window in scope later, otherwise it disappears
-        self.breakpoint_window = None
+        self._breakpoint_window = None
 
-    def createToolBar(self) -> None:
+    def _create_menu_bar(self) -> None:
         """
-        Creates the toolbar containing file, layout and toolbar buttons.
-
-
-        Creates the toolbar containing the file, layout and toolbar
-        buttons with their respective sub buttons.
+        Create a main menu bar and connect its signals to appropriate slots of
+        other widgets.
         """
 
-        # Create toolbar
-        toolbar = CustomToolBar("Main toolbar")
-        self.addToolBar(toolbar)
-
-        # Create load action
-        self.load_action = QAction("Load", self)
-        self.load_action.setStatusTip("Load processor state from file")
-        self.load_action.triggered.connect(self.loadToolBarButtonClick)
-
-        # Create save action
-        self.save_action = QAction("Save", self)
-        self.save_action.setStatusTip("Save processor state to file")
-        self.save_action.triggered.connect(self.saveStateMenuButtonClick)
-
-        self.reset_action = QAction("Reset", self)
-        self.reset_action.setStatusTip("Reset processor")
-        self.reset_action.triggered.connect(self.resetMenuButtonClick)
-
-        # Create File menu for load and save
-        menu = self.menuBar()
-        file_menu = menu.addMenu("&File")
-        file_menu.addAction(self.load_action)
-        file_menu.addAction(self.save_action)
-        file_menu.addAction(self.reset_action)
-
-        # create load layout action
-        load_layout_action = QAction("Load layout", self)
-        load_layout_action.setStatusTip("Loads the selected layout from file.")
-        load_layout_action.triggered.connect(self.loadLayoutToolBarButtonClick)
-
-        # create load default layout action
-        load_default_layout_action = QAction("Load default layout", self)
-        load_default_layout_action.setStatusTip("Loads the default layout from file.")
-        load_default_layout_action.triggered.connect(
-            self.loadDefaultLayoutToolBarButtonClick
+        self._menu_bar = MainMenuBar(self)
+        self.setMenuBar(self._menu_bar)
+
+        # Connect signals for processor related actions
+        self._menu_bar.load.connect(self._processor_handler.load_state)
+        self._menu_bar.save.connect(self._processor_handler.save_state)
+        self._menu_bar.reset.connect(self._processor_handler.reset)
+        self._menu_bar.update_all_values.connect(
+            self._processor_handler.toggle_value_update_on_run
         )
+        self._menu_bar.set_delay.connect(self._processor_handler.set_update_delay)
 
-        # create save layout action
-        save_layout_action = QAction("Save layout", self)
-        save_layout_action.setStatusTip("Saves the current layout to a file.")
-        save_layout_action.triggered.connect(self.saveLayoutToolBarButtonClick)
-
-        # create layout lock action
-        self.lock_layout_action = QAction("Lock layout", self, checkable=True)
-        self.lock_layout_action.setStatusTip("Lock layout so items can't be moved.")
-        self.lock_layout_action.triggered.connect(self.toggleLayoutLockMenuButtonClick)
-
-        # Create show signals actions
-        self.signal_vis_action = QAction("Show signals", self, checkable=True)
-        self.signal_vis_action.setChecked(True)
-        self.signal_vis_action.setStatusTip("Toggle the visibility of signal.")
-        self.signal_vis_action.triggered.connect(self.showSignalsMenuButtonClick)
-
-        # Create show port name actions
-        self.port_vis_action = QAction("Show port names", self, checkable=True)
-        self.port_vis_action.setChecked(True)
-        self.port_vis_action.setStatusTip("Toggle the visibility of port names.")
-        self.port_vis_action.triggered.connect(self.showPortNamesBarButtonClick)
-
-        # Create Layout menu for layout actions
-        layout_menu = menu.addMenu("&Layout")
-        layout_menu.addAction(load_layout_action)
-        layout_menu.addAction(load_default_layout_action)
-        layout_menu.addAction(save_layout_action)
-        layout_menu.addAction(self.lock_layout_action)
-        layout_menu.addAction(self.signal_vis_action)
-        layout_menu.addAction(self.port_vis_action)
-
-        # Create breakpoint window action
-        self.breakpoint_action = QAction("Breakpoints", self)
-        self.breakpoint_action.setStatusTip("Open breakpoint window.")
-        self.breakpoint_action.triggered.connect(self.openBreakpointWindow)
-
-        # Create 'update value' window button
-        self.update_value_action = QAction(
-            "Update values while running", self, checkable=True
+        # Connect signals for managing the processor layout
+        self._menu_bar.load_layout.connect(self._graphics_scene.load_layout)
+        self._menu_bar.load_default_layout.connect(
+            self._graphics_scene.load_default_layout
         )
-        self.update_value_action.setChecked(False)
-        self.update_value_action.setStatusTip("Toggle value updates while running.")
-        self.update_value_action.triggered.connect(self.toggle_value_update_on_run)
-
-        # Create 'set delay' window button
-        self.set_delay_action = QAction("Set update delay", self)
-        self.set_delay_action.setStatusTip(
-            "Sets the delay between each update when the cpu is running."
+        self._menu_bar.save_layout.connect(self._graphics_scene.save_layout)
+        self._menu_bar.lock_layout.connect(self._graphics_scene.toggle_layout_lock)
+        self._menu_bar.show_all_signals.connect(self._graphics_scene.show_all_signals)
+        self._menu_bar.show_port_names.connect(self._graphics_scene.show_port_names)
+
+        # Connect signal for managing breakpoints
+        self._menu_bar.show_breakpoints.connect(self.openBreakpointWindow)
+
+        self._processor_handler.running.connect(
+            self._menu_bar.set_disabled_when_running
         )
-        self.set_delay_action.triggered.connect(self.set_update_delay)
-
-        # Create Tools menu for tool actions actions
-        tools_menu = menu.addMenu("&Tools")
-        tools_menu.addAction(self.breakpoint_action)
-        tools_menu.addAction(self.update_value_action)
-        tools_menu.addAction(self.set_delay_action)
-
-        # Add run button on toolbar
-        arrow_icon = self.style().standardIcon(QStyle.SP_MediaPlay)
-        self.run_action = QAction(arrow_icon, "Run", self)
-        self.run_action.setStatusTip("Run until halt")
-        self.run_action.triggered.connect(self.runToolBarButtonClick)
-        toolbar.addAction(self.run_action)
-
-        # Add stop button on toolbar
-        stop_icon = self.style().standardIcon(QStyle.SP_MediaStop)
-        self.stop_action = QAction(stop_icon, "Stop", self)
-        self.stop_action.setStatusTip("Stop running")
-        self.stop_action.triggered.connect(self.stopToolBarButtonClick)
-        toolbar.addAction(self.stop_action)
-
-        # Add Asm label
-        self.clock_cycle_label = QLabel("Clock cycle: ", self)
-        toolbar.addWidget(self.clock_cycle_label)
-
-        # Add undo button on toolbar
-        backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward)
-        self.undo_action = QAction(backward_arrow_icon, "Undo", self)
-        self.undo_action.setStatusTip("Undo the last processor tick")
-        self.undo_action.triggered.connect(self.undoToolBarButtonClick)
-        toolbar.addAction(self.undo_action)
-
-        # Add step button on toolbar
-        forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward)
-        self.step_action = QAction(forward_arrow_icon, "Step", self)
-        self.step_action.setStatusTip("Run one clock cycle")
-        self.step_action.triggered.connect(self.stepToolBarButtonClick)
-        toolbar.addAction(self.step_action)
-
-        # Add box for jump value
-        self.jump_value_box = QSpinBox()
-        self.jump_value_box.setMinimum(999999)
-        self.jump_value_box.setMinimum(1)
-        self.jump_value_box.setValue(1)
-        toolbar.addWidget(self.jump_value_box)
-
-        # Add seperator so clock gets better spacing
-        spacing = QWidget()
-        spacing.setFixedWidth(10)
-        toolbar.addWidget(spacing)
-        toolbar.addSeparator()
-        spacing = QWidget()
-        spacing.setFixedWidth(10)
-        toolbar.addWidget(spacing)
-
-        # Add clock counter
-        self.clock_label = QLabel("Clock cycles: " + str(self.cpu.get_clock()), self)
-        toolbar.addWidget(self.clock_label)
-
-        # Add seperator so clock gets better spacing
-        spacing = QWidget()
-        spacing.setFixedWidth(10)
-        toolbar.addWidget(spacing)
-        toolbar.addSeparator()
-        spacing = QWidget()
-        spacing.setFixedWidth(10)
-        toolbar.addWidget(spacing)
-
-        # Add Asm label
-        self.asm_label = QLabel("Assembler Instructions: ", self)
-        toolbar.addWidget(self.asm_label)
-
-        # Add undo asm button on toolbar
-        backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward)
-        self.undo_asm_action = QAction(backward_arrow_icon, "Undo Asm", self)
-        self.undo_asm_action.setStatusTip("Undo the last assembler instruction")
-        self.undo_asm_action.triggered.connect(self.undoAsmToolBarButtonClick)
-        toolbar.addAction(self.undo_asm_action)
-
-        # Add step button on toolbar
-        forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward)
-        self.step_asm_action = QAction(forward_arrow_icon, "Step Asm", self)
-        self.step_asm_action.setStatusTip("Run one assembler instruction")
-        self.step_asm_action.triggered.connect(self.stepAsmToolBarButtonClick)
-        toolbar.addAction(self.step_asm_action)
-
-        # Add box for jump value
-        self.asm_jump_value_box = QSpinBox()
-        self.asm_jump_value_box.setMinimum(999999)
-        self.asm_jump_value_box.setMinimum(1)
-        self.asm_jump_value_box.setValue(1)
-        toolbar.addWidget(self.asm_jump_value_box)
+
+    def _create_tool_bar(self) -> None:
+        """
+        Create a simulation toolbar and connect its signals to appropriate
+        slots of other widgets.
+        """
+        self._toolbar = SimulationToolBar("Main toolbar", self)
+        self.addToolBar(self._toolbar)
+        self._toolbar.run.connect(self._processor_handler.run_simulation)
+        self._toolbar.stop.connect(self._processor_handler.stop_simulation)
+        self._toolbar.step.connect(self._processor_handler.step_cycles)
+        self._toolbar.undo.connect(self._processor_handler.undo_cycles)
+        self._toolbar.step_asm.connect(self._processor_handler.step_asm_instructions)
+        self._toolbar.undo_asm.connect(self._processor_handler.undo_asm_instructions)
+        self._processor_handler.cycle_changed.connect(self._toolbar.update_clock)
+        self._processor_handler.running.connect(self._toolbar.set_disabled_when_running)
 
     def connectModuleActions(self, action_signals: []) -> None:
         """
@@ -357,50 +145,30 @@ class GUI(QMainWindow):
                     # LambdaBPs are not tested since we dont
                     # actually have any modules that make them yet
                     signal.connect(self.addLambdaBreakpoint)
-                case "UPDATE":
-                    signal.connect(self.updateCpuListeners)
-                case "CLOCKUPDATE":
-                    signal.connect(self.updateCpuClockCycle)
 
     def init_pipeline(self) -> None:
-        """Initialize the pipeline diagram. 
+        """Initialize the pipeline diagram.
 
         Sets its height, width and instructions.
         """
-        size = self.cpu.get_pipeline_dimensions()
-
-        self.pipeline.set_height(size[0])
-        self.pipeline.set_width(size[1])
-        self.update_pipeline()
+        size = self._processor.get_pipeline_dimensions()
 
-        self.pipeline.show()
+        self._pipeline.set_height(size[0])
+        self._pipeline.set_width(size[1])
 
-    def updateCpuListeners(self) -> None:
-        """
-        Updates the graphics items in the scene, the clock, and the pipeline diagram.
-
-        Used after the cpu has run or when the user has edited somehting.
-        """
-        self.cpu_graphics_scene.updateGraphicsItems()
-        self.updateCpuClockCycle()
-        self.update_pipeline()
-
-    def update_pipeline(self) -> None:
-        self.pipeline.set_instructions(self.cpu.get_current_instructions())
-
-    def updateCpuClockCycle(self) -> None:
-        """
-        Update the clock cycle counter.
-
-        Used while the program is running to show the user nothing has crashed.
-        """
-        self.clock_label.setText("Clockcycle: " + str(self.cpu.get_clock()))
+        self._processor_handler.changed_instruction.connect(
+            self._pipeline.set_instructions
+        )
+        # TODO: Find prettier way of making the processor handler emit its
+        # signal for the current processor instructions
+        self._processor_handler._signal_processor_changed()
+        self._pipeline.show()
 
     def lambdaBreakpointDialog(self) -> None:
         """
         Opens dialog window for user to create a breakpoint.
         """
-        lambdas = self.cpu.get_breakpoint_lambdas()
+        lambdas = self._processor.get_breakpoint_lambdas()
         lambda_br_dialog = LambdaBreakpointDialog(lambdas, self)
         lambda_br_dialog.accepted.connect(self.addLambdaBreakpoint)
 
@@ -409,8 +177,8 @@ class GUI(QMainWindow):
         Updates the breakpoint window when new breakpoints are added.
         """
         # Don't do anything if window is closed
-        if self.breakpoint_window is not None:
-            self.breakpoint_window.update()
+        if self._breakpoint_window is not None:
+            self._breakpoint_window.update()
 
     """
     @Slot is used to explicitly mark a python method as a Qt slot
@@ -428,13 +196,13 @@ class GUI(QMainWindow):
         try:
             parsed_value = ast.literal_eval(value)
         except SyntaxError as e:
-            self.errorMessageWidget.showMessage(str(e))
+            self._error_msg_widget.showMessage(str(e))
         else:
-            module = self.cpu.get_module(module_name)
+            module = self._processor.get_module(module_name)
             module_state = module.get_state()
             module_state[state] = parsed_value
             module.set_state(module_state)
-            self.cpu_graphics_scene.updateGraphicsItems()
+            self._graphics_scene.update_modules()
 
     @Slot(str, str, str)
     def editMemoryContent(self, module_name: str, adress: str, value: str) -> None:
@@ -447,17 +215,17 @@ class GUI(QMainWindow):
             parsed_adress = int(adress, 16)
             parsed_value = ast.literal_eval(value)
         except SyntaxError as e:
-            self.errorMessageWidget.showMessage(str(e))
+            self._error_msg_widget.showMessage(str(e))
         except ValueError:
-            self.errorMessageWidget.showMessage(
+            self._error_msg_widget.showMessage(
                 "You must enter a hexadecimal" "number preceeded by '0x' (e.g." "0xc3)."
             )
         else:
-            module = self.cpu.get_module(module_name)
+            module = self._processor.get_module(module_name)
             module_state = module.get_state()
             module_state['memory'][parsed_adress] = parsed_value
             module.set_state(module_state)
-            self.cpu_graphics_scene.updateGraphicsItems()
+            self._graphics_scene.update_modules()
 
     @Slot(str, str, str)
     def addStateBreakpoint(self, module_name: str, state: str, value: str) -> None:
@@ -466,10 +234,10 @@ class GUI(QMainWindow):
         """
         try:
             parsed_value = ast.literal_eval(value)
-            self.cpu.add_state_breakpoint(module_name, state, parsed_value)
+            self._processor.add_state_breakpoint(module_name, state, parsed_value)
             self.updateBreakpointWindow()
         except (ValueError, SyntaxError) as e:
-            self.errorMessageWidget.showMessage(str(e))
+            self._error_msg_widget.showMessage(str(e))
 
     @Slot(str, str)
     def addLambdaBreakpoint(self, lambda_name, kwargs_str) -> None:
@@ -481,10 +249,10 @@ class GUI(QMainWindow):
             for kwarg in kwargs_str.split(' '):
                 key, value = kwarg.split('=')
                 lambda_kwargs[key] = ast.literal_eval(value)
-            self.cpu.add_lambda_breakpoint(lambda_name, **lambda_kwargs)
+            self._processor.add_lambda_breakpoint(lambda_name, **lambda_kwargs)
             self.updateBreakpointWindow()
         except (ValueError, SyntaxError) as e:
-            self.errorMessageWidget.showMessage(str(e))
+            self._error_msg_widget.showMessage(str(e))
 
     @Slot(str, str, str)
     def addMemoryBreakpoint(self, module_name: str, adress: str, value: str) -> None:
@@ -494,498 +262,56 @@ class GUI(QMainWindow):
         try:
             parsed_adress = int(adress)
             parsed_value = int(value)
-            self.cpu.add_memory_breakpoint(module_name, parsed_adress, parsed_value)
+            self._processor.add_memory_breakpoint(
+                module_name, parsed_adress, parsed_value
+            )
             self.updateBreakpointWindow()
         except SyntaxError as e:
-            self.errorMessageWidget.showMessage(str(e))
-
-    def setDisabledWhenRunning(self, is_disable):
-        """
-        Greys out buttons for actions that can't be done while cpu is running.
-        """
-        self.load_action.setDisabled(is_disable)
-        self.save_action.setDisabled(is_disable)
-        self.reset_action.setDisabled(is_disable)
-        self.run_action.setDisabled(is_disable)
-        self.step_action.setDisabled(is_disable)
-        self.undo_action.setDisabled(is_disable)
-
-    def stepToolBarButtonClick(self):
-        """
-        Runs the cpu a specified number of clock cycles according to the jump value box.
-        """
-
-        # Don't do steps if cpu is running
-        if self.cpu_running:
-            return
-
-        steps = self.jump_value_box.value()
-        self.cpu_running = True
-        self.setDisabledWhenRunning(True)
-        simulation_thread = RunThread(
-            self.cpu, self.cpu_tick_signal, self.update_delay, False, False, steps
-        )
-        self.threadpool.start(simulation_thread)
-
-    def stepAsmToolBarButtonClick(self):
-        """
-        Runs the cpu a specified number of asm instructions according to the jump value box.
-        """
-
-        # Don't do steps if cpu is running
-        if self.cpu_running:
-            return
-
-        steps = self.asm_jump_value_box.value()
-        self.cpu_running = True
-        self.setDisabledWhenRunning(True)
-        simultaion_thread = RunThread(
-            self.cpu, self.cpu_tick_signal, self.update_delay, False, True, steps
-        )
-        self.threadpool.start(simultaion_thread)
-        self.updateCpuListeners()
-
-    def runToolBarButtonClick(self) -> None:
-        """
-        Runs the cpu until it is stopped by user input, breakpoint or similar.
-        """
-
-        # Don't run if already running
-        if self.cpu_running:
-            return
-
-        # Create own thread for cpu simulation so gui dosent freeze
-        self.cpu_running = True
-        self.setDisabledWhenRunning(True)
-        simulation_thread = RunThread(self.cpu, self.cpu_tick_signal, self.update_delay)
-        self.threadpool.start(simulation_thread)
-
-    @Slot(int)
-    def handleCpuTick(self, steps: int) -> None:
-        """
-        Called from other thread after every cpu tick.
-        Will inform the user and update visuals.
-        """
-
-        # Update cpu clock counter every tick
-        self.updateCpuClockCycle()
-
-        if self.update_all_values:
-            self.updateCpuListeners()
-
-        # A signal of 0 steps signifies end of execution, i.e. the CPU has
-        # halted or run the specified amount of ticks
-        # => Enable the relevant parts of the GUI again
-        if steps == 0:
-            self.cpu_running = False
-            self.setDisabledWhenRunning(False)
-            self.updateCpuListeners()
-
-            # Inform user of reached break point
-            if self.cpu.breakpoint_reached:
-                self.messageBox(
-                    "Reached breakpoint: " + self.cpu.last_breakpoint.__str__()
-                )
-
-            # Inform user of halt
-            if self.cpu.should_halt():
-                self.messageBox("The processor halted.")
-
-    def stopToolBarButtonClick(self) -> None:
-        """
-        Tells the cpu to stop. It will then stop at an appropriate in its own thread.
-        """
-        self.cpu.stop()
-
-    def folderSaveDialog(self) -> str:
-        """
-        Open a file explorer in a new window. Return the absolute path to the selected file.
-
-        Can return existing as well as non-existing files.
-        If the selected files does not exist it will be created.
-        """
-        dialog = QFileDialog()
-        dialog.setFileMode(QFileDialog.AnyFile)
-        dialog.setAcceptMode(QFileDialog.AcceptOpen)
-        dialog.setDirectory("~/simudator")  # TODO: does this work when exported?
-
-        # getSaveFileName() can returncan return existing file but also create new ones
-        return dialog.getSaveFileName()[0]
-
-    def folderLoadDialog(self) -> str:
-        """
-        Open a file explorer in a new window. Return the absolute path to the selected file.
-
-        Can only return existing files.
-        """
-        dialog = QFileDialog()
-        dialog.setFileMode(QFileDialog.AnyFile)
-        dialog.setAcceptMode(QFileDialog.AcceptOpen)
-        dialog.setDirectory("~/simudator")  # TODO: does this work when exported?
-
-        # getOpenFileName() will only return already existing files
-        return dialog.getOpenFileName()[0]
-
-    def loadToolBarButtonClick(self) -> None:
-        """
-        Loads the processor state from selected file.
-        """
-
-        # not safe to load while cpu is running
-        if self.cpu_running:
-            return
-
-        path = self.folderLoadDialog()
-        try:
-            self.cpu.load_state_from_file(path)
-        except KeyError:
-            self.errorBox("Could not load selected file.")
-        except ValueError:
-            self.errorBox("Selected file was empty.")
-        except FileNotFoundError:
-            # This error is triggered when no file was selected by
-            # by the user. The user should know they did not select a
-            # file and therefore there no need to display a box telling
-            # them that.
-            pass
-            # self.errorBox("No vaild file was selected.")
-        else:
-            self.messageBox("Loaded file successfully.")
-            self.updateCpuListeners()
-
-    def resetMenuButtonClick(self) -> None:
-        """
-        Will reset processor to inital values.
-        """
-
-        # not safe to reset while cpu running
-        if self.cpu_running:
-            return
-
-        answer = QMessageBox.question(
-            self, "Reset Processor", "Are you sure you want to reset the processor?"
-        )
-
-        if answer == QMessageBox.Yes:
-            self.cpu.reset()
-            self.updateCpuListeners()
-
-    def undoNToolBarButtonClick(self) -> None:
-        # TODO: I dont think this is used
-        """
-        Undo zero to N cycles.
-        """
-
-        # Don't try to undo while running
-        if self.cpu_running:
-            return
-
-        cycles, ok = QInputDialog(self).getInt(
-            self,
-            "Input number of cycles to run",
-            "Input number of cycles to run",
-        )
-        if ok:
-            if cycles < 1:
-                self.errorBox("Please input a number larger than 0.")
-                return
-            try:
-                # Make sure we load 0 as lowest
-                self.cpu.load_cycle(max(self.cpu.get_clock() - cycles, 0))
-                self.updateCpuListeners()
-            except (ValueError, IndexError):
-                self.errorBox("Unable to undo the cycle(s).")
-
-    def saveLayoutToolBarButtonClick(self) -> None:
-        """
-        Saves the layout of all the modules in a (somewhat) human readable format.
-
-        This also erases the previous content of the file before saving
-        the data.
-        """
-        path = self.folderSaveDialog()
-
-        # Change file to the selected file if a file was selected
-        if path == '':
-            return
-
-        # Open the given file erasing the previous content
-        with open(path, "w") as fp:
-            graphics_modules_data = {}
-            ports_data = {}
-            graphics_signals_data = {}
-
-            graphics_modules = self.cpu_graphics_scene.getModulesGraphicsItems()
-            for graphics_module in graphics_modules:
-                pos = (graphics_module.x(), graphics_module.y())
-                graphics_modules_data[graphics_module.getName()] = pos
-
-                for port in graphics_module.getPorts():
-                    visibility = port.isVisible()
-                    orientation = int(port.getOrientation())
-                    data = (port.x(), port.y(), orientation, visibility)
-                    ports_data[port.getID()] = data
-
-            for graphics_signal in self.cpu_graphics_scene.getSignalGraphicsItems():
-                visibility = graphics_signal.isVisible()
-                points = []
-                for point in graphics_signal.getPoints():
-                    points.append((point.x(), point.y()))
-                data = (points, visibility)
-                graphics_signals_data[graphics_signal.getID()] = data
-
-            data = (graphics_modules_data, ports_data, graphics_signals_data)
-            json.dump(data, fp)
-
-        fp.close()
-
-    def loadDefaultLayoutToolBarButtonClick(self) -> None:
-        """
-        Places all the module_graphic objects in a diagonal going down to the right.
-        """
-        counter = 0
-        for key in self.cpu_graphics_scene.module_graphics_items:
-            module_graphic = self.cpu_graphics_scene.module_graphics_items[key]
-            module_graphic.showPorts()
-            counter += 1
-            self.cpu_graphics_scene.replaceModuleGraphicsItem(
-                module_graphic, counter, counter
-            )
-        self.cpu_graphics_scene.resetSignals()
-
-    def loadLayoutFromFile(self, file) -> None:
-        """
-        Loads a layout for the current cpu from the selected file.
-
-        If at anypoint this funciton would error, the default layout
-        is loaded instead.
-        """
-        # TODO: speed(?)
-        graphics_modules = self.cpu_graphics_scene.moduleGraphicsItemsDict()
-        ports = {}
-        graphics_signals = {}
-
-        for graphics_module in graphics_modules.values():
-            for port in graphics_module.getPorts():
-                ports[port.getID()] = port
-
-        for graphics_signal in self.cpu_graphics_scene.getSignalGraphicsItems():
-            graphics_signals[graphics_signal.getID()] = graphics_signal
-
-        # Open the file in 'read-only'
-        with open(file, 'rb') as fp:
-            data = json.load(fp)
-            graphics_modules_data = data[0]
-            ports_data = data[1]
-            graphics_signals_data = data[2]
-
-            for g_module_name, g_module_data in graphics_modules_data.items():
-                g_module = graphics_modules[g_module_name]
-                x = g_module_data[0]
-                y = g_module_data[1]
-                self.loadGraphicsModule(g_module, x, y)
-
-            for port_id, port_data in ports_data.items():
-                port = ports[int(port_id)]
-                self.loadPort(port, *port_data)
-
-            for g_signal_id, g_signal_data in graphics_signals_data.items():
-                g_signal = graphics_signals[int(g_signal_id)]
-                self.loadSignal(g_signal, *g_signal_data)
-
-        fp.close()
-
-    def loadSignal(
-        self,
-        graphic_signal: SignalGraphicsItem,
-        signal_points: list[tuple[float, float]],
-        visibility: bool,
-    ) -> None:
-        """
-        Changes the graphical signal to have the positions given as argument.
-        """
-        qpoints = []
-        # Turn points -> QPointF
-        # list[int, int] -> QPointF object
-        for point in signal_points:
-            qpoints.append(QPointF(point[0], point[1]))
-
-        # Set the new points
-        graphic_signal.setPoints(qpoints)
-        graphic_signal.setVisible(visibility)
-
-    def loadGraphicsModule(
-        self,
-        graphics_module: ModuleGraphicsItem,
-        graphics_module_x: float,
-        graphics_module_y: float,
-    ) -> None:
-        """
-        Changes the positions of graphical modules to the ones given as argument.
-        """
-        graphics_module.setX(graphics_module_x)
-        graphics_module.setY(graphics_module_y)
-
-    def loadPort(
-        self,
-        port: PortGraphicsItem,
-        x: float,
-        y: float,
-        orientation: Orientation,
-        visibility: bool,
-    ) -> None:
-        port.setOrientation(orientation)
-        port.setX(x)
-        port.setY(y)
-        port.setVisible(visibility)
-
-    def saveStateMenuButtonClick(self) -> None:
-        """
-        Save state of cpu to file.
-
-        This erases the previous content of the file before saving the data.
-        """
-
-        # Not safe to save while running
-        if self.cpu_running:
-            return
-
-        path = self.folderSaveDialog()
-
-        # Change file to the selected file if a file was selected
-        if path == '':
-            return
-
-        res = self.cpu.save_state_to_file(path)
-
-        if res:
-            self.messageBox("File saved.")
-        else:
-            self.errorBox("Unable to save.")
+            self._error_msg_widget.showMessage(str(e))
 
+    @Slot()
     def openBreakpointWindow(self) -> None:
         """
         Opens window for editing breakpoints.
         """
-        self.breakpoint_window = BreakpointWindow(self.cpu)
-        self.breakpoint_window.show()
+        self._breakpoint_window = BreakpointWindow(self._processor)
+        self._breakpoint_window.show()
 
-    def toggle_value_update_on_run(self):
-        """
-        Toggles whether all values or only clock cycle is being updated each tick.
-        """
-        self.update_all_values = not self.update_all_values
-
-    def set_update_delay(self):
-        """
-        Sets the update delay for the visual updates while the cpu is running.
-        """
-        delay, ok = QInputDialog.getDouble(
-            self, "Input Dialog", "Enter a float value:", decimals=5
-        )
-        if ok:
-            self.update_delay = delay
-
-    def showPortNamesBarButtonClick(self):
-        """
-        Toggles showing port names in the graphics scene.
-        """
-        self.cpu_graphics_scene.setPortNamesVisibility(self.port_vis_action.isChecked())
-
-    def showSignalsMenuButtonClick(self) -> None:
-        """
-        Toggle shoing the signals in the graphics scene.
-        """
-        self.cpu_graphics_scene.setAllSignalsVisibility(
-            self.signal_vis_action.isChecked()
-        )
-
-    def toggleLayoutLockMenuButtonClick(self) -> None:
-        """
-        Toggles so the layout can not be edited.
-        """
-        self.cpu_graphics_scene.setLayoutLock(self.lock_layout_action.isChecked())
-
-    def loadLayoutToolBarButtonClick(self) -> None:
-        """
-        Loads a given layout from a selected file.
-
-        If the layout was
-        unable to load, an error message will pop up informing the user
-        and the default layout will be loaded.
-        """
-
-        path = self.folderLoadDialog()
-
-        # If no file was selected, do nothing
-        if path == '':
-            return
-
-        try:
-            self.loadLayoutFromFile(path)
-
-        # Anything goes wrong with loading the selected one,
-        # we dont care about what went wrong
-        except (OSError, JSONDecodeError):
-            self.loadDefaultLayoutToolBarButtonClick()
-            self.errorBox("Unable to load given file.")
-        else:
-            self.messageBox("File loaded successfully.")
-
-    def errorBox(self, message="Something went wrong.") -> None:
-        """
-        Displays a simple box with the given error message.
-        """
-
-        dlg = QMessageBox(self)
-        dlg.setWindowTitle("Error")
-        dlg.setText(message)
-        dlg.exec()
-
-    def messageBox(self, message="Something happend.") -> None:
+    def messageBox(self, message, title="Message") -> None:
         """
         Displays a simple box with the given message.
         """
-
         dlg = QMessageBox(self)
-        dlg.setWindowTitle("Message")
+        dlg.setWindowTitle(title)
         dlg.setText(message)
         dlg.exec()
 
-    def undoToolBarButtonClick(self) -> None:
+    def add_module_graphics_item(self, item: ModuleGraphicsItem) -> None:
         """
-        Undos as many processor cycles as the number entered in the box.
+        Add a module graphics item to the graphics scene and connect its
+        QT signals and slots.
         """
-        try:
-            steps = self.jump_value_box.value()
-            self.cpu.load_cycle(max(self.cpu.get_clock() - steps, 0))
-            self.updateCpuListeners()
-        except (ValueError, IndexError):
-            self.errorBox("Unable to undo the cycle.")
+        self._graphics_scene.add_module(item)
+        self.connectModuleActions(item.getActionSignals())
+        self._processor_handler.changed.connect(item.update)
 
-    def undoAsmToolBarButtonClick(self) -> None:
+    def add_all_signals(self) -> None:
         """
-        Undos as many processor cycles as the number entered in the box.
+        Add visual representations of all processor signals between all modules
+        added to the GUI.
         """
-        try:
-            steps = self.asm_jump_value_box.value()
-            self.cpu.undo_asm_instruction(steps)
-            self.updateCpuListeners()
-        except (ValueError, IndexError):
-            self.errorBox("Unable to undo the cycle.")
+        self._graphics_scene.add_all_signals()
 
-    def addModuleGraphicsItem(self, graphics_item: ModuleGraphicsItem) -> None:
-        """
-        Adds an item to the graphics scene.
-        """
-        self.cpu_graphics_scene.addModuleGraphicsItem(graphics_item)
-        self.connectModuleActions(graphics_item.getActionSignals())
+    def load_layout(self, file_path: str) -> None:
+        """Load a processor layout from file.
+
+        Parameters
+        ----------
+        file_path : str
+            Path to the file containing a processor layout to load.
 
-    def addAllSignals(self) -> None:
-        """
-        Add signals depending on modules in the graphics scene.
         """
-        self.cpu_graphics_scene.addAllSignals()
+        self._graphics_scene.load_layout_from_file(file_path)
 
 
 if __name__ == '__main__':
diff --git a/src/simudator/gui/menu_bar.py b/src/simudator/gui/menu_bar.py
new file mode 100644
index 0000000000000000000000000000000000000000..29183a9df0ef1db0a99dd035a3407b83e93eead9
--- /dev/null
+++ b/src/simudator/gui/menu_bar.py
@@ -0,0 +1,192 @@
+import typing
+
+from PyQt5.QtWidgets import QAction
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtWidgets import QMenuBar, QWidget
+
+
+class MainMenuBar(QMenuBar):
+    """
+    Main menu bar of the simudator GUI. Contains submenus for file I/O,
+    layout managing and miscellaneous tools.
+
+    Parameters
+    ----------
+    parent : QWidget | None
+        Parent of the widget. The widget will be a new window if it is ``None``
+        (default).
+    """
+
+    load = pyqtSignal()
+    """
+    QT signal emitted when the user has requested to load a processor state.
+    """
+
+    save = pyqtSignal()
+    """
+    QT signal emitted when the user has requested to save the processor state.
+    """
+
+    reset = pyqtSignal()
+    """
+    QT signal emitted when the user has request to reset the processor state.
+    """
+
+    load_layout = pyqtSignal()
+    """
+    QT signal emitted when the user has request to load a processor layout.
+    """
+
+    load_default_layout = pyqtSignal()
+    """
+    QT signal emitted when the user has request to load the default processor
+    layout.
+    """
+
+    save_layout = pyqtSignal()
+    """
+    QT signal emitted when the user has request to save the processor layout.
+    """
+
+    lock_layout = pyqtSignal(bool)
+    """
+    QT signal emitted when the user has request to toggle the lock of editing
+    the processor layout.
+    """
+
+    show_breakpoints = pyqtSignal()
+    """
+    QT signal emitted when the user has requested to show and manage
+    the processor breakpoints.
+    """
+
+    show_all_signals = pyqtSignal(bool)
+    """
+    QT signal emitted when the user has requested to toggle showing all
+    processor signals.
+    """
+
+    show_port_names = pyqtSignal(bool)
+    """
+    QT signal emitted when the user has requested to toggle showing the names
+    of processor module ports.
+    """
+
+    update_all_values = pyqtSignal(bool)
+    """
+    QT signal emitted when the user has requested to see all values of
+    processor modules update in real time when running the processor.
+    """
+
+    set_delay = pyqtSignal()
+    """
+    QT signal emitted when the user has requested to set the delay between
+    clock cycles when running the processor.
+    """
+
+    def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
+        super().__init__(parent)
+
+        # Create load action
+        self._load_action = QAction("Load", self)
+        self._load_action.setStatusTip("Load processor state from file")
+        self._load_action.triggered.connect(self.load)
+
+        # Create save action
+        self._save_action = QAction("Save", self)
+        self._save_action.setStatusTip("Save processor state to file")
+        self._save_action.triggered.connect(self.save)
+
+        self._reset_action = QAction("Reset", self)
+        self._reset_action.setStatusTip("Reset processor")
+        self._reset_action.triggered.connect(self.reset)
+
+        # Create File menu for load and save
+        file_menu = self.addMenu("&File")
+        file_menu.addAction(self._load_action)
+        file_menu.addAction(self._save_action)
+        file_menu.addAction(self._reset_action)
+
+        # create load layout action
+        load_layout_action = QAction("Load layout", self)
+        load_layout_action.setStatusTip("Loads the selected layout from file.")
+        load_layout_action.triggered.connect(self.load_layout)
+
+        # create load default layout action
+        load_default_layout_action = QAction("Load default layout", self)
+        load_default_layout_action.setStatusTip("Loads the default layout from file.")
+        load_default_layout_action.triggered.connect(self.load_default_layout)
+
+        # create save layout action
+        save_layout_action = QAction("Save layout", self)
+        save_layout_action.setStatusTip("Saves the current layout to a file.")
+        save_layout_action.triggered.connect(self.save_layout)
+
+        # create layout lock action
+        self._lock_layout_action = QAction("Lock layout", self, checkable=True)
+        self._lock_layout_action.setStatusTip("Lock layout so items can't be moved.")
+        self._lock_layout_action.triggered.connect(self.lock_layout)
+
+        # Create show signals actions
+        self._signal_vis_action = QAction("Show signals", self, checkable=True)
+        self._signal_vis_action.setChecked(True)
+        self._signal_vis_action.setStatusTip("Toggle the visibility of signal.")
+        self._signal_vis_action.triggered.connect(self.show_all_signals)
+
+        # Create show port name actions
+        self._port_vis_action = QAction("Show port names", self, checkable=True)
+        self._port_vis_action.setChecked(True)
+        self._port_vis_action.setStatusTip("Toggle the visibility of port names.")
+        self._port_vis_action.triggered.connect(self.show_port_names)
+
+        # Create Layout menu for layout actions
+        layout_menu = self.addMenu("&Layout")
+        layout_menu.addAction(load_layout_action)
+        layout_menu.addAction(load_default_layout_action)
+        layout_menu.addAction(save_layout_action)
+        layout_menu.addAction(self._lock_layout_action)
+        layout_menu.addAction(self._signal_vis_action)
+        layout_menu.addAction(self._port_vis_action)
+
+        # Create breakpoint window action
+        self._breakpoint_action = QAction("Breakpoints", self)
+        self._breakpoint_action.setStatusTip("Open breakpoint window.")
+        self._breakpoint_action.triggered.connect(self.show_breakpoints)
+
+        # Create 'update value' window button
+        self._update_value_action = QAction(
+            "Update values while running", self, checkable=True
+        )
+        self._update_value_action.setChecked(False)
+        self._update_value_action.setStatusTip("Toggle value updates while running.")
+        self._update_value_action.triggered.connect(self.update_all_values)
+
+        # Create 'set delay' window button
+        self._set_delay_action = QAction("Set update delay", self)
+        self._set_delay_action.setStatusTip(
+            "Sets the delay between each update when the cpu is running."
+        )
+        self._set_delay_action.triggered.connect(self.set_delay)
+
+        # Create Tools menu for tool actions actions
+        tools_menu = self.addMenu("&Tools")
+        tools_menu.addAction(self._breakpoint_action)
+        tools_menu.addAction(self._update_value_action)
+        tools_menu.addAction(self._set_delay_action)
+
+    @Slot(bool)
+    def set_disabled_when_running(self, value: bool) -> None:
+        """
+        Toggle the menu bar between enabled and disabled. When disabled,
+        actions which should not be performed while a processor is running
+        are disabled and greyed out.
+
+        Parameters
+        ----------
+        value : bool
+            ``True`` to disable the menu bar, ``False`` to enable it.
+        """
+        self._load_action.setDisabled(value)
+        self._save_action.setDisabled(value)
+        self._reset_action.setDisabled(value)
diff --git a/src/simudator/gui/module_graphics_item/memory_graphic.py b/src/simudator/gui/module_graphics_item/memory_graphic.py
index 563036d2ddb51ecdda681c6aad9cb83bf47c5954..c4a944d213270e0f99330c5dede4d914dfc2a611 100644
--- a/src/simudator/gui/module_graphics_item/memory_graphic.py
+++ b/src/simudator/gui/module_graphics_item/memory_graphic.py
@@ -340,7 +340,8 @@ class MemoryGraphicsItem(ModuleGraphicsItem):
             module_state = self.module.get_state()
             module_state['memory'][parsed_address] = value
             self.module.set_state(module_state)
-            self.update_graphics_signal.emit()
+            self.module_edited.emit()
+            self.update()
 
     def getActionSignals(self) -> []:
         # Do parent signals and then add memory specific signals
diff --git a/src/simudator/gui/module_graphics_item/module_graphics_item.py b/src/simudator/gui/module_graphics_item/module_graphics_item.py
index 30ae67a723d2f6c1fd73596e917d49a19c63b8d6..155567816c45c4e13061b8cd93931d2b9c0ddd5c 100644
--- a/src/simudator/gui/module_graphics_item/module_graphics_item.py
+++ b/src/simudator/gui/module_graphics_item/module_graphics_item.py
@@ -38,6 +38,8 @@ class ModuleGraphicsItem(QGraphicsObject, QGraphicsItem):
     new_state_breakpoint_signal = pyqtSignal(str, str, str)
     update_graphics_signal = pyqtSignal()
 
+    module_edited = pyqtSignal()
+
     def __init__(self, module: Module, name: str = None):
         super().__init__()
 
@@ -190,8 +192,10 @@ class ModuleGraphicsItem(QGraphicsObject, QGraphicsItem):
             module_state[state] = parsed_value
             self.module.set_state(module_state)
             # Since we have changed a value we send a signal to the gui to update
-            self.update_graphics_signal.emit()
+            self.module_edited.emit()
+            self.update()
 
+    @Slot()
     def update(self):
         """
         Update the visuals of the graphics item to match it's module.
diff --git a/src/simudator/gui/pipeline.py b/src/simudator/gui/pipeline.py
index 16dbfc1261bf81b74e6b4aaaee42c2a42bc7f7e6..3ba4ffea08234d266b38e1b1526f6803c6059432 100644
--- a/src/simudator/gui/pipeline.py
+++ b/src/simudator/gui/pipeline.py
@@ -1,5 +1,6 @@
 from typing import Any
 
+from qtpy.QtCore import Slot
 from qtpy.QtWidgets import QTableWidget, QTableWidgetItem
 
 
@@ -15,6 +16,7 @@ class PipeLine(QTableWidget):
         super().__init__()
         self.setEditTriggers(self.NoEditTriggers)
 
+    @Slot(list)
     def set_instructions(self, instructions: list[tuple[str, int, int]]) -> None:
         """
         Give the pipeline the current CPU instructions.
diff --git a/src/simudator/gui/processor_handler.py b/src/simudator/gui/processor_handler.py
new file mode 100644
index 0000000000000000000000000000000000000000..91612fd9a6e5af44e70f145e0745729c632d44a7
--- /dev/null
+++ b/src/simudator/gui/processor_handler.py
@@ -0,0 +1,335 @@
+from qtpy import QtCore
+from qtpy.QtCore import QObject, QThreadPool
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtWidgets import QErrorMessage, QFileDialog, QInputDialog, QMessageBox
+
+from simudator.core.processor import Processor
+from simudator.gui.run_continuously_thread import RunThread
+
+
+class ProcessorHandler(QObject):
+    """
+    Wrapper class for interacting with the processor in the GUI.
+
+    Parameters
+    ----------
+    processor : Processor
+    parent : QObject | None
+        Optional parent QObject that takes ownership of this QObject.
+    """
+
+    running = pyqtSignal(bool)
+    """
+    PyQT signal emitted when the processor has started or finished running. 
+    Emits ``True`` when it has started and ``False`` when it has finished.
+    """
+    cycle_changed = pyqtSignal(int)
+    """
+    PyQT signal emitted when the current clock cycle count of the processor 
+    has changed. Emits the current clock cycle count as an ``int``.
+    """
+    changed = pyqtSignal()
+    """
+    PyQT signal emitted when the processor has changed state.
+    """
+    changed_instruction = pyqtSignal(list)
+    """
+    PyQT signal emitted when the processor has changed the current instruction(s)
+    being executed. Emits a list of instructions.
+
+    See Also
+    --------
+    simudator.core.processor.get_current_instructions :
+        Method of the processor used to get instructions that are emitted.
+    """
+
+    def __init__(
+        self,
+        processor: Processor,
+        parent: QObject | None = None,
+    ) -> None:
+        super().__init__(parent)
+        self._processor = processor
+        # Threadpool for running cpu and gui concurrently
+        self._threadpool = QThreadPool.globalInstance()
+
+        # Used to set if all values in the gui should be updated each tick
+        # or only the clock counter
+        self._update_all_values = False
+
+        # Used to set the update delay
+        # Useful when watching the values being updated while running
+        self._update_delay = 0.00
+
+        # Used to lock some actions in ui when cpu is running in another thread
+        # Using the cpu's internal status directly could case problems
+        self._processor_running = False
+
+        self._error_msg_box = QErrorMessage()
+
+    def _signal_processor_changed(self) -> None:
+        """
+        Emit signals for signifying that the processor has changed its state.
+        """
+        self.cycle_changed.emit(self._processor.get_clock())
+        self.changed.emit()
+        self.changed_instruction.emit(self._processor.get_current_instructions())
+
+    @Slot(int)
+    def step_cycles(self, cycles: int):
+        """
+        Run some number of clock cycles of the processor.
+
+        Parameters
+        ----------
+        Number of clock cycles to perform.
+        """
+
+        # Don't do steps if cpu is running
+        if self._processor_running:
+            return
+
+        self._processor_running = True
+        self.running.emit(True)
+        simulation_thread = RunThread(
+            self._processor,
+            self._update_delay,
+            False,
+            False,
+            cycles,
+        )
+        simulation_thread.processor_tick.connect(self.on_processor_tick)
+        self._threadpool.start(simulation_thread)
+
+    @Slot(int)
+    def step_asm_instructions(self, instructions: int):
+        """
+        Run some numer of asm instructions.
+
+        Parameters
+        ----------
+        instructions : int
+            Number of asm instructions to perform.
+        """
+
+        # Don't do steps if cpu is running
+        if self._processor_running:
+            return
+
+        self._processor_running = True
+        self.running.emit(True)
+        simulation_thread = RunThread(
+            self._processor, self._update_delay, False, True, instructions
+        )
+        simulation_thread.processor_tick.connect(self.on_processor_tick)
+        self._threadpool.start(simulation_thread)
+
+    @Slot()
+    def run_simulation(self) -> None:
+        """
+        Run the processor continuously until it is stopped by user input,
+        reaches a breakpoint or halts on its own.
+        """
+
+        # Don't run if already running
+        if self._processor_running:
+            return
+
+        # Create own thread for cpu simulation so gui dosent freeze
+        self._processor_running = True
+        self.running.emit(True)
+        simulation_thread = RunThread(self._processor, self._update_delay)
+        simulation_thread.processor_tick.connect(self.on_processor_tick)
+        self._threadpool.start(simulation_thread)
+
+    @Slot(int)
+    def on_processor_tick(self, steps: int) -> None:
+        """
+        Called from other thread after every cpu tick.
+        Will inform the user and update visuals.
+        """
+
+        # Update cpu clock counter every tick
+        self.cycle_changed.emit(self._processor.get_clock())
+
+        if self._update_all_values:
+            self._signal_processor_changed()
+
+        # A signal of 0 steps signifies end of execution, i.e. the CPU has
+        # halted or run the specified amount of ticks
+        # => Enable the relevant parts of the GUI again
+        if steps == 0:
+            self._processor_running = False
+            self.running.emit(False)
+            self._signal_processor_changed()
+
+            # Inform user of reached break point
+            if self._processor.breakpoint_reached:
+                QMessageBox.information(
+                    self.parent(),
+                    "SimuDator",
+                    f"Reached breakpoint: {str(self._processor.last_breakpoint)}",
+                )
+
+            # Inform user of halt
+            if self._processor.should_halt():
+                QMessageBox.information(
+                    self.parent(), "SimuDator", "The processor halted."
+                )
+
+    @Slot()
+    def stop_simulation(self) -> None:
+        """
+        Stop the processor.
+        """
+        self._processor.stop()
+
+    def get_save_path(self) -> str:
+        """
+        Prompt the user for a file path meant for saving to file. Creates the
+        specified file if it does not exist.
+        """
+        dialog = QFileDialog()
+        dialog.setFileMode(QFileDialog.AnyFile)
+        dialog.setAcceptMode(QFileDialog.AcceptOpen)
+
+        return dialog.getSaveFileName()[0]
+
+    def get_file_path(self) -> str:
+        """
+        Prompt the user for a file path meant for loading the processor state.
+        """
+        dialog = QFileDialog()
+        dialog.setFileMode(QFileDialog.AnyFile)
+        dialog.setAcceptMode(QFileDialog.AcceptOpen)
+
+        return dialog.getOpenFileName()[0]
+
+    @Slot()
+    def load_state(self) -> None:
+        """
+        Load the processor state from selected file.
+        """
+
+        # not safe to load while cpu is running
+        if self._processor_running:
+            return
+
+        path = self.get_file_path()
+        try:
+            self._processor.load_state_from_file(path)
+        except KeyError:
+            self._error_msg_box.showMessage("Could not load selected file.", "load_err")
+        except ValueError:
+            self._error_msg_box.showMessage("Selected file was empty.", "load_empty")
+        except FileNotFoundError:
+            # This error is triggered when no file was selected by
+            # by the user. The user should know they did not select a
+            # file and therefore there no need to display a box telling
+            # them that.
+            # self.messageBox("No valid file was selected.")
+            pass
+        else:
+            QMessageBox.information(None, "SimuDator", "Loaded file successfully.")
+            self._signal_processor_changed()
+
+    @Slot()
+    def reset(self) -> None:
+        """
+        Reset the processor to initial values and signal the processor state
+        change.
+        """
+
+        # not safe to reset while cpu running
+        if self._processor_running:
+            return
+
+        answer = QMessageBox.question(
+            self.parent(),
+            "Reset Processor",
+            "Are you sure you want to reset the processor?",
+        )
+
+        if answer == QMessageBox.Yes:
+            self._processor.reset()
+            self._signal_processor_changed()
+
+    @Slot()
+    def save_state(self) -> None:
+        """
+        Save the state of the processor to file.
+
+        This erases the previous content of the file before saving the data.
+        """
+
+        # Not safe to save while running
+        if self._processor_running:
+            return
+
+        path = self.get_save_path()
+
+        # Change file to the selected file if a file was selected
+        if path == '':
+            return
+
+        res = self._processor.save_state_to_file(path)
+
+        if res:
+            QMessageBox.information(self.parent(), "SimuDator", "File saved.")
+        else:
+            self._error_msg_box.showMessage("Unable to save.", "save_err")
+
+    @Slot(int)
+    def undo_cycles(self, cycles: int) -> None:
+        """
+        Undo some number of processor clock cycles.
+
+        Parameters
+        ----------
+        cycles : int
+            Number of clock cycles to undo.
+        """
+        try:
+            self._processor.load_cycle(max(self._processor.get_clock() - cycles, 0))
+            self._signal_processor_changed()
+        except (ValueError, IndexError):
+            self._error_msg_box.showMessage("Unable to undo the cycle.")
+
+    @Slot(int)
+    def undo_asm_instructions(self, instructions: int) -> None:
+        """
+        Undo some number of asm instructions.
+
+        Parameters
+        ----------
+        instructions : int
+            Number of instructions to undo.
+        """
+        try:
+            self._processor.undo_asm_instruction(instructions)
+            self._signal_processor_changed()
+        except (ValueError, IndexError):
+            self._error_msg_box("Unable to undo the instruction.")
+
+    @Slot()
+    def toggle_value_update_on_run(self):
+        """
+        Toggle whether all values in the GUI related to the processor should be
+        updated after each clock cycle or after having run several cycles at a
+        time / running continuously. The clock cycle counter is always updated
+        after each tick regardless.
+        """
+        self._update_all_values = not self._update_all_values
+
+    @Slot()
+    def set_update_delay(self):
+        """
+        Set the delay waited between each consecutive clock cycle when running
+        several cycles or continuously. Prompts the user for the delay.
+        """
+        delay, ok = QInputDialog.getDouble(
+            None, "Input Dialog", "Enter a float value:", decimals=5
+        )
+        if ok:
+            self._update_delay = delay
diff --git a/src/simudator/gui/run_continuously_thread.py b/src/simudator/gui/run_continuously_thread.py
index 72d0fdcbb58e3352901f21c5fca428a17200f2d4..13368de4f10407cce26c1fb8cf2bb1bc46c10186 100644
--- a/src/simudator/gui/run_continuously_thread.py
+++ b/src/simudator/gui/run_continuously_thread.py
@@ -1,39 +1,54 @@
 import time
 
-from qtpy.QtCore import QRunnable
+from qtpy.QtCore import QObject, QRunnable
+from qtpy.QtCore import Signal as pyqtSignal
+
+
+class SignalHolder(QObject):
+    """
+    Helper class for RunThread as the thread cannot inherit from QObject to
+    have signals directly. Instead, an instance of this class is used to hold
+    signals for the thread.
+    """
+
+    processor_tick = pyqtSignal(int)
+
 
 class RunThread(QRunnable):
     """
-    This class is used to run the simulated cpu several ticks or continuously on
-    a seperate thread. This allows the user to interact with the GUI while the 
-    simulation is running.
+    This class is used to run the simulated cpu several ticks or continuously
+    on a separate thread. This allows the user to interact with the GUI while
+    the simulation is running.
 
-    After each CPU tick, this thread will emit to its given QT signal so that
-    the GUI can update itself and possibly inform the user of when the execution 
+    After each CPU tick, this thread will emit to its QT signal so that the GUI
+    can update itself and possibly inform the user of when the execution
     has halted.
     """
 
-    def __init__(self, cpu, signal, delay: float, run_continuously=True, 
-                 step_asm=False, steps=0):
+    def __init__(
+        self, cpu, delay: float, run_continuously=True, step_asm=False, steps=0
+    ):
+
         super().__init__()
         self.cpu = cpu
-        self.signal = signal
         self.run_continuously = run_continuously
         self.steps = steps
         self.delay = delay
         self.step_asm = step_asm
+        self.signals = SignalHolder()
+        self.processor_tick = self.signals.processor_tick
 
     def run(self):
         if self.step_asm:
 
             while self.steps > 0:
                 self.cpu.do_tick()
+                self.processor_tick.emit(1)
 
                 # We only care about asm instructions
                 if self.cpu.is_new_instruction():
                     self.steps -= 1
 
-                self.signal.emit(1)
                 time.sleep(self.delay)
 
                 if self.cpu.is_stopped:
@@ -44,18 +59,17 @@ class RunThread(QRunnable):
                 self.cpu.unstop()
                 while not self.cpu.is_stopped:
                     self.cpu.do_tick()
-                    self.signal.emit(1)
+                    self.processor_tick.emit(1)
                     time.sleep(self.delay)
 
             else:
                 for _ in range(self.steps):
                     self.cpu.do_tick()
-                    self.signal.emit(1)
+                    self.processor_tick.emit(1)
                     time.sleep(self.delay)
 
                     if self.cpu.is_stopped:
                         break
 
-
         # Signal end of execution as having run 0 ticks
-        self.signal.emit(0)
+        self.processor_tick.emit(0)
diff --git a/src/simudator/gui/simulation_toolbar.py b/src/simudator/gui/simulation_toolbar.py
new file mode 100644
index 0000000000000000000000000000000000000000..090627eaae00362cee96f3aa2f61159b6fc9faf7
--- /dev/null
+++ b/src/simudator/gui/simulation_toolbar.py
@@ -0,0 +1,193 @@
+from PyQt5.QtWidgets import QAction
+from qtpy import QtGui
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtWidgets import QLabel, QSpinBox, QStyle, QToolBar, QWidget
+
+
+class SimulationToolBar(QToolBar):
+    """
+    A toolbar for basic controls of a processor simulation. This includes
+    running and stopping the processor, stepping and undoing clock cycles and
+    stepping and undoing asm instructions.
+
+    Parameters
+    ----------
+    title : str
+        Title of the toolbar.
+    parent : QWidget | None
+        Parent of the widget. The widget will be a new window if it is ``None``
+        (default).
+    """
+
+    run = pyqtSignal()
+    """
+    QT signal emitted when the user has requested to run the processor
+    continuously.
+    """
+
+    stop = pyqtSignal()
+    """
+    QT signal emitted when the user has requested to stop the processor.
+    """
+
+    step = pyqtSignal(int)
+    """
+    QT signal emitted when the user has requested to step some clock cycles
+    of the processor.
+    """
+
+    undo = pyqtSignal(int)
+    """
+    QT signal emitted when the user has requested to undo some clock cycles of
+    the processor.
+    """
+
+    step_asm = pyqtSignal(int)
+    """
+    QT signal emitted when the user has requested to step some asm instructions
+    of the processor.
+    """
+
+    undo_asm = pyqtSignal(int)
+    """
+    QT signal emitted when the user has requested to undo some asm instructions
+    of the processor.
+    """
+
+    def __init__(self, title, parent: QWidget | None = None):
+        super().__init__(title, parent)
+        # Run continuously button
+        arrow_icon = self.style().standardIcon(QStyle.SP_MediaPlay)
+        self._run_action = QAction(arrow_icon, "Run", self)
+        self._run_action.setStatusTip("Run until halt")
+        self._run_action.triggered.connect(self.run.emit)
+        self.addAction(self._run_action)
+
+        # Stop button
+        stop_icon = self.style().standardIcon(QStyle.SP_MediaStop)
+        self._stop_action = QAction(stop_icon, "Stop", self)
+        self._stop_action.setStatusTip("Stop running")
+        self._stop_action.triggered.connect(self.stop.emit)
+        self.addAction(self._stop_action)
+
+        # Separator and spacing between basic controls and stepping clock cycles
+        spacing = QWidget()
+        spacing.setFixedWidth(10)
+        self.addWidget(spacing)
+        self.addSeparator()
+        spacing = QWidget()
+        spacing.setFixedWidth(10)
+        self.addWidget(spacing)
+
+        # Clock cycle counter
+        self._clock_cycle_label = QLabel("Clock cycles: 0", self)
+        self._clock_cycle_label.setMinimumWidth(150)
+        self.addWidget(self._clock_cycle_label)
+
+        # Undo clock cycle button
+        backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward)
+        self._undo_action = QAction(backward_arrow_icon, "Undo", self)
+        self._undo_action.setStatusTip("Undo the last processor tick")
+        self._undo_action.triggered.connect(self._signal_undo)
+        self.addAction(self._undo_action)
+
+        # Step clock cycle button
+        forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward)
+        self._step_action = QAction(forward_arrow_icon, "Step", self)
+        self._step_action.setStatusTip("Run one clock cycle")
+        self._step_action.triggered.connect(self._signal_step)
+        self.addAction(self._step_action)
+
+        # Spin box for the number of clock cycles to step/undo at a time
+        self._jump_value_box = QSpinBox()
+        self._jump_value_box.setMinimum(999999)
+        self._jump_value_box.setMinimum(1)
+        self._jump_value_box.setValue(1)
+        self.addWidget(self._jump_value_box)
+
+        # Separator and spacing between steping clock cycles and
+        # stepping asm instructions
+        spacing = QWidget()
+        spacing.setFixedWidth(10)
+        self.addWidget(spacing)
+        self.addSeparator()
+        spacing = QWidget()
+        spacing.setFixedWidth(10)
+        self.addWidget(spacing)
+
+        # Asm instruction counter
+        self._asm_label = QLabel("Assembler instructions: ", self)
+        self.addWidget(self._asm_label)
+
+        # Undo asm instruction button
+        backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward)
+        self._undo_asm_action = QAction(backward_arrow_icon, "Undo Asm", self)
+        self._undo_asm_action.setStatusTip("Undo the last assembler instruction")
+        self._undo_asm_action.triggered.connect(self._signal_undo_asm)
+        self.addAction(self._undo_asm_action)
+
+        # Step asm instruction button
+        forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward)
+        self._step_asm_action = QAction(forward_arrow_icon, "Step Asm", self)
+        self._step_asm_action.setStatusTip("Run one assembler instruction")
+        self._step_asm_action.triggered.connect(self._signal_step_asm)
+        self.addAction(self._step_asm_action)
+
+        # Spin box for the number of asm instructions to step/undo at a time
+        self._asm_jump_value_box = QSpinBox()
+        self._asm_jump_value_box.setMinimum(999999)
+        self._asm_jump_value_box.setMinimum(1)
+        self._asm_jump_value_box.setValue(1)
+        self.addWidget(self._asm_jump_value_box)
+
+    def contextMenuEvent(self, a0: QtGui.QContextMenuEvent | None) -> None:
+        # Override to remove the functionality of hiding the toolbar as there
+        # is currently no way of showing it again
+        pass
+
+    @Slot(int)
+    def update_clock(self, clock_cycle: int) -> None:
+        """
+        Set the displayed number of the clock cycle counter.
+
+        Parameters
+        ----------
+        clock_cycle : int
+            Number of clock cycles to display.
+        """
+        self._clock_cycle_label.setText(f"Clock cycles: {clock_cycle}")
+
+    @Slot(bool)
+    def set_disabled_when_running(self, value: bool) -> None:
+        """
+        Disable relevant parts of the toolbar related to controlling a
+        processor. Intended to be used to disable stepping functionality
+        while the processor is running.
+
+        All buttons except for the stop button are disabled. The disabled
+        buttons are greyed out and cannot be interacted with while the toolbar
+        is disabled.
+
+        Parameters
+        ----------
+        value : bool
+            ``True`` to disable the toolbar, ``False`` to enable it.
+        """
+        self._run_action.setDisabled(value)
+        self._step_action.setDisabled(value)
+        self._undo_action.setDisabled(value)
+        self._step_asm_action.setDisabled(value)
+        self._undo_asm_action.setDisabled(value)
+
+    def _signal_step(self):
+        self.step.emit(self._jump_value_box.value())
+
+    def _signal_undo(self):
+        self.undo.emit(self._jump_value_box.value())
+
+    def _signal_step_asm(self):
+        self.step_asm.emit(self._asm_jump_value_box.value())
+
+    def _signal_undo_asm(self):
+        self.undo_asm.emit(self._asm_jump_value_box.value())
diff --git a/src/simudator/gui/view.py b/src/simudator/gui/view.py
new file mode 100644
index 0000000000000000000000000000000000000000..08e1b3535b69629ce2fa6227024ce9e95e71cb43
--- /dev/null
+++ b/src/simudator/gui/view.py
@@ -0,0 +1,49 @@
+from qtpy import QtCore, QtWidgets
+from qtpy.QtGui import QWheelEvent
+from qtpy.QtWidgets import QGraphicsView
+
+
+class View(QGraphicsView):
+    """
+    This class views all QGraphicsItems for the user. It takes the
+    QGraphicsScene as input and inherits all funcitonality from the
+    QGraphicsView and overrides the wheelEvent function.
+    This allows the users to navigate the view with their trackpads
+    and zoom in/out with their trackpads + ctrl.
+    """
+
+    def __init__(self, QGraphicsScene):
+        super().__init__(QGraphicsScene)
+        self.scene = QGraphicsScene
+
+    def wheelEvent(self, event: QWheelEvent | None) -> None:
+        """
+        Default behaviour if ctrl is not pressed, otherwise zoom in/out.
+        """
+        modifiers = QtWidgets.QApplication.keyboardModifiers()
+        if modifiers == QtCore.Qt.ControlModifier:
+            # Factor above 1 zooms in, below zooms out
+            factor = 1.03
+            if event.angleDelta().y() < 0:
+                factor = 0.97
+
+            # If event got triggered due to the x axis, do nothing
+            if event.pixelDelta().x() != 0:
+                return
+
+            view_pos = event.globalPosition()
+            scene_pos = self.mapToScene(int(view_pos.x()), int(view_pos.y()))
+
+            self.centerOn(scene_pos)
+            self.scale(factor, factor)
+
+            old_mapToScene = self.mapToScene(int(view_pos.x()), int(view_pos.y()))
+            new_mapToScene = self.mapToScene(self.viewport().rect().center())
+
+            delta = old_mapToScene - new_mapToScene
+
+            self.centerOn(scene_pos - delta)
+
+        else:
+            # Default behaviour
+            super().wheelEvent(event)
diff --git a/src/simudator/processor/mia/mia.py b/src/simudator/processor/mia/mia.py
index 536b2b0dfcc186ad58abe587479ca9b93913d622..b90e2f5efdc0b4514ea70a73595531ab1905b540 100644
--- a/src/simudator/processor/mia/mia.py
+++ b/src/simudator/processor/mia/mia.py
@@ -345,36 +345,36 @@ class MIA_CPU(Processor):
             (grx.signals["in_input"], grx.signals["out_content"]),
         ]
 
-        gui.addModuleGraphicsItem(
+        gui.add_module_graphics_item(
             BusGraphicsItem(self.get_module("Bus"), bus_signal_pairs)
         )
-        gui.addModuleGraphicsItem(uPcGraphicsItem(self.get_module("uPC")))
-        gui.addModuleGraphicsItem(SupcGraphicsItem(self.get_module("SuPC")))
-        gui.addModuleGraphicsItem(PcGraphicsItem(self.get_module("PC")))
-        gui.addModuleGraphicsItem(ArGraphicsItem(self.get_module("AR")))
-        gui.addModuleGraphicsItem(AsrGraphicsItem(self.get_module("ASR")))
-        gui.addModuleGraphicsItem(HrGraphicsItem(self.get_module("HR")))
-        gui.addModuleGraphicsItem(MicroMemoryGraphicsItem(self.get_module("uM")))
-        gui.addModuleGraphicsItem(AluGraphicsItem(self.get_module("ALU")))
-        gui.addModuleGraphicsItem(GrxGraphicsItem(self.get_module("GRx")))
-        gui.addModuleGraphicsItem(IrGraphicsItem(self.get_module("IR")))
+        gui.add_module_graphics_item(uPcGraphicsItem(self.get_module("uPC")))
+        gui.add_module_graphics_item(SupcGraphicsItem(self.get_module("SuPC")))
+        gui.add_module_graphics_item(PcGraphicsItem(self.get_module("PC")))
+        gui.add_module_graphics_item(ArGraphicsItem(self.get_module("AR")))
+        gui.add_module_graphics_item(AsrGraphicsItem(self.get_module("ASR")))
+        gui.add_module_graphics_item(HrGraphicsItem(self.get_module("HR")))
+        gui.add_module_graphics_item(MicroMemoryGraphicsItem(self.get_module("uM")))
+        gui.add_module_graphics_item(AluGraphicsItem(self.get_module("ALU")))
+        gui.add_module_graphics_item(GrxGraphicsItem(self.get_module("GRx")))
+        gui.add_module_graphics_item(IrGraphicsItem(self.get_module("IR")))
 
         flag_modules = ["Z-Flag", "N-Flag", "C-Flag", "O-Flag", "L-Flag", "LC"]
         for name in flag_modules:
             module = self.get_module(name)
             widget = FlagGraphicsItem(module)
-            gui.addModuleGraphicsItem(widget)
+            gui.add_module_graphics_item(widget)
 
         memory_modules = ["PM", "K1", "K2"]
         for name in memory_modules:
             module = self.get_module(name)
             widget = MiaMemoryGraphicsItem(module)
-            gui.addModuleGraphicsItem(widget)
+            gui.add_module_graphics_item(widget)
 
-        gui.addAllSignals()
+        gui.add_all_signals()
 
         gui.show()
-        gui.loadLayoutFromFile("mia_layout")
+        gui.load_layout("mia_layout")
         app.exec()
 
     def launch_cli(self):