diff --git a/README.md b/README.md
index 78ea9b4e9766e84513f1be889edda0e49b982498..1dd07961a2bddd9020c2bb064b50a4fd6b37e2d7 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,8 @@
-pyspread
-====================
+# pyspread
+
+[![pypi version](https://img.shields.io/pypi/v/pyspread.svg)](https://pypi.python.org/pypi/pyspread)
+[![CI pipeline](https://gitlab.com/pyspread/pyspread/badges/master/pipeline.svg)](https://gitlab.com/pyspread/pyspread/-/pipelines?page=1&scope=branches&ref=master)
+[![pyspread community board](https://badges.gitter.im/pyspread/community.svg)](https://gitter.im/pyspread/community)
 
 **pyspread** is a non-traditional spreadsheet that is
 based on and written in the programming language Python.
@@ -13,45 +16,37 @@ It is released under the [GPL v3. LICENSE](LICENSE)
 
 # Installation
 
-## On Debian bullseye
-
-On Debian bullseye, pyspread is available as a package.
-Note that the version number for the Python3 beta release is >=1.99.1.
-
-```bash
-su -
-apt install pyspread
-```
-
-## Other platforms with packaged releases
+The table below shows for which operating systems, pyspread is available in which version.
+If pyspread is unavailable or outdated for your operating system, you can install it using one of the three methods below.
 
 ![Packaged](https://repology.org/badge/vertical-allrepos/pyspread.svg?header&columns=4)
 
-## Other platforms
-
-### Prerequisites
+### With pip
 
-Get the prerequisites:
-- Python (>=3.6)
-- PyQt5 (>=5.10) (must include PyQtSvg)
-- numpy (>=1.1)
-- setuptools (>=40.0)
+```bash
+pip install pyspread
+```
 
-and if needed the suggested modules:
-- matplotlib (>=1.1.1)
-- pyenchant (>=1.1)
-- pip (>=18)
+### From git
 
-Should the package pkg_resources be missing in your setup (e.g. on Ubuntu),
-then you may need to reinstall pip for Python3.
+It is assumed that python3 and git are installed.
 
-### With pip
-
-The example installs the dependencies for the current user. Make sure that
-you are using the Python3 version of pip.
+**Get sources and enter dir**
+```bash
+git clone https://gitlab.com/pyspread/pyspread.git
+# or
+git clone git@gitlab.com:pyspread/pyspread.git
+# then
+cd pyspread
+```
 
+**Install dependencies and pyspread**
 ```bash
-pip3 install --user requirements.txt
+pip3 install -r requirements.txt
+# or if pip3 is not present
+pip install -r requirements.txt
+# next
+python3 setup.py install
 ```
 
 ## Getting the bleeding edge version from the code repository
diff --git a/changelog b/changelog
index 313427808588dd149228f7d09601e6ae8318731d..92f4992e15f1f65e4130de836c7345bfc9930107 100644
--- a/changelog
+++ b/changelog
@@ -4,6 +4,48 @@ Changelog for pyspread
 Note: This changelog does not comprise development of pyspread for Python2
       It starts with the first Alpha release 1.99.0.0
 
+
+2.1
+---
+
+This release adds several new features:
+ * SVG exports
+ * Sorting of cells added
+ * Money support added using a new optional dependency `py-moneyed`
+ * Money support in CSV import dialog
+ * Matplotlib cells now use tight layout
+ * Quick summation button for selected cells. The result appears in the cell
+   below the bottom right cell of the selection
+ * Selection mode user handling improved
+
+Dependencies:
+ * Mandatory: Python (≥ 3.6), numpy (>=1.1), PyQt5 (≥ 5.10, requires PyQt5.Svg), setuptools (>=40.0), markdown2 (>= 2.3)
+ * Recommended: matplotlib (>=1.1.1), pyenchant (>=1.1), pip (>=18), python-dateutil (>= 2.7.0), py-moneyed (>=2.0)
+ * For building the apidocs with Sphinx see apidocs/requirements.txt
+
+Bug fixes:
+ * Cell edge rendering fixed
+ * Fix for copying outside of grid
+ * X,Y,Z set to None for macro execution
+ * Replace all is now faster
+ * `pyspread/share/metainfo/io.gitlab.pyspread.pyspread.metainfo.xml` fixed
+ * Entry line and cells now handle <Escape> properly
+ * When opening a pys or pysu file, pyspread now changes into the file's
+   directory
+
+2.0.2
+-----
+
+This is a bugfix release for pyspread 2.0 for Python 3.10 compatibility.
+
+Dependencies:
+ * Mandatory: Python (≥ 3.6), numpy (>=1.1), PyQt5 (≥ 5.10, requires PyQt5.Svg), setuptools (>=40.0), markdown2 (>= 2.3)
+ * Recommended: matplotlib (>=1.1.1), pyenchant (>=1.1), pip (>=18), python-dateutil (>= 2.7.0)
+ * For building the apidocs with Sphinx see apidocs/requirements.txt
+
+Bug fixes:
+ * Various type issues with Python 3.10 fixed
+
 2.0.1
 -----
 
diff --git a/pyspread.desktop b/pyspread.desktop
index 61299b18fb8bb144dc7bee33cb75b38d31410ef4..435a688c1e6e760d4754bf6689ac4b20da479400 100755
--- a/pyspread.desktop
+++ b/pyspread.desktop
@@ -1,9 +1,9 @@
 #!/usr/bin/env xdg-open
 [Desktop Entry]
-Categories=Math;Spreadsheet;Office
-Comment[en_US]=Use Python expressions in grid cells and make spreadsheet specific language obsolete
-Comment=Use Python expressions in grid cells and make spreadsheet specific language obsolete
-Exec=/usr/bin/pyspread %f
+Categories=Office;Spreadsheet;
+Comment[en_US]=pyspread is a non-traditional spreadsheet application that is based on and written in the programming language Python
+Comment=pyspread is a non-traditional spreadsheet application that is based on and written in the programming language Python
+Exec=pyspread
 GenericName[en_US]=Python based Spreadsheet
 GenericName=Python based Spreadsheet
 Keywords=python;spreadsheet;matplotlib;math;
@@ -11,7 +11,6 @@ Icon=pyspread
 MimeType=application/x-pyspread-spreadsheet;application/x-pyspread-bz-spreadsheet
 Name[en_US]=pyspread
 Name=pyspread
-Path=
 StartupNotify=false
 Terminal=false
 Type=Application
diff --git a/pyspread/__init__.py b/pyspread/__init__.py
index 924bc4bd8b3727d1cf65b03449fa87dfbf048788..b8f10d8bfc5416b691a1c4966ddff674485ac4ae 100644
--- a/pyspread/__init__.py
+++ b/pyspread/__init__.py
@@ -3,4 +3,4 @@
 APP_NAME = "pyspread"
 
 # Current pyspread version
-VERSION = "2.0.1"
+VERSION = "2.1"
diff --git a/pyspread/actions.py b/pyspread/actions.py
index 4b27ac178115d840269de9323a065472543d706f..cbf4f233aabfda26b5ef12c066e15b8345af1782 100644
--- a/pyspread/actions.py
+++ b/pyspread/actions.py
@@ -61,7 +61,8 @@ class Action(QAction):
 
     def __init__(self, parent: QWidget, label: str, *callbacks: List[Callable],
                  icon: QIcon = None, shortcut: str = None,
-                 statustip: str = None, checkable: bool = False):
+                 statustip: str = None, checkable: bool = False,
+                 role: QAction.MenuRole = None):
         """
 
         :param parent: The parent object, normally :class:`pyspread.MainWindow`
@@ -71,6 +72,7 @@ class Action(QAction):
         :param shortcut: The magic kestrokes if ant
         :param statustip: The popup message
         :param checkable: Has a checkbox
+        :param role: Menu role for action for macOS
 
         """
         if icon is None:
@@ -84,6 +86,9 @@ class Action(QAction):
         if statustip is not None:
             self.setStatusTip(statustip)
 
+        if role is not None:
+            self.setMenuRole(role)
+
         for connect in callbacks:
             self.triggered.connect(connect)
 
@@ -131,12 +136,12 @@ class MainWindowActions(AttrDict):
                            shortcut='Ctrl+s' if self.shortcuts else "",
                            statustip='Save spreadsheet')
 
-        self.save_as = Action(self.parent, "Save &As",
-                              self.parent.workflows.file_save_as,
-                              icon=Icon.save_as,
-                              shortcut='Shift+Ctrl+s' if self.shortcuts \
-                                  else "",
-                              statustip='Save spreadsheet to a new file')
+        self.save_as = Action(
+            self.parent, "Save &As",
+            self.parent.workflows.file_save_as,
+            icon=Icon.save_as,
+            shortcut='Shift+Ctrl+s' if self.shortcuts else "",
+            statustip='Save spreadsheet to a new file')
 
         self.imprt = Action(self.parent, "&Import",
                             self.parent.workflows.file_import,
@@ -174,12 +179,13 @@ class MainWindowActions(AttrDict):
         self.preferences = Action(self.parent, "Preferences...",
                                   self.parent.on_preferences,
                                   icon=Icon.preferences,
-                                  statustip='Pyspread setup parameters')
+                                  statustip='Pyspread setup parameters',
+                                  role=QAction.PreferencesRole)
 
         self.quit = Action(self.parent, "&Quit", self.parent.closeEvent,
                            icon=Icon.quit,
                            shortcut='Ctrl+Q' if self.shortcuts else "",
-                           statustip='Exit pyspread')
+                           statustip='Exit pyspread', role=QAction.QuitRole)
 
     def create_edit_actions(self):
         """actions for Edit menu"""
@@ -209,13 +215,13 @@ class MainWindowActions(AttrDict):
                            statustip='Copy the input strings of the cells '
                                      'to the clipboard')
 
-        self.copy_results = Action(self.parent, "Copy results",
-                                   self.parent.workflows.edit_copy_results,
-                                   icon=Icon.copy_results,
-                                   shortcut='Shift+Ctrl+c' if self.shortcuts \
-                                       else "",
-                                   statustip='Copy the result strings of '
-                                             'the cells to the clipboard')
+        self.copy_results = \
+            Action(self.parent, "Copy results",
+                   self.parent.workflows.edit_copy_results,
+                   icon=Icon.copy_results,
+                   shortcut='Shift+Ctrl+c' if self.shortcuts else "",
+                   statustip='Copy the result strings of the cells to the '
+                             'clipboard')
 
         self.paste = Action(self.parent, "&Paste",
                             self.parent.workflows.edit_paste,
@@ -223,13 +229,12 @@ class MainWindowActions(AttrDict):
                             shortcut='Ctrl+v' if self.shortcuts else "",
                             statustip='Paste cells from the clipboard')
 
-        self.paste_as = Action(self.parent, "Paste as...",
-                               self.parent.workflows.edit_paste_as,
-                               icon=Icon.paste_as,
-                               shortcut='Shift+Ctrl+v' if self.shortcuts \
-                                   else "",
-                               statustip='Transform clipboard and paste '
-                                         'results')
+        self.paste_as = Action(
+            self.parent, "Paste as...",
+            self.parent.workflows.edit_paste_as,
+            icon=Icon.paste_as,
+            shortcut='Shift+Ctrl+v' if self.shortcuts else "",
+            statustip='Transform clipboard and paste results')
 
         self.find = Action(self.parent, "&Find...",
                            self.parent.workflows.edit_find,
@@ -243,17 +248,32 @@ class MainWindowActions(AttrDict):
                                 shortcut='F3' if self.shortcuts else "",
                                 statustip='Find next matching cell')
 
-        self.replace = Action(self.parent, "&Replace...",
-                              self.parent.workflows.edit_replace,
-                              icon=Icon.replace,
-                              shortcut='Shift+Ctrl+f' if self.shortcuts \
-                                  else "",
-                              statustip='Replace sub-strings in cells')
+        self.replace = Action(
+            self.parent, "&Replace...",
+            self.parent.workflows.edit_replace,
+            icon=Icon.replace,
+            shortcut='Shift+Ctrl+f' if self.shortcuts else "",
+            statustip='Replace sub-strings in cells')
+
+        self.sort_ascending = Action(
+            self.parent, "Sort ascending",
+            self.parent.workflows.edit_sort_ascending,
+            icon=Icon.sort_ascending,
+            statustip='Sort selected cells. The sort order is ascending and '
+                      'follows the current column.')
+
+        self.sort_descending = Action(
+            self.parent, "Sort descending",
+            self.parent.workflows.edit_sort_descending,
+            icon=Icon.sort_descending,
+            statustip='Sort selected cells. The sort order is descending and '
+                      'follows the current column.')
 
         self.toggle_selection_mode = Action(
             self.parent, "Selection mode",
             self.parent.grid.set_selection_mode,
             icon=Icon.selection_mode, checkable=True,
+            shortcut='Ins',
             statustip='Enter/leave selection mode')
 
         self.quote = Action(self.parent, "&Quote",
@@ -381,7 +401,7 @@ class MainWindowActions(AttrDict):
             Action(self.parent, "Refresh selected cells",
                    self.parent.grid.refresh_selected_frozen_cells,
                    icon=Icon.refresh,
-                   shortcut=QKeySequence.Refresh  if self.shortcuts else "",
+                   shortcut=QKeySequence.Refresh if self.shortcuts else "",
                    statustip='Refresh selected cells even when frozen')
 
         self.toggle_periodic_updates = \
@@ -745,6 +765,13 @@ class MainWindowActions(AttrDict):
                                    statustip='Create and display matplotlib '
                                              'chart')
 
+        self.insert_sum = Action(self.parent, "Insert sum",
+                                 self.parent.workflows.macro_insert_sum,
+                                 icon=Icon.insert_sum,
+                                 statustip='Insert sum of selection into the'
+                                           'cell below the bottom right cell '
+                                           'of the selection')
+
     def create_help_actions(self):
         """actions for Help menu"""
 
@@ -767,7 +794,8 @@ class MainWindowActions(AttrDict):
         self.about = Action(self.parent, "About pyspread...",
                             self.parent.on_about,
                             icon=Icon.pyspread,
-                            statustip='About pyspread')
+                            statustip='About pyspread',
+                            role=QAction.AboutRole)
 
     def disable_unavailable(self):
         """Disables unavailable menu items e.g. due to missing dependencies"""
diff --git a/pyspread/cli.py b/pyspread/cli.py
index 977a839635c94ad9399eeb1ae4310d7ac1c3bd0c..eed2b4fe4f55ae71e5590156f060d08831ef56fa 100644
--- a/pyspread/cli.py
+++ b/pyspread/cli.py
@@ -28,7 +28,7 @@
 
 """
 
-from argparse import Action, ArgumentParser
+from argparse import ArgumentParser
 from pathlib import Path
 import sys
 
@@ -55,26 +55,23 @@ def check_mandatory_dependencies():
 
         """
 
-        sys.stdout.write('Warning: {}\n'.format(message))
+        sys.stdout.write(f'Warning: {message}\n')
 
     # Check Python version
     major = sys.version_info.major
     minor = sys.version_info.minor
     micro = sys.version_info.micro
     if major < 3 or major == 3 and minor < 6:
-        msg_tpl = "Python has version {}.{}.{} but ≥ 3.6 is required."
-        msg = msg_tpl.format(major, minor, micro)
+        msg = f"Python has version {major}.{minor}.{micro}" + \
+               " but ≥ 3.6 is required."
         dependency_warning(msg)
 
     for module in REQUIRED_DEPENDENCIES:
         if module.is_installed() is None or not module.is_installed():
-            msg_tpl = "Required module {} not found."
-            msg = msg_tpl.format(module.name)
-            dependency_warning(msg)
+            dependency_warning(f"Required module {module.name} not found.")
         elif module.version < module.required_version:
-            msg_tpl = "Module {} has version {} but {} is required."
-            msg = msg_tpl.format(module.name, module.version,
-                                 module.required_version)
+            msg = f"Module {module.name} has version {module.version}" + \
+                  f"but {module.required_version} is required."
             dependency_warning(msg)
     if pyqtsvg is None:
         # Import of mandatory module failed
@@ -82,18 +79,6 @@ def check_mandatory_dependencies():
         dependency_warning(msg)
 
 
-class PathAction(Action):
-    """Action that handles paths with spaces and provides a pathlib Path"""
-
-    def __call__(self, parser, namespace, values, option_string=None):
-        """Overrides __call__ to enable spaces in path names"""
-
-        if values:
-            setattr(namespace, self.dest, Path(" ".join(values)))
-        else:
-            setattr(namespace, self.dest, None)
-
-
 class PyspreadArgumentParser(ArgumentParser):
     """Parser for the command line"""
 
@@ -104,18 +89,13 @@ class PyspreadArgumentParser(ArgumentParser):
                       "based on and written in the programming language " \
                       "Python."
 
-        # Override usage because of the PathAction fix for paths with spaces
-        usage_tpl = "{} [-h] [--version] [--default-settings] [file]"
-        usage = usage_tpl.format(APP_NAME)
-
-        super().__init__(prog=APP_NAME, description=description, usage=usage)
+        super().__init__(prog=APP_NAME, description=description)
 
         self.add_argument('--version', action='version', version=VERSION)
 
-        default_settings_help = 'start with default settings and save on exit'
-
         self.add_argument('--default-settings', action='store_true',
-                          help=default_settings_help)
+                          help='start with default settings and save them on '
+                               'exit')
 
-        file_help = 'open pyspread file in pys or pysu format'
-        self.add_argument('file', action=PathAction, nargs="*", help=file_help)
+        self.add_argument('file', type=Path, nargs='?', default=None,
+                          help='open pyspread file in pys or pysu format')
diff --git a/pyspread/commands.py b/pyspread/commands.py
index 8f30f42da0d4401dbbe2a5d737b24d552d2d8ab1..0bd0dcdc3743e97d8231ed270f3a434ca882f55c 100644
--- a/pyspread/commands.py
+++ b/pyspread/commands.py
@@ -36,6 +36,7 @@ Pyspread undoable commands
 """
 
 from copy import copy
+from math import isclose
 from typing import List, Iterable, Tuple
 
 from PyQt5.QtCore import Qt, QModelIndex, QAbstractTableModel
@@ -198,7 +199,8 @@ class SetRowsHeight(QUndoCommand):
         self.old_height = old_height
         self.new_height = new_height
 
-        self.default_size = self.grid.verticalHeader().defaultSectionSize()
+        self.default_size = \
+            self.grid.verticalHeader().defaultSectionSize() / self.grid.zoom
 
     def id(self) -> int:
         """Command id that enables command merging"""
@@ -220,32 +222,42 @@ class SetRowsHeight(QUndoCommand):
     def redo(self):
         """Redo row height setting"""
 
+        # Update all frontend grids
         for grid in self.grid.main_window.grids:
+            with grid.undo_resizing_row():
+                for row in self.rows:
+                    grid.setRowHeight(row, int(self.new_height * grid.zoom))
+
+        code_array = self.grid.model.code_array
+        with self.grid.undo_resizing_row():
             for row in self.rows:
-                if self.new_height != self.default_size:
-                    grid.model.code_array.row_heights[(row, self.table)] = \
-                        self.new_height / grid.zoom
-                if grid.rowHeight(row) != self.new_height:
-                    with grid.undo_resizing_row():
-                        grid.setRowHeight(row, self.new_height)
+                if isclose(self.new_height, self.default_size):
+                    try:
+                        code_array.row_heights.pop((row, self.table))
+                    except KeyError:
+                        pass
+                else:
+                    code_array.row_heights[(row, self.table)] = self.new_height
 
     def undo(self):
         """Undo row height setting"""
 
+        # Update all frontend grids
         for grid in self.grid.main_window.grids:
+            with grid.undo_resizing_row():
+                for row in self.rows:
+                    grid.setRowHeight(row, int(self.old_height * grid.zoom))
+
+        code_array = self.grid.model.code_array
+        with self.grid.undo_resizing_row():
             for row in self.rows:
-                if self.old_height == self.default_size:
+                if isclose(self.old_height, self.default_size):
                     try:
-                        grid.model.code_array.row_heights.pop((row,
-                                                               self.table))
+                        code_array.row_heights.pop((row, self.table))
                     except KeyError:
                         pass
                 else:
-                    grid.model.code_array.row_heights[(row, self.table)] = \
-                        self.old_height / grid.zoom
-                if grid.rowHeight(row) != self.old_height:
-                    with grid.undo_resizing_row():
-                        grid.setRowHeight(row, self.old_height)
+                    code_array.row_heights[(row, self.table)] = self.old_height
 
 
 class SetColumnsWidth(QUndoCommand):
@@ -271,7 +283,8 @@ class SetColumnsWidth(QUndoCommand):
         self.old_width = old_width
         self.new_width = new_width
 
-        self.default_size = self.grid.horizontalHeader().defaultSectionSize()
+        self.default_size = \
+            self.grid.horizontalHeader().defaultSectionSize() / self.grid.zoom
 
     def id(self) -> int:
         """Command id that enables command merging"""
@@ -293,32 +306,45 @@ class SetColumnsWidth(QUndoCommand):
     def redo(self):
         """Redo column width setting"""
 
+        # Update all frontend grids
         for grid in self.grid.main_window.grids:
+            with grid.undo_resizing_column():
+                for column in self.columns:
+                    grid.setColumnWidth(column,
+                                        int(self.new_width * grid.zoom))
+
+        code_array = self.grid.model.code_array
+        with self.grid.undo_resizing_column():
             for column in self.columns:
-                if self.new_width != self.default_size:
-                    grid.model.code_array.col_widths[(column, self.table)] = \
-                        self.new_width / grid.zoom
-                if grid.columnWidth(column) != self.new_width:
-                    with grid.undo_resizing_column():
-                        grid.setColumnWidth(column, self.new_width)
+                if isclose(self.new_width, self.default_size):
+                    try:
+                        code_array.col_widths.pop((column, self.table))
+                    except KeyError:
+                        pass
+                else:
+                    code_array.col_widths[(column, self.table)] = \
+                        self.new_width
 
     def undo(self):
         """Undo column width setting"""
 
+        # Update all frontend grids
         for grid in self.grid.main_window.grids:
+            with grid.undo_resizing_column():
+                for column in self.columns:
+                    grid.setColumnWidth(column, int(self.old_width*grid.zoom))
+
+        code_array = self.grid.model.code_array
+        with self.grid.undo_resizing_column():
             for column in self.columns:
-                if self.old_width == self.default_size:
+                if isclose(self.old_width, self.default_size):
                     try:
-                        grid.model.code_array.col_widths.pop((column,
-                                                              self.table))
+                        code_array.col_widths.pop((column, self.table))
                     except KeyError:
                         pass
                 else:
-                    grid.model.code_array.col_widths[(column, self.table)] = \
-                        self.old_width / grid.zoom
-                if grid.columnWidth(column) != self.old_width:
-                    with grid.undo_resizing_column():
-                        grid.setColumnWidth(column, self.old_width)
+                    code_array.col_widths[(column, self.table)] = \
+                        self.old_width
 
 
 class InsertRows(QUndoCommand):
diff --git a/pyspread/dialogs.py b/pyspread/dialogs.py
index f9821b814dbcbedba588c8dc5499794ae0f829ce..06d2f8b7eb09e1f0c9ca3d4f8f225970d13b7b35 100644
--- a/pyspread/dialogs.py
+++ b/pyspread/dialogs.py
@@ -32,6 +32,7 @@
  * :class:`SinglePageArea`
  * :class:`MultiPageArea`
  * :class:`CsvExportAreaDialog`
+ * :class:`SvgExportAreaDialog`
  * :class:`PrintAreaDialog`
  * (:class:`FileDialogBase`)
  * :class:`FileOpenDialog`
@@ -65,7 +66,7 @@ from PyQt5.QtWidgets \
             QFormLayout, QVBoxLayout, QGroupBox, QDialogButtonBox, QSplitter,
             QTextBrowser, QCheckBox, QGridLayout, QLayout, QHBoxLayout,
             QPushButton, QWidget, QComboBox, QTableView, QAbstractItemView,
-            QPlainTextEdit, QToolBar, QMainWindow, QTabWidget)
+            QPlainTextEdit, QToolBar, QMainWindow, QTabWidget, QInputDialog)
 from PyQt5.QtGui \
     import (QIntValidator, QImageWriter, QStandardItemModel, QStandardItem,
             QValidator, QWheelEvent)
@@ -82,16 +83,15 @@ except ImportError:
 try:
     from pyspread.actions import ChartDialogActions
     from pyspread.toolbar import ChartTemplatesToolBar
-    from pyspread.widgets import HelpBrowser
-    from pyspread.lib.csv import (sniff, csv_reader, get_header, typehandlers,
-                                  convert)
+    from pyspread.widgets import HelpBrowser, TypeMenuComboBox
+    from pyspread.lib.csv import sniff, csv_reader, get_header, convert
     from pyspread.lib.spelltextedit import SpellTextEdit
     from pyspread.settings import TUTORIAL_PATH, MANUAL_PATH, MPL_TEMPLATE_PATH
 except ImportError:
     from actions import ChartDialogActions
     from toolbar import ChartTemplatesToolBar
-    from widgets import HelpBrowser
-    from lib.csv import sniff, csv_reader, get_header, typehandlers, convert
+    from widgets import HelpBrowser, TypeMenuComboBox
+    from lib.csv import sniff, csv_reader, get_header, convert
     from lib.spelltextedit import SpellTextEdit
     from settings import TUTORIAL_PATH, MANUAL_PATH, MPL_TEMPLATE_PATH
 
@@ -148,7 +148,8 @@ class DiscardDataDialog(DiscardChangesDialog):
         :param text: Message text
 
         """
-        self.main_window = main_window
+
+        super().__init__(main_window)
         self.text = text
 
 
@@ -440,6 +441,17 @@ class CsvExportAreaDialog(DataEntryDialog):
                 return
 
 
+class SvgExportAreaDialog(CsvExportAreaDialog):
+    """Modal dialog for entering svg export area
+
+    Initially, this dialog is filled with the selection bounding box
+    if present or with the visible area of <= 1 cell is selected.
+
+    """
+
+    groupbox_title = "SVG export area"
+
+
 class PrintAreaDialog(CsvExportAreaDialog):
     """Modal dialog for entering print area
 
@@ -650,10 +662,9 @@ class ImageFileOpenDialog(FileDialogBase):
     title = "Insert image"
 
     img_formats = QImageWriter.supportedImageFormats()
-    img_format_strings = ("*." + fmt.data().decode('utf-8')
-                          for fmt in img_formats)
+    img_format_strings = (f"*.{fmt.data().decode()}" for fmt in img_formats)
     img_format_string = " ".join(img_format_strings)
-    name_filter = "Images ({})".format(img_format_string) + ";;" \
+    name_filter = f"Images ({img_format_string})" + ";;" \
                   "Scalable Vector Graphics (*.svg *.svgz)"
 
     def show_dialog(self):
@@ -710,7 +721,7 @@ class FileExportDialog(FileDialogBase):
     def suffix(self) -> str:
         """Suffix for filepath"""
 
-        return ".{}".format(self.selected_filter.split()[0].lower())
+        return f".{self.selected_filter.split()[0].lower()}"
 
     def show_dialog(self):
         """Present dialog and update values"""
@@ -769,7 +780,11 @@ class FindDialog(QDialog):
             self.restore(state)
 
     def _create_widgets(self):
-        """Create find dialog widgets"""
+        """Create find dialog widgets
+
+        :param results_checkbox: Show find results checkbox
+
+        """
 
         self.search_text_label = QLabel("Search for:")
         self.search_text_editor = QLineEdit()
@@ -885,6 +900,8 @@ class ReplaceDialog(FindDialog):
 
         self.setWindowTitle("Replace")
 
+        self.results_checkbox.setDisabled(True)
+
         self.replace_text_label = QLabel("Replace with:")
         self.replace_text_editor = QLineEdit()
         self.replace_text_label.setBuddy(self.replace_text_editor)
@@ -933,7 +950,7 @@ class ChartDialog(QDialog):
 
         self.chart_templates_toolbar = ChartTemplatesToolBar(self)
 
-        self.setWindowTitle("Chart dialog for cell {}".format(key))
+        self.setWindowTitle(f"Chart dialog for cell {key}")
 
         self.resize(*size)
         self.parent = parent
@@ -1008,7 +1025,7 @@ class ChartDialog(QDialog):
                 pass
         else:
             if isinstance(figure, Exception):
-                msg = stdout_str + "Error:\n{}".format(figure)
+                msg = stdout_str + f"Error:\n{figure}"
                 self.message.setText(msg)
             else:
                 msg = stdout_str
@@ -1038,25 +1055,6 @@ class CsvParameterGroupBox(QGroupBox):
 
     title = "Parameters"
 
-    encodings = (
-        "ascii", "big5", "big5hkscs", "cp037", "cp424", "cp437",
-        "cp500", "cp720", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
-        "cp857", "cp858", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865",
-        "cp866", "cp869", "cp874", "cp875", "cp932", "cp949", "cp950",
-        "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252", "cp1253",
-        "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "euc-jp",
-        "euc-jis-2004", "euc-jisx0213", "euc-kr", "gb2312", "gbk", "gb18030",
-        "hz", "iso2022-jp", "iso2022-jp-1", "iso2022-jp-2", "iso2022-jp-2004",
-        "iso2022-jp-3", "iso2022-jp-ext", "iso2022-kr", "latin-1", "iso8859-2",
-        "iso8859-3", "iso8859-4", "iso8859-5", "iso8859-6", "iso8859-7",
-        "iso8859-8", "iso8859-9", "iso8859-10", "iso8859-13", "iso8859-14",
-        "iso8859-15", "iso8859-16", "johab", "koi8-r", "koi8-u",
-        "mac-cyrillic", "mac-greek", "mac-iceland", "mac-latin2", "mac-roman",
-        "mac-turkish", "ptcp154", "shift-jis", "shift-jis-2004",
-        "shift-jisx0213", "utf-32", "utf-32-be", "utf-32-le", "utf-16",
-        "utf-16-be", "utf-16-le", "utf-7", "utf-8", "utf-8-sig",
-    )
-
     quotings = "QUOTE_ALL", "QUOTE_MINIMAL", "QUOTE_NONNUMERIC", "QUOTE_NONE"
 
     # Tooltips
@@ -1086,7 +1084,6 @@ class CsvParameterGroupBox(QGroupBox):
         "the delimiter is ignored."
 
     # Default values that are displayed if the sniffer fails
-    default_encoding = "utf-8"
     default_quoting = "QUOTE_MINIMAL"
     default_quotechar = '"'
     default_delimiter = ','
@@ -1099,6 +1096,8 @@ class CsvParameterGroupBox(QGroupBox):
 
         super().__init__(parent)
         self.parent = parent
+        self.default_encoding = parent.csv_encoding
+        self.encodings = parent.parent.settings.encodings
 
         self.setTitle(self.title)
         self._create_widgets()
@@ -1226,8 +1225,7 @@ class CsvParameterGroupBox(QGroupBox):
 
         """
 
-        for parameter in self.csv_parameter2widget:
-            widget = self.csv_parameter2widget[parameter]
+        for parameter, widget in self.csv_parameter2widget.items():
             if hasattr(widget, "currentText"):
                 value = widget.currentText()
             elif hasattr(widget, "isChecked"):
@@ -1235,7 +1233,7 @@ class CsvParameterGroupBox(QGroupBox):
             elif hasattr(widget, "text"):
                 value = widget.text()
             else:
-                raise AttributeError("{} unsupported".format(widget))
+                raise AttributeError(f"{widget} unsupported")
 
             # Convert strings to csv constants
             if parameter == "quoting" and isinstance(value, str):
@@ -1273,7 +1271,7 @@ class CsvParameterGroupBox(QGroupBox):
                 elif hasattr(widget, "setText"):
                     widget.setText(value)
                 else:
-                    raise AttributeError("{} unsupported".format(widget))
+                    raise AttributeError(f"{widget} unsupported")
         if not self.hasheader_widget.isChecked():
             self.keepheader_widget.setEnabled(False)
 
@@ -1307,17 +1305,8 @@ class CsvTable(QTableView):
 
         """
 
-        class TypeCombo(QComboBox):
-            """ComboBox for type choice"""
-
-            def __init__(self):
-                super().__init__()
-
-                for typehandler in typehandlers:
-                    self.addItem(typehandler)
-
         item_row = map(QStandardItem, [''] * length)
-        self.comboboxes = [TypeCombo() for _ in range(length)]
+        self.comboboxes = [TypeMenuComboBox() for _ in range(length)]
         self.model.appendRow(item_row)
         for i, combobox in enumerate(self.comboboxes):
             self.setIndexWidget(self.model.index(0, i), combobox)
@@ -1337,26 +1326,36 @@ class CsvTable(QTableView):
         self.verticalHeader().hide()
 
         try:
-            with open(filepath, newline='', encoding='utf-8') as csvfile:
-                if hasattr(dialect, 'hasheader') and dialect.hasheader:
-                    header = get_header(csvfile, dialect)
-                    self.model.setHorizontalHeaderLabels(header)
-                    self.horizontalHeader().show()
-                else:
-                    self.horizontalHeader().hide()
-
-                for i, row in enumerate(csv_reader(csvfile, dialect)):
-                    if i >= self.no_rows:
-                        break
-                    if i == 0:
-                        self.add_choice_row(len(row))
-                    if digest_types is None:
-                        item_row = map(QStandardItem, map(str, row))
+            if hasattr(dialect, "encoding"):
+                encoding = dialect.encoding
+            else:
+                encoding = self.parent.csv_encoding
+            try:
+                with open(filepath, newline='', encoding=encoding) as csvfile:
+                    if hasattr(dialect, 'hasheader') and dialect.hasheader:
+                        header = get_header(csvfile, dialect)
+                        self.model.setHorizontalHeaderLabels(header)
+                        self.horizontalHeader().show()
                     else:
-                        codes = (convert(ele, t)
-                                 for ele, t in zip(row, digest_types))
-                        item_row = map(QStandardItem, codes)
-                    self.model.appendRow(item_row)
+                        self.horizontalHeader().hide()
+
+                    for i, row in enumerate(csv_reader(csvfile, dialect)):
+                        if i >= self.no_rows:
+                            break
+                        if i == 0:
+                            self.add_choice_row(len(row))
+                        if digest_types is None:
+                            item_row = map(QStandardItem, map(str, row))
+                        else:
+                            codes = (convert(ele, t)
+                                     for ele, t in zip(row, digest_types))
+                            item_row = map(QStandardItem, codes)
+                        self.model.appendRow(item_row)
+            except UnicodeDecodeError:
+                QMessageBox.warning(self.parent, "Encoding error",
+                                    f"File is not encoded in {encoding}.")
+                self.model.clear()
+
         except (OSError, csv.Error) as error:
             title = "CSV Import Error"
             text_tpl = "Error importing csv file {path}.\n \n" +\
@@ -1368,7 +1367,10 @@ class CsvTable(QTableView):
     def get_digest_types(self) -> List[str]:
         """Returns list of digest types from comboboxes"""
 
-        return [cbox.currentText() for cbox in self.comboboxes]
+        try:
+            return [cbox.currentText() for cbox in self.comboboxes]
+        except RuntimeError:
+            return []
 
     def update_comboboxes(self, digest_types: List[str]):
         """Updates the cono boxes to show digest_types
@@ -1403,6 +1405,7 @@ class CsvImportDialog(QDialog):
 
         self.sniff_size = parent.settings.sniff_size
 
+        self.csv_encoding = 'utf-8'
         self.dialect = None
 
         self.setWindowTitle(self.title)
@@ -1434,20 +1437,33 @@ class CsvImportDialog(QDialog):
 
         return button_box
 
-    # Button event handlers
-
-    def reset(self):
-        """Button event handler, resets parameter_groupbox and csv_table"""
+    def _sniff_dialect(self):
+        """"""
 
         try:
-            dialect = sniff(self.filepath, self.sniff_size)
+            return sniff(self.filepath, self.sniff_size, self.csv_encoding)
+        except UnicodeError:
+            self.csv_encoding, ok = QInputDialog().getItem(
+                self, f"{self.filepath} not encoded in utf-8",
+                f"Encoding of {self.filepath}",
+                self.parent.settings.encodings)
+            if ok:
+                return self._sniff_dialect()
         except (OSError, csv.Error) as error:
             title = "CSV Import Error"
-            text_tpl = "Error sniffing csv file {path}.\n \n{errtype}: {error}"
-            text = text_tpl.format(path=self.filepath,
-                                   errtype=type(error).__name__, error=error)
+            text = f"Error sniffing csv file {self.filepath}.\n \n" + \
+                   f"{type(error).__name__}: {error}"
             QMessageBox.warning(self.parent, title, text)
+
+    # Button event handlers
+
+    def reset(self):
+        """Button event handler, resets parameter_groupbox and csv_table"""
+
+        dialect = self._sniff_dialect()
+        if dialect is None:
             return
+
         self.parameter_groupbox.set_csvdialect(dialect)
         self.csv_table.fill(self.filepath, dialect)
         if self.digest_types is not None:
@@ -1456,15 +1472,10 @@ class CsvImportDialog(QDialog):
     def apply(self):
         """Button event handler, applies parameters to csv_table"""
 
-        try:
-            sniff_dialect = sniff(self.filepath, self.sniff_size)
-        except (OSError, csv.Error) as error:
-            title = "CSV Import Error"
-            text_tpl = "Error sniffing csv file {path}.\n \n{errtype}: {error}"
-            text = text_tpl.format(path=self.filepath,
-                                   errtype=type(error).__name__, error=error)
-            QMessageBox.warning(self.parent, title, text)
+        sniff_dialect = self._sniff_dialect()
+        if sniff_dialect is None:
             return
+
         try:
             dialect = self.parameter_groupbox.adjust_csvdialect(sniff_dialect)
         except AttributeError as error:
@@ -1483,15 +1494,10 @@ class CsvImportDialog(QDialog):
     def accept(self):
         """Button event handler, starts csv import"""
 
-        try:
-            sniff_dialect = sniff(self.filepath, self.sniff_size)
-        except csv.Error as error:
-            title = "CSV Import Error"
-            text_tpl = "Error sniffing csv file {path}.\n \n{errtype}: {error}"
-            text = text_tpl.format(path=self.filepath,
-                                   errtype=type(error).__name__, error=error)
-            QMessageBox.warning(self.parent, title, text)
+        sniff_dialect = self._sniff_dialect()
+        if sniff_dialect is None:
             return
+
         try:
             dialect = self.parameter_groupbox.adjust_csvdialect(sniff_dialect)
         except AttributeError as error:
@@ -1669,8 +1675,7 @@ class ManualDialog(TutorialDialog):
         """Creates tabbar and dialog browser"""
 
         self.tabbar = QTabWidget(self)
-        for title in self.title2path:
-            path = self.title2path[title]
+        for title, path in self.title2path.items():
             self.tabbar.addTab(HelpBrowser(self, path), title)
 
     def _layout(self):
@@ -1685,7 +1690,6 @@ class ManualDialog(TutorialDialog):
         layout.addWidget(self.tabbar)
 
 
-
 class PrintPreviewDialog(QPrintPreviewDialog):
     """Adds Mouse wheel functionality"""
 
diff --git a/pyspread/entryline.py b/pyspread/entryline.py
index 69fd74c3c92d53f50f248af5ff4beab5fe13eb2e..e52e489a435daf6c33b3a6871585572ecece3164 100644
--- a/pyspread/entryline.py
+++ b/pyspread/entryline.py
@@ -126,13 +126,19 @@ class Entryline(SpellTextEdit):
             if event.modifiers() == Qt.ShiftModifier:
                 self.insertPlainText('\n')
             else:
-                self.store_data()
-                grid.row += 1
+                if grid.selection_mode:
+                    grid.set_selection_mode(False)
+                else:
+                    self.store_data()
+                    grid.row += 1
         elif self.last_key == Qt.Key_Tab:
             self.store_data()
             grid.column += 1
+        elif self.last_key == Qt.Key_Escape:
+            grid.on_current_changed()
+            grid.setFocus()
         elif self.last_key == Qt.Key_Insert:
-            grid.selection_mode = not grid.selection_mode
+            grid.set_selection_mode(not grid.selection_mode)
         else:
             super().keyPressEvent(event)
 
diff --git a/pyspread/grid.py b/pyspread/grid.py
index 42d980c646d43de91983477e285dbed20cc13626..dc5421e3df5f89754cb85a55a8941a44830a92db 100644
--- a/pyspread/grid.py
+++ b/pyspread/grid.py
@@ -50,7 +50,10 @@ from PyQt5.QtGui \
             QWheelEvent, QContextMenuEvent, QTextCursor)
 from PyQt5.QtCore \
     import (Qt, QAbstractTableModel, QModelIndex, QVariant, QEvent, QSize,
-            QRect, QRectF, QItemSelectionModel, QObject, QAbstractItemModel)
+            QRect, QRectF, QItemSelectionModel, QObject, QAbstractItemModel,
+            QByteArray)
+
+from PyQt5.QtSvg import QSvgRenderer
 
 try:
     import matplotlib
@@ -59,20 +62,19 @@ except ImportError:
     matplotlib = None
 
 try:
-    import pyspread.commands as commands
+    from pyspread import commands
     from pyspread.dialogs import DiscardDataDialog
     from pyspread.grid_renderer import painter_save, CellRenderer, QColorCache
     from pyspread.model.model import (CodeArray, CellAttribute,
                                       DefaultCellAttributeDict)
     from pyspread.lib.attrdict import AttrDict
     from pyspread.lib.selection import Selection
-    from pyspread.lib.string_helpers import quote, wrap_text, get_svg_size
+    from pyspread.lib.string_helpers import quote, wrap_text
     from pyspread.lib.qimage2ndarray import array2qimage
-    from pyspread.lib.qimage_svg import QImageSvg
     from pyspread.lib.typechecks import is_svg, check_shape_validity
-    from pyspread.menus \
-        import (GridContextMenu, TableChoiceContextMenu,
-                HorizontalHeaderContextMenu, VerticalHeaderContextMenu)
+    from pyspread.menus import (GridContextMenu, TableChoiceContextMenu,
+                                HorizontalHeaderContextMenu,
+                                VerticalHeaderContextMenu)
     from pyspread.widgets import CellButton
 except ImportError:
     import commands
@@ -81,13 +83,11 @@ except ImportError:
     from model.model import CodeArray, CellAttribute, DefaultCellAttributeDict
     from lib.attrdict import AttrDict
     from lib.selection import Selection
-    from lib.string_helpers import quote, wrap_text, get_svg_size
+    from lib.string_helpers import quote, wrap_text
     from lib.qimage2ndarray import array2qimage
-    from lib.qimage_svg import QImageSvg
     from lib.typechecks import is_svg, check_shape_validity
-    from menus \
-        import (GridContextMenu, TableChoiceContextMenu,
-                HorizontalHeaderContextMenu, VerticalHeaderContextMenu)
+    from menus import (GridContextMenu, TableChoiceContextMenu,
+                       HorizontalHeaderContextMenu, VerticalHeaderContextMenu)
     from widgets import CellButton
 
 
@@ -362,7 +362,7 @@ class Grid(QTableView):
 
     @selection_mode.setter
     def selection_mode(self, on: bool):
-        """Sets or unsets selection mode
+        """Sets or unsets selection mode for this grid
 
         In selection mode, cells cannot be edited.
         This triggers the selection_mode icon in the statusbar.
@@ -387,9 +387,10 @@ class Grid(QTableView):
                                  | QAbstractItemView.AnyKeyPressed)
             self.selection_mode_exiting = False
             self.main_window.selection_mode_widget.hide()
+            self.main_window.entry_line.setFocus()
 
     def set_selection_mode(self, value=True):
-        """Setter for selection mode
+        """Setter for selection mode for all grids
 
         This method is required for accessing selection mode from QActions.
 
@@ -401,6 +402,11 @@ class Grid(QTableView):
         for grid in self.main_window.grids:
             grid.selection_mode = value
 
+        # Adjust the menu
+        toggle_selection_mode = \
+            self.main_window.main_window_actions.toggle_selection_mode
+        toggle_selection_mode.setChecked(value)
+
     # Overrides
 
     def focusInEvent(self, event):
@@ -499,9 +505,13 @@ class Grid(QTableView):
     def adjust_size(self):
         """Adjusts size to header maxima"""
 
-        w = self.horizontalHeader().length() + self.verticalHeader().width()
-        h = self.verticalHeader().length() + self.horizontalHeader().height()
-        self.resize(w, h)
+        horizontal_header = self.horizontalHeader()
+        vertical_header = self.verticalHeader()
+
+        width = horizontal_header.length() + vertical_header.width()
+        height = vertical_header.length() + horizontal_header.height()
+
+        self.resize(width, height)
 
     def _selected_idx_to_str(self, selected_idx: Iterable[QModelIndex]) -> str:
         """Converts selected_idx to string with cell indices
@@ -513,9 +523,9 @@ class Grid(QTableView):
         if len(selected_idx) <= 6:
             return ", ".join(str(self.model.current(idx))
                              for idx in selected_idx)
-        else:
-            return ", ".join(str(self.model.current(idx))
-                             for idx in selected_idx[:6]) + "..."
+
+        return ", ".join(str(self.model.current(idx))
+                         for idx in selected_idx[:6]) + "..."
 
     def update_zoom(self):
         """Updates the zoom level visualization to the current zoom factor"""
@@ -596,7 +606,7 @@ class Grid(QTableView):
             selected_cell_gen = self.selection.cell_generator(self.model.shape,
                                                               self.table)
             cell_list = list(selected_cell_gen)
-            msg = "Selection: {} cells".format(len(cell_list))
+            msg = f"Selection: {len(cell_list)} cells"
 
             res_gen = (self.model.code_array[key] for key in cell_list)
             sum_list = [res for res in res_gen if res is not None]
@@ -630,9 +640,10 @@ class Grid(QTableView):
         else:
             rows = [row]
 
-        description = "Resize rows {} to {}".format(rows, new_height)
-        command = commands.SetRowsHeight(self, rows, self.table, old_height,
-                                         new_height, description)
+        description = f"Resize rows {rows} to {new_height}"
+        command = commands.SetRowsHeight(self, rows, self.table,
+                                         old_height / self.zoom,
+                                         new_height / self.zoom, description)
         self.main_window.undo_stack.push(command)
 
     def on_column_resized(self, column: int, old_width: float,
@@ -654,9 +665,10 @@ class Grid(QTableView):
         else:
             columns = [column]
 
-        description = "Resize columns {} to {}".format(columns, new_width)
+        description = f"Resize columns {columns} to {new_width}"
         command = commands.SetColumnsWidth(self, columns, self.table,
-                                           old_width, new_width, description)
+                                           old_width / self.zoom,
+                                           new_width / self.zoom, description)
         self.main_window.undo_stack.push(command)
 
     def on_zoom_in(self):
@@ -750,7 +762,7 @@ class Grid(QTableView):
             attr_dict.strikethrough = font.strikeOut()
             attr = CellAttribute(self.selection, self.table, attr_dict)
             idx_string = self._selected_idx_to_str(self.selected_idx)
-            description = "Set font {} for indices {}".format(font, idx_string)
+            description = f"Set font {font} for indices {idx_string}"
             command = commands.SetCellFormat(attr, self.model,
                                              self.currentIndex(),
                                              self.selected_idx, description)
@@ -763,7 +775,7 @@ class Grid(QTableView):
         attr_dict = AttrDict([("textfont", font)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Set font {} for indices {}".format(font, idx_string)
+        description = f"Set font {font} for indices {idx_string}"
         command = commands.SetCellFormat(attr, self.model, self.currentIndex(),
                                          self.selected_idx, description)
         self.main_window.undo_stack.push(command)
@@ -775,7 +787,7 @@ class Grid(QTableView):
         attr_dict = AttrDict([("pointsize", size)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Set font size {} for cells {}".format(size, idx_string)
+        description = f"Set font size {size} for cells {idx_string}"
         command = commands.SetCellFormat(attr, self.model, self.currentIndex(),
                                          self.selected_idx, description)
         self.main_window.undo_stack.push(command)
@@ -791,8 +803,7 @@ class Grid(QTableView):
         attr_dict = AttrDict([("fontweight", fontweight)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Set font weight {} for cells {}".format(fontweight,
-                                                               idx_string)
+        description = f"Set font weight {fontweight} for cells {idx_string}"
         command = commands.SetCellFormat(attr, self.model, self.currentIndex(),
                                          self.selected_idx, description)
         self.main_window.undo_stack.push(command)
@@ -808,8 +819,7 @@ class Grid(QTableView):
         attr_dict = AttrDict([("fontstyle", fontstyle)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Set font style {} for cells {}".format(fontstyle,
-                                                              idx_string)
+        description = f"Set font style {fontstyle} for cells {idx_string}"
         command = commands.SetCellFormat(attr, self.model, self.currentIndex(),
                                          self.selected_idx, description)
         self.main_window.undo_stack.push(command)
@@ -824,8 +834,7 @@ class Grid(QTableView):
         attr_dict = AttrDict([("underline", toggled)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Set font underline {} for cells {}".format(toggled,
-                                                                  idx_string)
+        description = f"Set font underline {toggled} for cells {idx_string}"
         command = commands.SetCellFormat(attr, self.model, self.currentIndex(),
                                          self.selected_idx, description)
         self.main_window.undo_stack.push(command)
@@ -840,23 +849,19 @@ class Grid(QTableView):
         attr_dict = AttrDict([("strikethrough", toggled)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description_tpl = "Set font strikethrough {} for cells {}"
-        description = description_tpl.format(toggled, idx_string)
+        description = \
+            f"Set font strikethrough {toggled} for cells {idx_string}"
         command = commands.SetCellFormat(attr, self.model, self.currentIndex(),
                                          self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_text_renderer_pressed(self, toggled: bool):
-        """Text renderer button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_text_renderer_pressed(self):
+        """Text renderer button pressed event handler"""
 
         attr_dict = AttrDict([("renderer", "text")])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Set text renderer for cells {}".format(idx_string)
+        description = f"Set text renderer for cells {idx_string}"
         entry_line = self.main_window.entry_line
         document = entry_line.document()
 
@@ -870,34 +875,26 @@ class Grid(QTableView):
                                            self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_image_renderer_pressed(self, toggled: bool):
-        """Image renderer button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_image_renderer_pressed(self):
+        """Image renderer button pressed event handler"""
 
         attr_dict = AttrDict([("renderer", "image")])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Set image renderer for cells {}".format(idx_string)
+        description = f"Set image renderer for cells {idx_string}"
         entry_line = self.main_window.entry_line
         command = commands.SetCellRenderer(attr, self.model, entry_line, None,
                                            self.currentIndex(),
                                            self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_markup_renderer_pressed(self, toggled: bool):
-        """Markup renderer button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_markup_renderer_pressed(self):
+        """Markup renderer button pressed event handler"""
 
         attr_dict = AttrDict([("renderer", "markup")])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Set markup renderer for cells {}".format(idx_string)
+        description = f"Set markup renderer for cells {idx_string}"
         entry_line = self.main_window.entry_line
         document = entry_line.document()
 
@@ -911,17 +908,13 @@ class Grid(QTableView):
                                            self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_matplotlib_renderer_pressed(self, toggled: bool):
-        """Matplotlib renderer button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_matplotlib_renderer_pressed(self):
+        """Matplotlib renderer button pressed event handler"""
 
         attr_dict = AttrDict([("renderer", "matplotlib")])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Set matplotlib renderer for cells {}".format(idx_string)
+        description = f"Set matplotlib renderer for cells {idx_string}"
         entry_line = self.main_window.entry_line
         document = entry_line.document()
 
@@ -945,197 +938,145 @@ class Grid(QTableView):
         attr_dict = AttrDict([("locked", toggled)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Set locked state to {} for cells {}".format(toggled,
-                                                                   idx_string)
+        description = f"Set locked state to {toggled} for cells {idx_string}"
         command = commands.SetCellFormat(attr, self.model, self.currentIndex(),
                                          self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_rotate_0(self, toggled: bool):
-        """Set cell rotation to 0° left button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_rotate_0(self):
+        """Set cell rotation to 0° left button pressed event handler"""
 
         attr_dict = AttrDict([("angle", 0.0)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Set cell rotation to 0° for cells {}".format(idx_string)
+        description = f"Set cell rotation to 0° for cells {idx_string}"
         command = commands.SetCellTextAlignment(attr, self.model,
                                                 self.currentIndex(),
                                                 self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_rotate_90(self, toggled: bool):
-        """Set cell rotation to 90° left button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_rotate_90(self):
+        """Set cell rotation to 90° left button pressed event handler"""
 
         attr_dict = AttrDict([("angle", 90.0)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description_tpl = "Set cell rotation to 90° for cells {}"
-        description = description_tpl.format(idx_string)
+        description = f"Set cell rotation to 90° for cells {idx_string}"
         command = commands.SetCellTextAlignment(attr, self.model,
                                                 self.currentIndex(),
                                                 self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_rotate_180(self, toggled: bool):
-        """Set cell rotation to 180° left button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_rotate_180(self):
+        """Set cell rotation to 180° left button pressed event handler"""
 
         attr_dict = AttrDict([("angle", 180.0)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description_tpl = "Set cell rotation to 180° for cells {}"
-        description = description_tpl.format(idx_string)
+        description = f"Set cell rotation to 180° for cells {idx_string}"
         command = commands.SetCellTextAlignment(attr, self.model,
                                                 self.currentIndex(),
                                                 self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_rotate_270(self, toggled: bool):
-        """Set cell rotation to 270° left button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_rotate_270(self):
+        """Set cell rotation to 270° left button pressed event handler"""
 
         attr_dict = AttrDict([("angle", 270.0)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description_tpl = "Set cell rotation to 270° for cells {}"
-        description = description_tpl.format(idx_string)
+        description = f"Set cell rotation to 270° for cells {idx_string}"
         command = commands.SetCellTextAlignment(attr, self.model,
                                                 self.currentIndex(),
                                                 self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_justify_left(self, toggled: bool):
-        """Justify left button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_justify_left(self):
+        """Justify left button pressed event handler"""
 
         attr_dict = AttrDict([("justification", "justify_left")])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Justify cells {} left".format(idx_string)
+        description = f"Justify cells {idx_string} left"
         command = commands.SetCellTextAlignment(attr, self.model,
                                                 self.currentIndex(),
                                                 self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_justify_fill(self, toggled: bool):
-        """Justify fill button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_justify_fill(self):
+        """Justify fill button pressed event handler"""
 
         attr_dict = AttrDict([("justification", "justify_fill")])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Justify cells {} filled".format(idx_string)
+        description = f"Justify cells {idx_string} filled"
         command = commands.SetCellTextAlignment(attr, self.model,
                                                 self.currentIndex(),
                                                 self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_justify_center(self, toggled: bool):
-        """Justify center button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_justify_center(self):
+        """Justify center button pressed event handler"""
 
         attr_dict = AttrDict([("justification", "justify_center")])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Justify cells {} centered".format(idx_string)
+        description = f"Justify cells {idx_string} centered"
         command = commands.SetCellTextAlignment(attr, self.model,
                                                 self.currentIndex(),
                                                 self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_justify_right(self, toggled: bool):
-        """Justify right button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_justify_right(self):
+        """Justify right button pressed event handler"""
 
         attr_dict = AttrDict([("justification", "justify_right")])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Justify cells {} right".format(idx_string)
+        description = f"Justify cells {idx_string} right"
         command = commands.SetCellTextAlignment(attr, self.model,
                                                 self.currentIndex(),
                                                 self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_align_top(self, toggled: bool):
-        """Align top button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_align_top(self):
+        """Align top button pressed event handler"""
 
         attr_dict = AttrDict([("vertical_align", "align_top")])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Align cells {} to top".format(idx_string)
+        description = f"Align cells {idx_string} to top"
         command = commands.SetCellTextAlignment(attr, self.model,
                                                 self.currentIndex(),
                                                 self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_align_middle(self, toggled: bool):
-        """Align centere button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_align_middle(self):
+        """Align centere button pressed event handler"""
 
         attr_dict = AttrDict([("vertical_align", "align_center")])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Align cells {} to center".format(idx_string)
+        description = f"Align cells {idx_string} to center"
         command = commands.SetCellTextAlignment(attr, self.model,
                                                 self.currentIndex(),
                                                 self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_align_bottom(self, toggled: bool):
-        """Align bottom button pressed event handler
-
-        :param toggled: Toggle state
-
-        """
+    def on_align_bottom(self):
+        """Align bottom button pressed event handler"""
 
         attr_dict = AttrDict([("vertical_align", "align_bottom")])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description = "Align cells {} to bottom".format(idx_string)
+        description = f"Align cells {idx_string} to bottom"
         command = commands.SetCellTextAlignment(attr, self.model,
                                                 self.currentIndex(),
                                                 self.selected_idx, description)
         self.main_window.undo_stack.push(command)
 
-    def on_border_choice(self, event: QEvent):
-        """Border choice style event handler
-
-        :param event: Any event
-
-        """
+    def on_border_choice(self):
+        """Border choice style event handler"""
 
         self.main_window.settings.border_choice = self.sender().text()
         self.gui_update()
@@ -1148,8 +1089,8 @@ class Grid(QTableView):
         attr_dict = AttrDict([("textcolor", text_color_rgb)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description_tpl = "Set text color to {} for cells {}"
-        description = description_tpl.format(text_color_rgb, idx_string)
+        description = \
+            f"Set text color to {text_color_rgb} for cells {idx_string}"
         command = commands.SetCellFormat(attr, self.model, self.currentIndex(),
                                          self.selected_idx, description)
         self.main_window.undo_stack.push(command)
@@ -1175,8 +1116,7 @@ class Grid(QTableView):
         attr_right = CellAttribute(right_selection, self.table,
                                    attr_dict_right)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description_tpl = "Set line color {} for cells {}"
-        description = description_tpl.format(line_color_rgb, idx_string)
+        description = f"Set line color {line_color_rgb} for cells {idx_string}"
         command = commands.SetCellFormat(attr_bottom, self.model,
                                          self.currentIndex(),
                                          self.selected_idx, description)
@@ -1196,8 +1136,8 @@ class Grid(QTableView):
         attr_dict = AttrDict([("bgcolor", bg_color_rgb)])
         attr = CellAttribute(self.selection, self.table, attr_dict)
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description_tpl = "Set cell background color to {} for cells {}"
-        description = description_tpl.format(bg_color_rgb, idx_string)
+        description = f"Set cell background color to {bg_color_rgb} for " +\
+                      f"cells {idx_string}"
         command = commands.SetCellFormat(attr, self.model, self.currentIndex(),
                                          self.selected_idx, description)
         self.main_window.undo_stack.push(command)
@@ -1223,8 +1163,7 @@ class Grid(QTableView):
                                    attr_dict_right)
 
         idx_string = self._selected_idx_to_str(self.selected_idx)
-        description_tpl = "Set border width to {} for cells {}"
-        description = description_tpl.format(width, idx_string)
+        description = f"Set border width to {width} for cells {idx_string}"
         command = commands.SetCellFormat(attr_bottom, self.model,
                                          self.currentIndex(),
                                          self.selected_idx, description)
@@ -1241,7 +1180,7 @@ class Grid(QTableView):
 
         spans = {}  # Dict of (top, left): (bottom, right)
 
-        for selection, table, attrs in self.model.code_array.cell_attributes:
+        for _, table, attrs in self.model.code_array.cell_attributes:
             if table == self.table:
                 try:
                     if "merge_area" in attrs and attrs.merge_area is not None:
@@ -1301,11 +1240,11 @@ class Grid(QTableView):
                                                    table=self.table))
         if toggled:
             # We have an non-frozen cell that has to be frozen
-            description = "Freeze cells {}".format(cells)
+            description = f"Freeze cells {cells}"
             command = commands.FreezeCell(self.model, cells, description)
         else:
             # We have an frozen cell that has to be unfrozen
-            description = "Thaw cells {}".format(cells)
+            description = f"Thaw cells {cells}"
             command = commands.ThawCell(self.model, cells, description)
         self.main_window.undo_stack.push(command)
 
@@ -1331,15 +1270,13 @@ class Grid(QTableView):
                                                 "Button text:",
                                                 QLineEdit.Normal, "")
             if accept and text:
-                description_tpl = "Make cell {} a button cell"
-                description = description_tpl.format(grid.current)
+                description = f"Make cell {grid.current} a button cell"
                 command = commands.MakeButtonCell(self, text,
                                                   grid.currentIndex(),
                                                   description)
                 self.main_window.undo_stack.push(command)
         else:
-            description_tpl = "Make cell {} a non-button cell"
-            description = description_tpl.format(grid.current)
+            description = f"Make cell {grid.current} a non-button cell"
             command = commands.RemoveButtonCell(self, grid.currentIndex(),
                                                 description)
             self.main_window.undo_stack.push(command)
@@ -1362,18 +1299,17 @@ class Grid(QTableView):
             selection = Selection([], [], [], [], [(top, left)])
             attr_dict = AttrDict([("merge_area", None)])
             attr = CellAttribute(selection, self.table, attr_dict)
-            description_tpl = "Unmerge cells with top-left cell {}"
+            description = f"Unmerge cells with top-left cell {(top, left)}"
         elif bottom > top or right > left:
             # Merge and store the current selection
             merging_selection = Selection([], [], [], [], [(top, left)])
             attr_dict = AttrDict([("merge_area", (top, left, bottom, right))])
             attr = CellAttribute(merging_selection, self.table, attr_dict)
-            description_tpl = "Merge cells with top-left cell {}"
+            description = "Merge cells with top-left cell {(top, left)}"
         else:
             # Cells are not merged because the span is one
             return
 
-        description = description_tpl.format((top, left))
         command = commands.SetCellMerge(attr, self.model, self.currentIndex(),
                                         self.selected_idx, description)
         self.main_window.undo_stack.push(command)
@@ -1383,8 +1319,7 @@ class Grid(QTableView):
     def on_quote(self):
         """Quote cells event handler"""
 
-        description_tpl = "Quote code for cell selection {}"
-        description = description_tpl.format(id(self.selection))
+        description = f"Quote code for cell selection {id(self.selection)}"
 
         for idx in self.selected_idx:
             row = idx.row()
@@ -1454,8 +1389,7 @@ class Grid(QTableView):
                 return
 
         index = self.currentIndex()
-        description_tpl = "Insert {} rows above row {}"
-        description = description_tpl.format(count, top)
+        description = f"Insert {count} rows above row {top}"
         command = commands.InsertRows(self, self.model, index, top, count,
                                       description)
         self.main_window.undo_stack.push(command)
@@ -1471,8 +1405,7 @@ class Grid(QTableView):
         count = bottom - top + 1
 
         index = self.currentIndex()
-        description_tpl = "Delete {} rows starting from row {}"
-        description = description_tpl.format(count, top)
+        description = f"Delete {count} rows starting from row {top}"
         command = commands.DeleteRows(self, self.model, index, top, count,
                                       description)
         self.main_window.undo_stack.push(command)
@@ -1495,8 +1428,7 @@ class Grid(QTableView):
                 return
 
         index = self.currentIndex()
-        description_tpl = "Insert {} columns left of column {}"
-        description = description_tpl.format(count, left)
+        description = f"Insert {count} columns left of column {left}"
         command = commands.InsertColumns(self, self.model, index, left, count,
                                          description)
         self.main_window.undo_stack.push(command)
@@ -1512,8 +1444,8 @@ class Grid(QTableView):
         count = right - left + 1
 
         index = self.currentIndex()
-        description_tpl = "Delete {} columns starting from column {}"
-        description = description_tpl.format(count, self.column)
+        description = \
+            f"Delete {count} columns starting from column {self.column}"
         command = commands.DeleteColumns(self, self.model, index, left, count,
                                          description)
         self.main_window.undo_stack.push(command)
@@ -1528,7 +1460,7 @@ class Grid(QTableView):
             if DiscardDataDialog(self.main_window, text).choice is not True:
                 return
 
-        description = "Insert table in front of table {}".format(self.table)
+        description = f"Insert table in front of table {self.table}"
         command = commands.InsertTable(self, self.model, self.table,
                                        description)
         self.main_window.undo_stack.push(command)
@@ -1536,7 +1468,7 @@ class Grid(QTableView):
     def on_delete_table(self):
         """Delete table event handler"""
 
-        description = "Delete table {}".format(self.table)
+        description = f"Delete table {self.table}"
         command = commands.DeleteTable(self, self.model, self.table,
                                        description)
         self.main_window.undo_stack.push(command)
@@ -1587,10 +1519,10 @@ class GridHeaderView(QHeaderView):
         """
 
         unzoomed_rect = QRect(0, 0,
-                              rect.width()//self.grid.zoom,
-                              rect.height()//self.grid.zoom)
+                              int(round(rect.width()/self.grid.zoom)),
+                              int(round(rect.height()/self.grid.zoom)))
         with painter_save(painter):
-            painter.translate(rect.x(), rect.y())
+            painter.translate(rect.x()+1, rect.y()+1)
             painter.scale(self.grid.zoom, self.grid.zoom)
             super().paintSection(painter, unzoomed_rect, logicalIndex)
 
@@ -1720,6 +1652,8 @@ class GridTableModel(QAbstractTableModel):
 
     @property
     def grid(self) -> Grid:
+        """The main grid"""
+
         return self.main_window.grid
 
     @property
@@ -1976,7 +1910,7 @@ class GridTableModel(QAbstractTableModel):
                 else:
                     self.code_array[key] = value
             else:
-                self.code_array[key] = "{}".format(value)
+                self.code_array[key] = f"{value}"
 
             if not self.main_window.prevent_updates:
                 self.dataChanged.emit(index, index)
@@ -1985,9 +1919,8 @@ class GridTableModel(QAbstractTableModel):
 
         if role in (Qt.DecorationRole, Qt.TextAlignmentRole):
             if not isinstance(value[2], AttrDict):
-                msg_tpl = "{} has type {} that is not instance of AttrDict"
-                msg = msg_tpl.format(value[2], type(value[2]))
-                raise Warning(msg)
+                raise Warning(f"{value[2]} has type {type(value[2])} that "
+                              "is not instance of AttrDict")
             self.code_array.cell_attributes.append(value)
             # We have a selection and no single cell
             with self.main_window.workflows.busy_cursor():
@@ -2082,7 +2015,7 @@ class GridCellDelegate(QStyledItemDelegate):
         doc.setDefaultTextOption(QTextOption(alignment))
 
         bg_color = self.grid.model.data(index, role=Qt.BackgroundColorRole)
-        css = "background-color: {bg_color};".format(bg_color=bg_color)
+        css = f"background-color: {bg_color};"
         doc.setDefaultStyleSheet(css)
 
         doc.setTextWidth(rect.width())
@@ -2236,62 +2169,23 @@ class GridCellDelegate(QStyledItemDelegate):
         return QRectF(image_x, image_y, image_width, image_height)
 
     def _render_qimage(self, painter: QPainter, rect: QRectF,
-                       index: QModelIndex, qimage: Union[str, QImage] = None):
+                       index: QModelIndex, qimage: QImage = None):
         """QImage renderer
 
         :param painter: Painter with which qimage is rendered
         :param rect: Cell rect of the cell to be painted
         :param index: Index of cell for which qimage is rendered
-        :param qimage: Image to be rendered, may be svg string
+        :param qimage: Image to be rendered, decoration drawn if not provided
 
         """
 
         if qimage is None:
             qimage = index.data(Qt.DecorationRole)
 
-        if isinstance(qimage, QImage):
-            img_width, img_height = qimage.width(), qimage.height()
-        else:
-            if qimage is None:
-                return
-            try:
-                svg_bytes = bytes(qimage)
-            except TypeError:
-                try:
-                    svg_bytes = bytes(qimage, encoding='utf-8')
-                except TypeError:
-                    return
-
-            if not is_svg(svg_bytes):
-                return
+        if not isinstance(qimage, QImage):
+            raise TypeError(f"{qimage} not of type QImage")
 
-            svg_width, svg_height = get_svg_size(svg_bytes)
-
-            try:
-                svg_aspect = svg_width / svg_height
-            except ZeroDivisionError:
-                svg_aspect = 1
-            try:
-                rect_aspect = rect.width() / rect.height()
-            except ZeroDivisionError:
-                rect_aspect = 1
-
-            if svg_aspect > rect_aspect:
-                # svg is wider than rect --> shrink height
-                img_width = rect.width()
-                img_height = rect.width() / svg_aspect
-            else:
-                img_width = rect.height() * svg_aspect
-                img_height = rect.height()
-
-            if self.main_window.settings.print_zoom is not None:
-                img_width *= self.main_window.settings.print_zoom
-                img_height *= self.main_window.settings.print_zoom
-            else:  # Adjust for HiDpi
-                img_width *= 3
-                img_height *= 3
-            qimage = QImageSvg(img_width, img_height, QImage.Format_ARGB32)
-            qimage.from_svg_bytes(svg_bytes)
+        img_width, img_height = qimage.width(), qimage.height()
 
         img_rect = self._get_aligned_image_rect(rect, index,
                                                 img_width, img_height)
@@ -2302,11 +2196,11 @@ class GridCellDelegate(QStyledItemDelegate):
         justification = self.cell_attributes[key].justification
 
         if justification == "justify_fill":
-            qimage = qimage.scaled(img_width, img_height,
+            qimage = qimage.scaled(int(img_width), int(img_height),
                                    Qt.IgnoreAspectRatio,
                                    Qt.SmoothTransformation)
         else:
-            qimage = qimage.scaled(img_width, img_height,
+            qimage = qimage.scaled(int(img_width), int(img_height),
                                    Qt.KeepAspectRatio,
                                    Qt.SmoothTransformation)
 
@@ -2323,6 +2217,74 @@ class GridCellDelegate(QStyledItemDelegate):
             painter.scale(scale_x, scale_y)
             painter.drawImage(0, 0, qimage)
 
+    def _render_svg(self, painter: QPainter, rect: QRectF, index: QModelIndex,
+                    svg_str: str = None):
+        """SVG renderer
+
+        :param painter: Painter with which qimage is rendered
+        :param rect: Cell rect of the cell to be painted
+        :param index: Index of cell for which qimage is rendered
+        :param svg_str: SVG string
+
+        """
+
+        if svg_str is None:
+            svg_str = index.data(Qt.DecorationRole)
+
+        if svg_str is None:
+            return
+        try:
+            svg_bytes = bytes(svg_str)
+        except TypeError:
+            try:
+                svg_bytes = bytes(svg_str, encoding='utf-8')
+            except TypeError:
+                return
+
+        if not is_svg(svg_bytes):
+            return
+
+        key = index.row(), index.column(), self.grid.table
+        justification = self.cell_attributes[key].justification
+
+        svg = QSvgRenderer(QByteArray(svg_bytes))
+
+        if justification == "justify_fill":
+            svg.setAspectRatioMode(Qt.IgnoreAspectRatio)
+            svg_rect = rect
+            svg.render(painter, svg_rect)
+            return
+
+        svg.setAspectRatioMode(Qt.KeepAspectRatio)
+
+        svg_size = svg.defaultSize()
+
+        try:
+            svg_aspect = svg_size.width() / svg_size.height()
+        except ZeroDivisionError:
+            svg_aspect = 1
+        try:
+            rect_aspect = rect.width() / rect.height()
+        except ZeroDivisionError:
+            rect_aspect = 1
+
+        if svg_aspect > rect_aspect:
+            # svg is wider than rect
+            svg_width = rect.width()
+            svg_height = rect.width() / svg_aspect
+        else:
+            # svg is taller than rect
+            svg_width = rect.height() * svg_aspect
+            svg_height = rect.height()
+
+        svg_rect = self._get_aligned_image_rect(rect, index,
+                                                svg_width, svg_height)
+
+        if svg_rect is None:
+            return
+
+        svg.render(painter, svg_rect)
+
     def _render_matplotlib(self, painter: QPainter, rect: QRectF,
                            index: QModelIndex):
         """Matplotlib renderer
@@ -2346,12 +2308,12 @@ class GridCellDelegate(QStyledItemDelegate):
         # Save SVG in a fake file object.
         with BytesIO() as filelike:
             try:
-                figure.savefig(filelike, format="svg")
+                figure.savefig(filelike, format="svg", bbox_inches="tight")
             except Exception:
                 return
             svg_str = filelike.getvalue().decode()
 
-        self._render_qimage(painter, rect, index, qimage=svg_str)
+        self._render_svg(painter, rect, index, svg_str=svg_str)
 
     def paint_(self, painter: QPainter, rect: QRectF,
                option: QStyleOptionViewItem, index: QModelIndex):
@@ -2364,6 +2326,11 @@ class GridCellDelegate(QStyledItemDelegate):
 
         """
 
+        painter.setRenderHints(QPainter.LosslessImageRendering
+                               | QPainter.Antialiasing
+                               | QPainter.TextAntialiasing
+                               | QPainter.SmoothPixmapTransform)
+
         key = index.row(), index.column(), self.grid.table
         renderer = self.cell_attributes[key].renderer
 
@@ -2379,7 +2346,11 @@ class GridCellDelegate(QStyledItemDelegate):
             self._render_markup(painter, rect, option, index)
 
         elif renderer == "image":
-            self._render_qimage(painter, rect, index)
+            image = index.data(Qt.DecorationRole)
+            if isinstance(image, QImage):
+                self._render_qimage(painter, rect, index)
+            elif isinstance(image, str):
+                self._render_svg(painter, rect, index)
 
         elif renderer == "matplotlib":
             self._render_matplotlib(painter, rect, index)
@@ -2397,7 +2368,7 @@ class GridCellDelegate(QStyledItemDelegate):
 
         key = index.row(), index.column(), self.grid.table
         if not self.cell_attributes[key].renderer == "markup":
-            return super(GridCellDelegate, self).sizeHint(option, index)
+            return super().sizeHint(option, index)
 
         # HTML
         options = QStyleOptionViewItem(option)
@@ -2444,8 +2415,7 @@ class GridCellDelegate(QStyledItemDelegate):
             self.main_window.workflows.macro_insert_chart()
             return
 
-        self.editor = super(GridCellDelegate, self).createEditor(parent,
-                                                                 option, index)
+        self.editor = super().createEditor(parent, option, index)
         self.editor.setPalette(self.editor.style().standardPalette())
         self.editor.installEventFilter(self)
         return self.editor
@@ -2468,11 +2438,11 @@ class GridCellDelegate(QStyledItemDelegate):
 
             code = quote(source.text())
             index = self.grid.currentIndex()
-            description = "Quote code for cell {}".format(index)
+            description = f"Quote code for cell {index}"
             cmd = commands.SetCellCode(code, self.grid.model, index,
                                        description)
             self.main_window.undo_stack.push(cmd)
-        return QWidget.eventFilter(self, source, event)
+        return super().eventFilter(source, event)
 
     def setEditorData(self, editor: QWidget, index: QModelIndex):
         """Overloads `setEditorData` to use code_array data
@@ -2499,7 +2469,7 @@ class GridCellDelegate(QStyledItemDelegate):
 
         """
 
-        description = "Set code for cell {}".format(model.current(index))
+        description = f"Set code for cell {model.current(index)}"
         command = commands.SetCellCode(editor.text(), model, index,
                                        description)
         self.main_window.undo_stack.push(command)
diff --git a/pyspread/grid_renderer.py b/pyspread/grid_renderer.py
index 6a5e2084116ddff0391ba573dcd7505284d200bc..7a0501c5408d9901d359501542acf83b3ec28bfb 100644
--- a/pyspread/grid_renderer.py
+++ b/pyspread/grid_renderer.py
@@ -40,8 +40,9 @@ except ImportError:
     from pyspread.lib.dataclasses import dataclass  # Python 3.6 compatibility
 from typing import List, Tuple
 
-from PyQt5.QtCore import Qt, QModelIndex, QRectF, QLineF, QPointF
-from PyQt5.QtGui import QBrush, QColor, QPainter, QPalette, QPen
+from PyQt5.QtCore import Qt, QModelIndex, QRectF, QPointF
+from PyQt5.QtGui import (QBrush, QColor, QPainter, QPalette, QPen,
+                         QPainterPath, QPolygonF, QPainterPathStroker)
 from PyQt5.QtWidgets import QTableView, QStyleOptionViewItem
 
 
@@ -227,7 +228,7 @@ class GridCellNavigator:
 
 @dataclass
 class EdgeBorders:
-    """Holds border data for an edge"""
+    """Holds border data for an edge and provides effective edge properties"""
 
     left_width: float
     right_width: float
@@ -245,25 +246,50 @@ class EdgeBorders:
     bottom_y: float
 
     @property
-    def widths(self) -> Tuple[float, float, float, float]:
+    def _border_widths(self) -> Tuple[float, float, float, float]:
         """Tuple of border widths in order left, right, top, bottom"""
 
         return (self.left_width, self.right_width,
                 self.top_width, self.bottom_width)
 
     @property
-    def colors(self) -> Tuple[QColor, QColor, QColor, QColor]:
+    def _border_colors(self) -> Tuple[QColor, QColor, QColor, QColor]:
         """Tuple of border colors in order left, right, top, bottom"""
 
         return (self.left_color, self.right_color,
                 self.top_color, self.bottom_color)
 
+    @property
+    def width(self) -> float:
+        """Edge width, i.e. thickest vertical border"""
+
+        return max(self.top_width, self.bottom_width)
+
+    @property
+    def height(self) -> float:
+        """Edge height, i.e. thickest horizontal border"""
+
+        return max(self.left_width, self.right_width)
+
+    @property
+    def color(self) -> QColor:
+        """Edge color, i.e. darkest color of thickest edge border"""
+
+        max_border_width = max(self.width, self.height)
+        colors = []
+        for width, color in zip(self._border_widths, self._border_colors):
+            if width == max_border_width:
+                colors.append(color)
+        colors.sort(key=lambda color: color.lightnessF())
+
+        return colors[0]
+
 
 class CellEdgeRenderer:
     """Paints cell edges"""
 
     def __init__(self, painter: QPainter, center: QPointF,
-                 borders: EdgeBorders):
+                 borders: EdgeBorders, clip_path: QPainterPath, zoom: float):
         """
 
         Borders are provided by EdgeBorders in order: left, right, top, bottom
@@ -271,26 +297,39 @@ class CellEdgeRenderer:
         :param painter: Painter with which edge is drawn
         :param center: Edge center
         :param borders: Border widths and colors
+        :param clip_path: Clip rectangle that is requuired for QtSVG clipping
+        :param zoom: Current zoom level
 
         """
 
         self.painter = painter
-
-        lines = [QLineF(center.x(), center.y(), borders.left_x, center.y()),
-                 QLineF(center.x(), center.y(), borders.right_x, center.y()),
-                 QLineF(center.x(), center.y(), center.x(), borders.top_y),
-                 QLineF(center.x(), center.y(), center.x(), borders.bottom_y)]
-
-        self.edge_data = list(zip(borders.widths, borders.colors, lines))
-        self.edge_data.sort(key=lambda edge: (-edge[1].lightnessF(), edge[0]))
+        self.center = center
+        self.borders = borders
+        self.clip_path = clip_path
+        self.zoom = zoom
 
     def paint(self):
         """Paints the edge"""
 
-        for width, color, line in self.edge_data:
-            self.painter.setPen(QPen(QBrush(color), width,
-                                     Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin))
-            self.painter.drawLine(line)
+        if not self.borders.width or not self.borders.height:
+            return  # Invisible edge
+
+        x, y = self.center.x(), self.center.y()
+        width = self.borders.width * self.zoom
+        height = self.borders.height * self.zoom
+
+        rect = QRectF(x-width/2, y-height/2, width, height)
+
+        rect_path = QPainterPath()  # Required for clipping in SVG export
+        rect_path.addRect(rect)
+
+        color = self.borders.color
+
+        self.painter.setPen(QPen(Qt.NoPen))
+        self.painter.setBrush(QBrush(color))
+
+        self.painter.drawPath(self.clip_path.intersected(rect_path))
+        self.painter.setPen(QPen(Qt.SolidLine))
 
 
 class QColorCache(dict):
@@ -387,10 +426,24 @@ class CellRenderer:
             self.grid.delegate.paint_(self.painter, zrect, self.option,
                                       self.index)
 
-    def paint_bottom_border(self, rect: QRectF):
+    def _get_border_pen(self, width, zoom):
+        """Gets zoomed border pen
+
+        :param width: Unzoomed line width
+        :param zoom: Current zoom level of grid
+
+        """
+
+        zoomed_width = max(1, width * zoom)
+
+        return QPen(QColor(255, 255, 255, 0), zoomed_width,
+                    Qt.SolidLine, Qt.FlatCap, Qt.MiterJoin)
+
+    def paint_bottom_border(self, rect: QRectF, clip_path: QPainterPath):
         """Paint bottom border of cell
 
         :param rect: Cell rect of the cell to be painted
+        :param clip_path: Clip rectangle that is required for QtSVG clipping
 
         """
 
@@ -398,20 +451,30 @@ class CellRenderer:
             return
 
         line_color = self.qcolor_cache[self.cell_nav.border_color_bottom]
-        line_width = self.cell_nav.borderwidth_bottom * self.grid.zoom
-        self.painter.setPen(QPen(QBrush(line_color), line_width,
-                                 Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin))
 
-        bottom_border_line = QLineF(rect.x(),
-                                    rect.y() + rect.height(),
-                                    rect.x() + rect.width(),
-                                    rect.y() + rect.height())
-        self.painter.drawLine(bottom_border_line)
+        point1 = QPointF(rect.x(), rect.y() + rect.height())
+        point2 = QPointF(rect.x() + rect.width(), rect.y() + rect.height())
+        line_polygon = QPolygonF((point1, point2))
+        line_path = QPainterPath()
+        line_path.addPolygon(line_polygon)
+
+        pen = self._get_border_pen(self.cell_nav.borderwidth_bottom,
+                                   self.grid.zoom)
+        stroker = QPainterPathStroker(pen)
+        stroked_path = stroker.createStroke(line_path)
+
+        alpha = max(0, round(255 - 255 * self.cell_nav.borderwidth_bottom *
+                             self.grid.zoom))
 
-    def paint_right_border(self, rect: QRectF):
+        self.painter.setPen(QPen(QColor(255, 255, 255, alpha)))
+        self.painter.setBrush(QBrush(line_color))
+        self.painter.drawPath(clip_path.intersected(stroked_path))
+
+    def paint_right_border(self, rect: QRectF, clip_path: QPainterPath):
         """Paint right border of cell
 
         :param rect: Cell rect of the cell to be painted
+        :param clip_path: Clip rectangle that is requuired for QtSVG clipping
 
         """
 
@@ -419,25 +482,38 @@ class CellRenderer:
             return
 
         line_color = self.qcolor_cache[self.cell_nav.border_color_right]
-        line_width = self.cell_nav.borderwidth_right * self.grid.zoom
-        self.painter.setPen(QPen(QBrush(line_color), line_width,
-                                 Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin))
 
-        right_border_line = QLineF(rect.x() + rect.width(),
-                                   rect.y(),
-                                   rect.x() + rect.width(),
-                                   rect.y() + rect.height())
-        self.painter.drawLine(right_border_line)
+        point1 = QPointF(rect.x() + rect.width(), rect.y())
+        point2 = QPointF(rect.x() + rect.width(), rect.y() + rect.height())
+        line_polygon = QPolygonF((point1, point2))
+        line_path = QPainterPath()
+        line_path.addPolygon(line_polygon)
+
+        pen = self._get_border_pen(self.cell_nav.borderwidth_right,
+                                   self.grid.zoom)
+        stroker = QPainterPathStroker(pen)
+        stroked_path = stroker.createStroke(line_path)
+
+        alpha = max(0, round(255 - 255 * self.cell_nav.borderwidth_bottom *
+                             self.grid.zoom))
 
-    def paint_above_borders(self, rect: QRectF):
+        self.painter.setPen(QPen(QColor(255, 255, 255, alpha)))
+        self.painter.setBrush(QBrush(line_color))
+        self.painter.drawPath(clip_path.intersected(stroked_path))
+
+    def paint_above_borders(self, rect: QRectF, clip_path: QPainterPath):
         """Paint lower borders of all above cells
 
         :param rect: Cell rect of below cell, in which the borders are painted
+        :param clip_path: Clip rectangle that is requuired for QtSVG clipping
 
         """
 
         for above_key in self.cell_nav.above_keys():
             above_cell_nav = GridCellNavigator(self.grid, above_key)
+            if not above_cell_nav.borderwidth_bottom:
+                continue
+
             merge_area = above_cell_nav.merge_area
             if merge_area is None:
                 columns = [above_key[1]]
@@ -449,25 +525,39 @@ class CellRenderer:
                                    for column in columns)
 
             line_color = self.qcolor_cache[above_cell_nav.border_color_bottom]
-            line_width = above_cell_nav.borderwidth_bottom * self.grid.zoom
-            self.painter.setPen(QPen(QBrush(line_color), line_width,
-                                     Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin))
 
-            above_border_line = QLineF(above_rect_x,
-                                       rect.y(),
-                                       above_rect_x + above_rect_width,
-                                       rect.y())
-            self.painter.drawLine(above_border_line)
+            point1 = QPointF(above_rect_x, rect.y())
+            point2 = QPointF(above_rect_x + above_rect_width, rect.y())
+            line_polygon = QPolygonF((point1, point2))
+            line_path = QPainterPath()
+            line_path.addPolygon(line_polygon)
 
-    def paint_left_borders(self, rect: QRectF):
+            pen = self._get_border_pen(above_cell_nav.borderwidth_bottom,
+                                       self.grid.zoom)
+            stroker = QPainterPathStroker(pen)
+            stroked_path = stroker.createStroke(line_path)
+
+            alpha = max(0, round(255 - 255 *
+                                 above_cell_nav.borderwidth_bottom *
+                                 self.grid.zoom))
+
+            self.painter.setPen(QPen(QColor(255, 255, 255, alpha)))
+            self.painter.setBrush(QBrush(line_color))
+            self.painter.drawPath(clip_path.intersected(stroked_path))
+
+    def paint_left_borders(self, rect: QRectF, clip_path: QPainterPath):
         """Paint right borders of all left cells
 
         :param rect: Cell rect of right cell, in which the borders are painted
+        :param clip_path: Clip rectangle that is requuired for QtSVG clipping
 
         """
 
         for left_key in self.cell_nav.left_keys():
             left_cell_nav = GridCellNavigator(self.grid, left_key)
+            if not left_cell_nav.borderwidth_right:
+                continue
+
             merge_area = left_cell_nav.merge_area
             if merge_area is None:
                 rows = [left_key[0]]
@@ -478,20 +568,31 @@ class CellRenderer:
             left_rect_height = sum(self.grid.rowHeight(row) for row in rows)
 
             line_color = self.qcolor_cache[left_cell_nav.border_color_right]
-            line_width = left_cell_nav.borderwidth_right * self.grid.zoom
-            self.painter.setPen(QPen(QBrush(line_color), line_width,
-                                     Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin))
 
-            above_border_line = QLineF(rect.x(),
-                                       left_rect_y,
-                                       rect.x(),
-                                       left_rect_y + left_rect_height)
-            self.painter.drawLine(above_border_line)
+            point1 = QPointF(rect.x(), left_rect_y)
+            point2 = QPointF(rect.x(), left_rect_y + left_rect_height)
+            line_polygon = QPolygonF((point1, point2))
+            line_path = QPainterPath()
+            line_path.addPolygon(line_polygon)
+
+            pen = self._get_border_pen(left_cell_nav.borderwidth_right,
+                                       self.grid.zoom)
+            stroker = QPainterPathStroker(pen)
+            stroked_path = stroker.createStroke(line_path)
 
-    def paint_top_left_edge(self, rect: QRectF):
+            alpha = max(0, round(255 - 255 *
+                                 left_cell_nav.borderwidth_right *
+                                 self.grid.zoom))
+
+            self.painter.setPen(QPen(QColor(255, 255, 255, alpha)))
+            self.painter.setBrush(QBrush(line_color))
+            self.painter.drawPath(clip_path.intersected(stroked_path))
+
+    def paint_top_left_edge(self, rect: QRectF, clip_path: QPainterPath):
         """Paints top left edge of the cell
 
         :param rect: Cell rect of cell, for which the edge is painted
+        :param clip_path: Clip rectangle that is requuired for QtSVG clipping
 
                   top
                TL  |  T
@@ -511,10 +612,10 @@ class CellRenderer:
         left_cell_nav = GridCellNavigator(self.grid, left_key)
         top_cell_nav = GridCellNavigator(self.grid, top_key)
 
-        left_width = top_left_cell_nav.borderwidth_bottom * self.grid.zoom
-        right_width = top_cell_nav.borderwidth_bottom * self.grid.zoom
-        top_width = top_left_cell_nav.borderwidth_right * self.grid.zoom
-        bottom_width = left_cell_nav.borderwidth_right * self.grid.zoom
+        left_width = top_left_cell_nav.borderwidth_bottom
+        right_width = top_cell_nav.borderwidth_bottom
+        top_width = top_left_cell_nav.borderwidth_right
+        bottom_width = left_cell_nav.borderwidth_right
 
         left_color = self.qcolor_cache[top_left_cell_nav.border_color_bottom]
         right_color = self.qcolor_cache[top_cell_nav.border_color_bottom]
@@ -530,13 +631,15 @@ class CellRenderer:
                               left_color, right_color, top_color, bottom_color,
                               left_x, right_x, top_y, bottom_y)
 
-        renderer = CellEdgeRenderer(self.painter, center, borders)
+        renderer = CellEdgeRenderer(self.painter, center, borders, clip_path,
+                                    self.grid.zoom)
         renderer.paint()
 
-    def paint_top_right_edge(self, rect: QRectF):
+    def paint_top_right_edge(self, rect: QRectF, clip_path: QPainterPath):
         """Paints top right edge of the cell
 
         :param rect: Cell rect of cell, for which the edge is painted
+        :param clip_path: Clip rectangle that is requuired for QtSVG clipping
 
                   top
                 T  |  TR
@@ -554,10 +657,10 @@ class CellRenderer:
         top_cell_nav = GridCellNavigator(self.grid, top_key)
         top_right_cell_nav = GridCellNavigator(self.grid, top_right_key)
 
-        left_width = top_cell_nav.borderwidth_bottom * self.grid.zoom
-        right_width = top_right_cell_nav.borderwidth_bottom * self.grid.zoom
-        top_width = top_cell_nav.borderwidth_right * self.grid.zoom
-        bottom_width = self.cell_nav.borderwidth_right * self.grid.zoom
+        left_width = top_cell_nav.borderwidth_bottom
+        right_width = top_right_cell_nav.borderwidth_bottom
+        top_width = top_cell_nav.borderwidth_right
+        bottom_width = self.cell_nav.borderwidth_right
 
         left_color = self.qcolor_cache[top_cell_nav.border_color_bottom]
         right_color = self.qcolor_cache[top_right_cell_nav.border_color_bottom]
@@ -573,13 +676,15 @@ class CellRenderer:
                               left_color, right_color, top_color, bottom_color,
                               left_x, right_x, top_y, bottom_y)
 
-        renderer = CellEdgeRenderer(self.painter, center, borders)
+        renderer = CellEdgeRenderer(self.painter, center, borders, clip_path,
+                                    self.grid.zoom)
         renderer.paint()
 
-    def paint_bottom_left_edge(self, rect: QRectF):
+    def paint_bottom_left_edge(self, rect: QRectF, clip_path: QPainterPath):
         """Paints bottom left edge of the cell
 
         :param rect: Cell rect of cell, for which the edge is painted
+        :param clip_path: Clip rectangle that is requuired for QtSVG clipping
 
                   top
                L   |  C
@@ -597,10 +702,10 @@ class CellRenderer:
         left_cell_nav = GridCellNavigator(self.grid, left_key)
         bottom_left_cell_nav = GridCellNavigator(self.grid, bottom_left_key)
 
-        left_width = left_cell_nav.borderwidth_bottom * self.grid.zoom
-        right_width = self.cell_nav.borderwidth_bottom * self.grid.zoom
-        top_width = left_cell_nav.borderwidth_right * self.grid.zoom
-        bottom_width = bottom_left_cell_nav.borderwidth_right * self.grid.zoom
+        left_width = left_cell_nav.borderwidth_bottom
+        right_width = self.cell_nav.borderwidth_bottom
+        top_width = left_cell_nav.borderwidth_right
+        bottom_width = bottom_left_cell_nav.borderwidth_right
 
         left_color = self.qcolor_cache[left_cell_nav.border_color_bottom]
         right_color = self.qcolor_cache[self.cell_nav.border_color_bottom]
@@ -617,13 +722,15 @@ class CellRenderer:
                               left_color, right_color, top_color, bottom_color,
                               left_x, right_x, top_y, bottom_y)
 
-        renderer = CellEdgeRenderer(self.painter, center, borders)
+        renderer = CellEdgeRenderer(self.painter, center, borders, clip_path,
+                                    self.grid.zoom)
         renderer.paint()
 
-    def paint_bottom_right_edge(self, rect: QRectF):
+    def paint_bottom_right_edge(self, rect: QRectF, clip_path: QPainterPath):
         """Paints bottom right edge of the cell
 
         :param rect: Cell rect of cell, for which the edge is painted
+        :param clip_path: Clip rectangle that is requuired for QtSVG clipping
 
                  top
                C  |  R
@@ -641,10 +748,10 @@ class CellRenderer:
         right_cell_nav = GridCellNavigator(self.grid, right_key)
         bottom_cell_nav = GridCellNavigator(self.grid, bottom_key)
 
-        left_width = self.cell_nav.borderwidth_bottom * self.grid.zoom
-        right_width = right_cell_nav.borderwidth_bottom * self.grid.zoom
-        top_width = self.cell_nav.borderwidth_right * self.grid.zoom
-        bottom_width = bottom_cell_nav.borderwidth_right * self.grid.zoom
+        left_width = self.cell_nav.borderwidth_bottom
+        right_width = right_cell_nav.borderwidth_bottom
+        top_width = self.cell_nav.borderwidth_right
+        bottom_width = bottom_cell_nav.borderwidth_right
 
         left_color = self.qcolor_cache[self.cell_nav.border_color_bottom]
         right_color = self.qcolor_cache[right_cell_nav.border_color_bottom]
@@ -660,21 +767,29 @@ class CellRenderer:
                               left_color, right_color, top_color, bottom_color,
                               left_x, right_x, top_y, bottom_y)
 
-        renderer = CellEdgeRenderer(self.painter, center, borders)
+        renderer = CellEdgeRenderer(self.painter, center, borders, clip_path,
+                                    self.grid.zoom)
         renderer.paint()
 
     def paint_borders(self, rect):
         """Paint cell borders"""
 
-        self.paint_bottom_border(rect)
-        self.paint_right_border(rect)
-        self.paint_above_borders(rect)
-        self.paint_left_borders(rect)
+        clip_path = QPainterPath()  # Required for clipping in SVG export
+        clip_path.addRect(rect)
+
+        self.painter.save()
+
+        self.paint_bottom_border(rect, clip_path)
+        self.paint_right_border(rect, clip_path)
+        self.paint_above_borders(rect, clip_path)
+        self.paint_left_borders(rect, clip_path)
+
+        self.paint_top_left_edge(rect, clip_path)
+        self.paint_top_right_edge(rect, clip_path)
+        self.paint_bottom_left_edge(rect, clip_path)
+        self.paint_bottom_right_edge(rect, clip_path)
 
-        self.paint_top_left_edge(rect)
-        self.paint_top_right_edge(rect)
-        self.paint_bottom_left_edge(rect)
-        self.paint_bottom_right_edge(rect)
+        self.painter.restore()
 
     def paint(self):
         """Paints the cell"""
diff --git a/pyspread/icons.py b/pyspread/icons.py
index 3d43305dd2c282517faf6e8d714081d06d655de6..c744bf8790f2315e219f27440ac042ff3f31e048 100644
--- a/pyspread/icons.py
+++ b/pyspread/icons.py
@@ -41,7 +41,7 @@ except ImportError:
 class IconPath:
     """Holds icon paths as attributes"""
 
-    pyspread = ICON_PATH / 'pyspread.svg'
+    pyspread = ICON_PATH / 'hicolor' / 'svg' / 'pyspread.svg'
 
     # Status icons
     safe_mode = STATUS_PATH / 'status-safe-mode.svg'
@@ -75,6 +75,8 @@ class IconPath:
     find = ACTION_PATH / 'edit-find.svg'
     find_next = ACTION_PATH / 'edit-find-next.svg'
     replace = ACTION_PATH / 'edit-find-replace.svg'
+    sort_ascending = ACTION_PATH / 'edit-sort-ascending.svg'
+    sort_descending = ACTION_PATH / 'edit-sort-descending.svg'
     quote = ACTION_PATH / 'edit-quote.svg'
     sort_ascending = ACTION_PATH / 'edit-sort-ascending.svg'
     sort_descending = ACTION_PATH / 'edit-sort-descending.svg'
@@ -151,6 +153,7 @@ class IconPath:
     insert_image = ACTION_PATH / 'macro-insert-image.svg'
     link_image = ACTION_PATH / 'macro-link-image.svg'
     insert_chart = ACTION_PATH / 'macro-insert-chart.svg'
+    insert_sum = ACTION_PATH / 'macro-insert-sum.svg'
 
     # Help menu icons
     help = ACTION_PATH / 'help-browser.svg'
diff --git a/pyspread/installer.py b/pyspread/installer.py
index d5e9269bb96951736a70b2429d11c60a36f61de7..c95fc712524eddd048cb7c5326771b156532d640 100644
--- a/pyspread/installer.py
+++ b/pyspread/installer.py
@@ -124,6 +124,9 @@ OPTIONAL_DEPENDENCIES = [
            description="The dateutil module provides powerful extensions to "
                        "the standard datetime module, available in Python.",
            required_version=version.parse("2.7.0")),
+    Module(name="py-moneyed",
+           description="Import money from csv using the py-moneyed module",
+           required_version=version.parse("2.0")),
 ]
 
 DEPENDENCIES = REQUIRED_DEPENDENCIES + OPTIONAL_DEPENDENCIES
diff --git a/pyspread/lib/csv.py b/pyspread/lib/csv.py
index 4f01dbbdfad2d96ebb61f0d2f7e01ec0dbcbb424..b37aa693228578c9889633dc96396a9b6f7bc7a1 100644
--- a/pyspread/lib/csv.py
+++ b/pyspread/lib/csv.py
@@ -37,6 +37,7 @@
 
 import ast
 import csv
+from decimal import Decimal
 from pathlib import Path
 from typing import TextIO, Iterable, List
 
@@ -45,27 +46,39 @@ try:
 except ImportError:
     parse = None
 
+try:
+    from moneyed import Money, list_all_currencies
+except ImportError:
+    Money = None
+
 
-def sniff(filepath: Path, sniff_size: int) -> csv.Dialect:
+def sniff(filepath: Path, sniff_size: int, encoding: str) -> csv.Dialect:
     """Sniffs CSV dialect and header info
 
     :param filepath: Path of file to sniff
     :param sniff_size: Maximum no. bytes to use for sniffing
+    :param encoding: File encoding
     :return: csv.Dialect object with additional attribute `has_header`
 
     """
 
-    with open(filepath, newline='', encoding='utf-8') as csvfile:
+    with open(filepath, newline='', encoding=encoding) as csvfile:
         csv_str = csvfile.read(sniff_size)
 
     dialect = csv.Sniffer().sniff(csv_str)
     setattr(dialect, "hasheader", csv.Sniffer().has_header(csv_str))
+    setattr(dialect, "encoding", encoding)
 
     return dialect
 
 
 def get_header(csvfile: TextIO, dialect: csv.Dialect) -> List[str]:
-    """Returns list of first line items of file filepath"""
+    """Returns list of first line items of file filepath
+
+    :param csvfile: CSV file
+    :param dialect: Dialect of CSV file
+
+    """
 
     csvfile.seek(0)
     csvreader = csv.reader(csvfile, dialect=dialect)
@@ -115,6 +128,10 @@ def convert(string: str, digest_type: str) -> str:
     if digest_type is None:
         digest_type = 'repr'
 
+    if digest_type.split()[0] == "Money":
+        currency = digest_type.split()[1][1:-1]
+        return repr(typehandlers["Money"](string, currency=currency))
+
     try:
         return repr(typehandlers[digest_type](string))
 
@@ -158,14 +175,29 @@ def make_object(obj):
 typehandlers = {
     'object': ast.literal_eval,
     'repr': lambda x: x,
-    'bool': bool,
+    'str': str,
     'int': int,
     'float': float,
+    'decimal': Decimal,
     'complex': complex,
-    'str': str,
+    'bool': bool,
     'bytes': bytes,
 }
 
+
+if Money is not None:
+    def money(amount, currency="USD"):
+        """Money wrapper defining a standard currency"""
+
+        return Money(amount, currency=currency)
+
+    typehandlers.update({'Money': money})
+
+    currencies = list_all_currencies()
+else:
+    currencies = []
+
+
 if parse is not None:
     typehandlers.update({'date': date,
                          'datetime': datetime,
diff --git a/pyspread/lib/selection.py b/pyspread/lib/selection.py
index 4f2f1ce9c2e05bf6ea32d3d3643d35a6549d33de..eb817057b5b0686386982a2b1432fb30a069a85b 100644
--- a/pyspread/lib/selection.py
+++ b/pyspread/lib/selection.py
@@ -33,7 +33,7 @@ from builtins import zip, range, object
 from typing import Generator, List, Tuple
 
 
-class Selection(object):
+class Selection:
     """Represents grid selection"""
 
     def __init__(self,
@@ -71,7 +71,7 @@ class Selection(object):
 
         """
 
-        return "Selection{}".format(self.parameters)
+        return f"Selection{self.parameters}"
 
     def __eq__(self, other):
         """Eqality check
@@ -336,7 +336,7 @@ class Selection(object):
                 bb_left = left
             if bb_bottom is None or bb_bottom < bottom:
                 bb_bottom = bottom
-            if bb_right is None or bb_right > right:
+            if bb_right is None or bb_right < right:
                 bb_right = right
 
         # Row and column selections
@@ -410,51 +410,35 @@ class Selection(object):
 
         """
 
-        rows, columns, tables = shape
-
-        # Negative dimensions cannot be
-        if any(dim <= 0 for dim in shape):
-            raise Warning("Invalid shape {}".format(shape))
-            return
-
-        # Current table has to be in dimensions
-        if not 0 <= table < tables:
-            raise Warning("Table {} not in grid".format(table))
-            return
-
-        string_list = []
+        rows, columns, _ = shape
+        strings = []
 
         # Block selections
-        templ = "[(r, c, {}) for r in range({}, {}) for c in range({}, {})]"
         for (top, left), (bottom, right) in zip(self.block_tl, self.block_br):
-            string_list += [templ.format(table, top, bottom + 1,
-                                         left, right + 1)]
+            strings += [f"[(r, c, {table})"
+                        f" for r in range({top}, {bottom + 1})"
+                        f" for c in range({left}, {right + 1})]"]
 
         # Fully selected rows
-        template = "[({}, c, {}) for c in range({})]"
         for row in self.rows:
-            string_list += [template.format(row, table, columns)]
+            strings += [f"[({row}, c, {table}) for c in range({columns})]"]
 
         # Fully selected columns
-        template = "[(r, {}, {}) for r in range({})]"
         for column in self.columns:
-            string_list += [template.format(column, table, rows)]
+            strings += [f"[(r, {column}, {table}) for r in range({rows})]"]
 
         # Single cells
         for row, column in self.cells:
-            string_list += [repr([(row, column, table)])]
+            strings += [f"[({row}, {column}, {table})]"]
 
-        key_string = " + ".join(string_list)
-
-        if len(string_list) == 0:
+        if not strings:
             return ""
 
-        elif len(self.cells) == 1 and len(string_list) == 1:
-            return "S[{}]".format(string_list[0][1:-1])
+        if len(self.cells) == 1 and len(strings) == 1:
+            return f"S[{strings[0][2:-2]}]"
 
-        else:
-            template = "[S[key] for key in {} if S[key] is not None]"
-            return template.format(key_string)
+        key_string = " + ".join(strings)
+        return f"[S[key] for key in {key_string} if S[key] is not None]"
 
     def get_relative_access_string(self, shape: Tuple[int, int, int],
                                    current: Tuple[int, int, int]) -> str:
@@ -472,50 +456,37 @@ class Selection(object):
         rows, columns, tables = shape
         crow, ccolumn, ctable = current
 
-        # Negative dimensions cannot be
-        if any(dim <= 0 for dim in shape):
-            raise Warning("Invalid shape {}".format(shape))
-            return
-
-        if any(dim < 0 for dim in current):
-            raise Warning("Invalid cell {}".format(current))
-            return
-
-        string_list = []
+        strings = []
 
         # Block selections
-        tpl = "[(X + dr, Y + dc, Z) for dr in range({}, {})" + \
-              " for dc in range({}, {})]"
         for (top, left), (bottom, right) in zip(self.block_tl, self.block_br):
-            string_list += [tpl.format(top - crow, bottom - crow + 1,
-                                       left - ccolumn, right - ccolumn + 1)]
+            strings += [
+                "[(X + dr, Y + dc, Z)" +
+                f" for dr in range({top - crow}, {bottom - crow + 1})"
+                f" for dc in range({left - ccolumn}, {right - ccolumn + 1})]"]
 
         # Fully selected rows
-        template = "[(X + {}, c, Z) for c in range({})]"
         for row in self.rows:
-            string_list += [template.format(row - crow, columns)]
+            strings += [
+                f"[(X + {row - crow}, c, Z) for c in range({columns})]"]
 
         # Fully selected columns
-        template = "[(r, {}, Z) for r in range({})]"
         for column in self.columns:
-            string_list += [template.format(column - ccolumn, rows)]
+            strings += [f"[(r, {column - ccolumn}, Z) for r in range({rows})]"]
 
         # Single cells
         for row, column in self.cells:
-            cell_str = "[X + {}, Y + {}, Z]".format(row-crow, column-ccolumn)
-            string_list.append(cell_str)
+            strings += [f"[(X + {row-crow}, Y + {column-ccolumn}, Z)]"]
 
-        key_string = " + ".join(string_list)
+        key_string = " + ".join(strings)
 
-        if len(string_list) == 0:
+        if not strings:
             return ""
 
-        elif len(self.cells) == 1 and len(string_list) == 1:
-            return "S[{}]".format(string_list[0][1:-1])
+        if len(self.cells) == 1 and len(strings) == 1:
+            return f"S[{strings[0][2:-2]}]"
 
-        else:
-            template = "[S[key] for key in {} if S[key] is not None]"
-            return template.format(key_string)
+        return f"[S[key] for key in {key_string} if S[key] is not None]"
 
     def shifted(self, rows: int, columns: int):
         """Get a shifted selection
@@ -568,30 +539,24 @@ class Selection(object):
         if border_choice == "All borders":
             return Selection([(top, left-1)], [(bottom, right)], [], [], [])
 
-        elif border_choice == "Top border":
+        if border_choice in ("Top border", "Bottom border",
+                             "Top and bottom borders"):
             return Selection([], [], [], [], [])
 
-        elif border_choice == "Bottom border":
-            return Selection([], [], [], [], [])
-
-        elif border_choice == "Left border":
+        if border_choice == "Left border":
             return Selection([(top, left-1)], [(bottom, left-1)], [], [], [])
 
-        elif border_choice == "Right border":
+        if border_choice == "Right border":
             return Selection([(top, right)], [(bottom, right)], [], [], [])
 
-        elif border_choice == "Outer borders":
+        if border_choice == "Outer borders":
             return Selection([(top, right), (top, left-1)],
                              [(bottom, right), (bottom, left-1)], [], [], [])
 
-        elif border_choice == "Inner borders":
+        if border_choice == "Inner borders":
             return Selection([(top, left)], [(bottom, right-1)], [], [], [])
 
-        elif border_choice == "Top and bottom borders":
-            return Selection([], [], [], [], [])
-
-        else:
-            raise ValueError("border_choice {} unknown.".format(border_choice))
+        raise ValueError(f"border_choice {border_choice} unknown.")
 
     def get_bottom_borders_selection(self, border_choice: str,
                                      shape: Tuple[int, int, int]):
@@ -619,31 +584,27 @@ class Selection(object):
         if border_choice == "All borders":
             return Selection([(top-1, left)], [(bottom, right)], [], [], [])
 
-        elif border_choice == "Top border":
+        if border_choice == "Top border":
             return Selection([(top-1, left)], [(top-1, right)], [], [], [])
 
-        elif border_choice == "Bottom border":
+        if border_choice == "Bottom border":
             return Selection([(bottom, left)], [(bottom, right)], [], [], [])
 
-        elif border_choice == "Left border":
+        if border_choice in ("Left border", "Right border"):
             return Selection([], [], [], [], [])
 
-        elif border_choice == "Right border":
-            return Selection([], [], [], [], [])
-
-        elif border_choice == "Outer borders":
+        if border_choice == "Outer borders":
             return Selection([(top-1, left), (bottom, left)],
                              [(top-1, right), (bottom, right)], [], [], [])
 
-        elif border_choice == "Inner borders":
+        if border_choice == "Inner borders":
             return Selection([(top, left)], [(bottom-1, right)], [], [], [])
 
-        elif border_choice == "Top and bottom borders":
+        if border_choice == "Top and bottom borders":
             return Selection([(top-1, left), (bottom, left)],
                              [(top-1, right), (bottom, right)], [], [], [])
 
-        else:
-            raise ValueError("border_choice {} unknown.".format(border_choice))
+        raise ValueError(f"border_choice {border_choice} unknown.")
 
     def single_cell_selected(self) -> bool:
         """
diff --git a/pyspread/lib/spelltextedit.py b/pyspread/lib/spelltextedit.py
index 709ec8d518c8d189483a616d6f26b90a78b118c7..f3f15dbffb20a85a7df57c6438612cc2067f4039 100644
--- a/pyspread/lib/spelltextedit.py
+++ b/pyspread/lib/spelltextedit.py
@@ -118,7 +118,7 @@ except ImportError:  # Older versions of PyEnchant as on *buntu 14.04
 
 # pylint: disable=no-name-in-module
 from PyQt5.Qt import Qt
-from PyQt5.QtCore import QEvent, QRegExp, QSize, QRect
+from PyQt5.QtCore import QEvent, QRegExp, QSize, QRect, QRectF
 from PyQt5.QtGui import (QFocusEvent, QSyntaxHighlighter, QTextBlockUserData,
                          QTextCharFormat, QTextCursor, QColor, QFont,
                          QFontMetricsF, QPainter, QPalette)
@@ -197,8 +197,8 @@ class LineNumberArea(QWidget):
             if block.isVisible() and (bottom >= event.rect().top()):
                 number = str(block_number + 1)
                 painter.setPen(text_color)
-                painter.drawText(0, top, self.width(), height, Qt.AlignRight,
-                                 number)
+                text_rect = QRectF(0, top, self.width(), height)
+                painter.drawText(text_rect, Qt.AlignRight, number)
 
             block = block.next()
             top = bottom
diff --git a/pyspread/lib/string_helpers.py b/pyspread/lib/string_helpers.py
index 9998fbf1f57d7dd6d7eecd1aa5238ca5ea096a47..b7857adf767e1f7600020e09fee5173ccc9b852d 100644
--- a/pyspread/lib/string_helpers.py
+++ b/pyspread/lib/string_helpers.py
@@ -24,13 +24,10 @@
 
  * :func:`quote`
  * :func:`wrap_text`
- * :func:`get_svg_size`
 
 """
 
-import xml.etree.ElementTree as ET
 import textwrap
-from typing import Tuple
 
 
 def quote(code: str) -> str:
@@ -80,21 +77,3 @@ def wrap_text(text, width=80, maxlen=2000):
     if maxlen is not None and len(text) > maxlen:
         text = text[:maxlen] + "..."
     return "\n".join(textwrap.wrap(text, width=width))
-
-
-def get_svg_size(svg_bytes: bytes) -> Tuple[int, int]:
-    """Get SVG size
-
-    :param svg_bytes: SVG image data
-    :return: Width, height
-
-    """
-
-    tree = ET.fromstring(svg_bytes)
-    width_str = tree.get("width")
-    height_str = tree.get("height")
-    width = int(float(''.join(n for n in width_str
-                              if n.isdigit() or n == '.')))
-    height = int(float(''.join(n for n in height_str
-                               if n.isdigit() or n == '.')))
-    return width, height
diff --git a/pyspread/lib/test/test_csv.py b/pyspread/lib/test/test_csv.py
index fe13f72715b55d37099a2f7e1ce02105873ffe01..54eab58f096e5ca83a57d38c42402b3850466a5f 100644
--- a/pyspread/lib/test/test_csv.py
+++ b/pyspread/lib/test/test_csv.py
@@ -57,7 +57,7 @@ def test_sniff(filepath, hasheader, delimiter, doublequote, quoting, quotechar,
                lineterminator, skipinitialspace):
     """Unit test for sniff"""
 
-    dialect = sniff(filepath, 1024)
+    dialect = sniff(filepath, 1024, encoding='utf-8')
     assert dialect.hasheader == hasheader
     assert dialect.delimiter == delimiter
     assert dialect.doublequote == doublequote
@@ -76,7 +76,7 @@ param_get_header = [
 def test_get_header(filepath, header):
     """Unit test for get_first_line"""
 
-    dialect = sniff(filepath, 1024)
+    dialect = sniff(filepath, 1024, encoding='utf-8')
     with open(filepath) as csvfile:
         __header = get_header(csvfile, dialect)
 
@@ -87,7 +87,7 @@ def test_csv_reader():
     """Unit test for csv_reader"""
 
     filepath = TESTPATH / 'valid1.csv'
-    dialect = sniff(filepath, 1024)
+    dialect = sniff(filepath, 1024, encoding='utf-8')
     result = [["1", "2", "3"], ["4", "5", "6"]]
 
     with open(filepath) as csvfile:
diff --git a/pyspread/lib/test/test_selection.py b/pyspread/lib/test/test_selection.py
index 9e47dbec66eb6f06ef8e8c602fe97fc896c9fec6..504fc6a9b1beb01b312ed42c7103ca5ce77a61c3 100644
--- a/pyspread/lib/test/test_selection.py
+++ b/pyspread/lib/test/test_selection.py
@@ -233,6 +233,8 @@ class TestSelection:
         (Selection([], [], [], [], [(32, 53), (34, 56)]),
          ((32, 53), (34, 56))),
         (Selection([(4, 5)], [(100, 200)], [], [], []), ((4, 5), (100, 200))),
+        (Selection([(0, 2), (0, 8), (0, 4)], [(60, 2), (60, 10), (60, 4)],
+                   [], [], []), ((0, 2), (60, 10))),
         (Selection([], [], [2], [3], []), ((None, None), (None, None))),
         (Selection([], [], [], [3], []), ((None, 3), (None, 3))),
     ]
diff --git a/pyspread/lib/test/test_string_helpers.py b/pyspread/lib/test/test_string_helpers.py
index 064ca21f139a1a3512da65e6cfa09e0d17397e41..7695e8f8d5f10ec925cbb2a2b5e8f706ec809259 100644
--- a/pyspread/lib/test/test_string_helpers.py
+++ b/pyspread/lib/test/test_string_helpers.py
@@ -26,7 +26,7 @@
 """
 
 import pytest
-from ..string_helpers import quote, wrap_text, get_svg_size
+from ..string_helpers import quote, wrap_text
 
 
 param_test_quote = [
@@ -67,78 +67,3 @@ def test_wrap_text(text, width, maxlen, res):
     """Unit test for wrap_text"""
 
     assert wrap_text(text, width, maxlen) == res
-
-
-SVG_1 = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="218.36047mm"
-   height="218.36047mm"
-   viewBox="0 0 218.36048 218.36048"
-   version="1.1"
-   id="svg8"
-   inkscape:version="0.92.3 (2405546, 2018-03-11)"
-   sodipodi:docname="format-borders-1.svg">
-  <defs
-     id="defs2" />
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="0.49497476"
-     inkscape:cx="-127.83766"
-     inkscape:cy="370.91478"
-     inkscape:document-units="mm"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     inkscape:window-width="1280"
-     inkscape:window-height="970"
-     inkscape:window-x="0"
-     inkscape:window-y="0"
-     inkscape:window-maximized="1"
-     fit-margin-top="0"
-     fit-margin-left="0"
-     fit-margin-right="0"
-     fit-margin-bottom="0" />
-  <metadata
-     id="metadata5">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title />
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(12.75978,-2.0734556)">
-    <path
-       style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       d="m 27.703878,111.238 137.401542,2e-5"
-       id="path846-3-0"
-       inkscape:connector-curvature="0" />
-  </g>
-</svg>
-"""
-
-
-def test_get_svg_size():
-    """Unit test for get_svg_size"""
-
-    assert get_svg_size(SVG_1) == (218, 218)
diff --git a/pyspread/main_window.py b/pyspread/main_window.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e51ba125fee008c3704cf720f8197ad2e7912a9
--- /dev/null
+++ b/pyspread/main_window.py
@@ -0,0 +1,786 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+# Copyright Martin Manns
+# Distributed under the terms of the GNU General Public License
+
+# --------------------------------------------------------------------
+# pyspread is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# pyspread is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pyspread.  If not, see <http://www.gnu.org/licenses/>.
+# --------------------------------------------------------------------
+
+"""
+
+pyspread
+========
+
+- Main Python spreadsheet application
+- Run this script to start the application.
+
+**Provides**
+
+* MainApplication: Initial command line operations and application launch
+* :class:`MainWindow`: Main windows class
+
+"""
+
+import os
+from pathlib import Path
+
+from PyQt5.QtCore import Qt, pyqtSignal, QEvent, QTimer, QRectF
+from PyQt5.QtWidgets import (QWidget, QMainWindow, QApplication,
+                             QMessageBox, QDockWidget, QUndoStack, QVBoxLayout,
+                             QStyleOptionViewItem, QSplitter)
+try:
+    from PyQt5.QtSvg import QSvgWidget
+except ImportError:
+    QSvgWidget = None
+from PyQt5.QtGui import QColor, QFont, QPalette, QPainter
+from PyQt5.QtPrintSupport import QPrinter, QPrintDialog
+
+try:
+    from pyspread.__init__ import VERSION, APP_NAME
+    from pyspread.settings import Settings, WEB_URL
+    from pyspread.icons import Icon, IconPath
+    from pyspread.grid import Grid, TableChoice
+    from pyspread.grid_renderer import painter_save
+    from pyspread.entryline import Entryline
+    from pyspread.menus import MenuBar
+    from pyspread.toolbar import (MainToolBar, FindToolbar, FormatToolbar,
+                                  MacroToolbar)
+    from pyspread.actions import MainWindowActions
+    from pyspread.workflows import Workflows
+    from pyspread.widgets import Widgets
+    from pyspread.dialogs import (ApproveWarningDialog, PreferencesDialog,
+                                  ManualDialog, TutorialDialog,
+                                  PrintAreaDialog, PrintPreviewDialog)
+    from pyspread.installer import DependenciesDialog
+    from pyspread.panels import MacroPanel
+    from pyspread.lib.hashing import genkey
+    from pyspread.model.model import CellAttributes
+except ImportError:
+    from __init__ import VERSION, APP_NAME
+    from settings import Settings, WEB_URL
+    from icons import Icon, IconPath
+    from grid import Grid, TableChoice
+    from grid_renderer import painter_save
+    from entryline import Entryline
+    from menus import MenuBar
+    from toolbar import MainToolBar, FindToolbar, FormatToolbar, MacroToolbar
+    from actions import MainWindowActions
+    from workflows import Workflows
+    from widgets import Widgets
+    from dialogs import (ApproveWarningDialog, PreferencesDialog, ManualDialog,
+                         TutorialDialog, PrintAreaDialog, PrintPreviewDialog)
+    from installer import DependenciesDialog
+    from panels import MacroPanel
+    from lib.hashing import genkey
+    from model.model import CellAttributes
+
+
+LICENSE = "GNU GENERAL PUBLIC LICENSE Version 3"
+
+os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
+QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
+QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
+
+
+class MainWindow(QMainWindow):
+    """Pyspread main window"""
+
+    gui_update = pyqtSignal(dict)
+
+    def __init__(self, filepath: Path = Path(),
+                 default_settings: bool = False):
+        """
+        :param filepath: File path for inital file to be opened
+        :param default_settings: Ignore stored `QSettings` and use defaults
+
+        """
+
+        super().__init__()
+
+        self._loading = True  # For initial loading of pyspread
+        self.prevent_updates = False  # Prevents setData updates in grid
+
+        self.settings = Settings(self, reset_settings=default_settings)
+        self.workflows = Workflows(self)
+        self.undo_stack = QUndoStack(self)
+        self.refresh_timer = QTimer()
+
+        self._init_widgets()
+
+        self.main_window_actions = MainWindowActions(self)
+        self.main_window_toolbar_actions = MainWindowActions(self,
+                                                             shortcuts=False)
+
+        self._init_window()
+        self._init_toolbars()
+
+        self.settings.restore()
+        if self.settings.signature_key is None:
+            self.settings.signature_key = genkey()
+
+        # Print area for print requests
+        self.print_area = None
+
+        # Update recent files in the file menu
+        self.menuBar().file_menu.history_submenu.update()
+
+        # Update toolbar toggle checkboxes
+        self.update_action_toggles()
+
+        # Update the GUI so that everything matches the model
+        cell_attributes = self.grid.model.code_array.cell_attributes
+        attributes = cell_attributes[self.grid.current]
+        self.on_gui_update(attributes)
+
+        self._last_focused_grid = self.grid
+
+        self._loading = False
+        self._previous_window_state = self.windowState()
+
+        # Open initial file if provided by the command line
+        if filepath is not None:
+            if self.workflows.filepath_open(filepath):
+                self.workflows.update_main_window_title()
+            else:
+                msg = f"File '{filepath}' could not be opened."
+                self.statusBar().showMessage(msg)
+
+    def _init_window(self):
+        """Initialize main window components"""
+
+        self.setWindowTitle(APP_NAME)
+        self.setWindowIcon(Icon.pyspread)
+
+        # Safe mode widget
+        self.safe_mode_widget = QSvgWidget(str(IconPath.safe_mode),
+                                           self.statusBar())
+        msg = f"{APP_NAME} is in safe mode.\nExpressions are not evaluated."
+        self.safe_mode_widget.setToolTip(msg)
+        self.statusBar().addPermanentWidget(self.safe_mode_widget)
+        self.safe_mode_widget.hide()
+
+        # Selection mode widget
+        self.selection_mode_widget = QSvgWidget(str(IconPath.selection_mode),
+                                                self.statusBar())
+        msg = "Selection mode active. Cells cannot be edited.\n" + \
+              "Selecting cells adds relative references into the entry " + \
+              "line. Additionally pressing `Meta` switches to absolute " + \
+              "references.\nEnd selection mode by clicking into the entry " + \
+              "line or with `Esc` when focusing the grid."
+        self.selection_mode_widget.setToolTip(msg)
+        self.statusBar().addPermanentWidget(self.selection_mode_widget)
+        self.selection_mode_widget.hide()
+
+        # Disable the approve fiel menu button
+        self.main_window_actions.approve.setEnabled(False)
+
+        self.setMenuBar(MenuBar(self))
+
+    def resizeEvent(self, event: QEvent):
+        """Overloaded, aborts on self._loading
+
+        :param event: Resize event
+
+        """
+
+        if self._loading:
+            return
+
+        super().resizeEvent(event)
+
+    def closeEvent(self, event: QEvent = None):
+        """Overloaded, allows saving changes or canceling close
+
+        :param event: Any QEvent
+
+        """
+
+        if event:
+            event.ignore()
+        self.workflows.file_quit()  # has @handle_changed_since_save decorator
+
+    def _init_widgets(self):
+        """Initialize widgets"""
+
+        self.widgets = Widgets(self)
+
+        self.entry_line = Entryline(self)
+
+        self.vsplitter = QSplitter(Qt.Vertical, self)
+        self.hsplitter_1 = QSplitter(Qt.Horizontal, self)
+        self.hsplitter_2 = QSplitter(Qt.Horizontal, self)
+
+        # Set up the table choice first
+        _no_tables = self.settings.shape[2]
+        self.table_choice = TableChoice(self, _no_tables)
+
+        # We have one main view that is used as default view
+        self.grid = Grid(self)
+        # Further views of the grid
+        self.grid_2 = Grid(self, self.grid.model)
+        self.grid_3 = Grid(self, self.grid.model)
+        self.grid_4 = Grid(self, self.grid.model)
+
+        self.grids = [self.grid, self.grid_2, self.grid_3, self.grid_4]
+
+        self.macro_panel = MacroPanel(self, self.grid.model.code_array)
+
+        self.main_panel = QWidget(self)
+
+        self.entry_line_dock = QDockWidget("Entry Line", self)
+        self.entry_line_dock.setObjectName("Entry Line Panel")
+        self.entry_line_dock.setWidget(self.entry_line)
+        self.addDockWidget(Qt.TopDockWidgetArea, self.entry_line_dock)
+        self.resizeDocks([self.entry_line_dock], [10], Qt.Horizontal)
+
+        self.macro_dock = QDockWidget("Macros", self)
+        self.macro_dock.setObjectName("Macro Panel")
+        self.macro_dock.setWidget(self.macro_panel)
+        self.addDockWidget(Qt.RightDockWidgetArea, self.macro_dock)
+
+        self.central_layout = QVBoxLayout(self.main_panel)
+        self._layout()
+
+        self.entry_line_dock.installEventFilter(self)
+        self.macro_dock.installEventFilter(self)
+
+        QApplication.instance().focusChanged.connect(self.on_focus_changed)
+        self.gui_update.connect(self.on_gui_update)
+        self.refresh_timer.timeout.connect(self.on_refresh_timer)
+
+        # Connect widgets only to first grid
+        self.widgets.text_color_button.colorChanged.connect(
+            self.grid.on_text_color)
+        self.widgets.background_color_button.colorChanged.connect(
+            self.grid.on_background_color)
+        self.widgets.line_color_button.colorChanged.connect(
+            self.grid.on_line_color)
+        self.widgets.font_combo.fontChanged.connect(self.grid.on_font)
+        self.widgets.font_size_combo.fontSizeChanged.connect(
+            self.grid.on_font_size)
+
+    def _layout(self):
+        """Layouts for main window"""
+
+        self.central_layout.addWidget(self.vsplitter)
+        self.central_layout.addWidget(self.grid.table_choice)
+
+        self.vsplitter.addWidget(self.hsplitter_1)
+        self.vsplitter.addWidget(self.hsplitter_2)
+
+        self.hsplitter_1.addWidget(self.grid)
+        self.hsplitter_1.addWidget(self.grid_2)
+        self.hsplitter_2.addWidget(self.grid_3)
+        self.hsplitter_2.addWidget(self.grid_4)
+
+        self.vsplitter.setSizes([1, 0])
+        self.hsplitter_1.setSizes([1, 0])
+        self.hsplitter_2.setSizes([1, 0])
+
+        self.main_panel.setLayout(self.central_layout)
+        self.setCentralWidget(self.main_panel)
+
+    def eventFilter(self, source: QWidget, event: QEvent) -> bool:
+        """Overloaded event filter for handling QDockWidget close events
+
+        Updates the menu if the macro panel is closed.
+
+        :param source: Source widget of event
+        :param event: Any QEvent
+
+        """
+
+        if event.type() == QEvent.Close and isinstance(source, QDockWidget):
+            if source.windowTitle() == "Macros":
+                self.main_window_actions.toggle_macro_dock.setChecked(False)
+            elif source.windowTitle() == "Entry Line":
+                self.main_window_actions.toggle_entry_line_dock.setChecked(
+                    False)
+
+        return super().eventFilter(source, event)
+
+    def _init_toolbars(self):
+        """Initialize the main window toolbars"""
+
+        self.main_toolbar = MainToolBar(self)
+        self.macro_toolbar = MacroToolbar(self)
+        self.find_toolbar = FindToolbar(self)
+        self.format_toolbar = FormatToolbar(self)
+
+        self.addToolBar(self.main_toolbar)
+        self.addToolBar(self.macro_toolbar)
+        self.addToolBar(self.find_toolbar)
+        self.addToolBarBreak()
+        self.addToolBar(self.format_toolbar)
+
+    def update_action_toggles(self):
+        """Updates the toggle menu check states"""
+
+        actions = self.main_window_actions
+
+        maintoolbar_visible = self.main_toolbar.isVisibleTo(self)
+        actions.toggle_main_toolbar.setChecked(maintoolbar_visible)
+
+        macrotoolbar_visible = self.macro_toolbar.isVisibleTo(self)
+        actions.toggle_macro_toolbar.setChecked(macrotoolbar_visible)
+
+        formattoolbar_visible = self.format_toolbar.isVisibleTo(self)
+        actions.toggle_format_toolbar.setChecked(formattoolbar_visible)
+
+        findtoolbar_visible = self.find_toolbar.isVisibleTo(self)
+        actions.toggle_find_toolbar.setChecked(findtoolbar_visible)
+
+        entryline_visible = self.entry_line_dock.isVisibleTo(self)
+        actions.toggle_entry_line_dock.setChecked(entryline_visible)
+
+        macrodock_visible = self.macro_dock.isVisibleTo(self)
+        actions.toggle_macro_dock.setChecked(macrodock_visible)
+
+    @property
+    def focused_grid(self):
+        """Returns grid with focus or self if none has focus"""
+
+        try:
+            return self._last_focused_grid
+        except AttributeError:
+            return self.grid
+
+    @property
+    def safe_mode(self) -> bool:
+        """Returns safe_mode state. In safe_mode cells are not evaluated."""
+
+        return self.grid.model.code_array.safe_mode
+
+    @safe_mode.setter
+    def safe_mode(self, value: bool):
+        """Sets safe mode.
+
+        This triggers the safe_mode icon in the statusbar.
+
+        If safe_mode changes from True to False then caches are cleared and
+        macros are executed.
+
+        :param value: Safe mode
+
+        """
+
+        if self.grid.model.code_array.safe_mode == bool(value):
+            return
+
+        self.grid.model.code_array.safe_mode = bool(value)
+
+        if value:  # Safe mode entered
+            self.safe_mode_widget.show()
+            # Enable approval menu entry
+            self.main_window_actions.approve.setEnabled(True)
+        else:  # Safe_mode disabled
+            self.safe_mode_widget.hide()
+            # Disable approval menu entry
+            self.main_window_actions.approve.setEnabled(False)
+            # Clear result cache
+            self.grid.model.code_array.result_cache.clear()
+            # Execute macros
+            self.macro_panel.on_apply()
+
+    def on_print(self):
+        """Print event handler"""
+
+        # Create printer
+        printer = QPrinter(mode=QPrinter.HighResolution)
+
+        # Get print area
+        self.print_area = PrintAreaDialog(self, self.grid,
+                                          title="Print area").area
+        if self.print_area is None:
+            return
+
+        # Create print dialog
+        dialog = QPrintDialog(printer, self)
+        if dialog.exec_() == QPrintDialog.Accepted:
+            self.on_paint_request(printer)
+
+    def on_preview(self):
+        """Print preview event handler"""
+
+        # Create printer
+        printer = QPrinter(mode=QPrinter.HighResolution)
+
+        # Get print area
+        self.print_area = PrintAreaDialog(self, self.grid,
+                                          title="Print area").area
+        if self.print_area is None:
+            return
+
+        # Create print preview dialog
+        dialog = PrintPreviewDialog(printer)
+
+        dialog.paintRequested.connect(self.on_paint_request)
+        dialog.exec_()
+
+    def on_paint_request(self, printer: QPrinter):
+        """Paints to printer
+
+        :param printer: Target printer
+
+        """
+
+        painter = QPainter(printer)
+        option = QStyleOptionViewItem()
+        painter.setRenderHints(QPainter.SmoothPixmapTransform
+                               | QPainter.SmoothPixmapTransform)
+
+        page_rect = printer.pageRect()
+
+        rows = list(self.workflows.get_paint_rows(self.print_area.top,
+                                                  self.print_area.bottom))
+        columns = list(self.workflows.get_paint_columns(self.print_area.left,
+                                                        self.print_area.right))
+        tables = list(self.workflows.get_paint_tables(self.print_area.first,
+                                                      self.print_area.last))
+        if not all((rows, columns, tables)):
+            return
+
+        old_table = self.grid.table
+
+        for i, table in enumerate(tables):
+            self.grid.table = table
+
+            zeroidx = self.grid.model.index(0, 0)
+            zeroidx_rect = self.grid.visualRect(zeroidx)
+
+            minidx = self.grid.model.index(min(rows), min(columns))
+            minidx_rect = self.grid.visualRect(minidx)
+
+            maxidx = self.grid.model.index(max(rows), max(columns))
+            maxidx_rect = self.grid.visualRect(maxidx)
+
+            grid_width = maxidx_rect.x() + maxidx_rect.width() \
+                - minidx_rect.x()
+            grid_height = maxidx_rect.y() + maxidx_rect.height() \
+                - minidx_rect.y()
+            grid_rect = QRectF(minidx_rect.x() - zeroidx_rect.x(),
+                               minidx_rect.y() - zeroidx_rect.y(),
+                               grid_width, grid_height)
+
+            self.settings.print_zoom = min(page_rect.width() / grid_width,
+                                           page_rect.height() / grid_height)
+
+            with painter_save(painter):
+                painter.scale(self.settings.print_zoom,
+                              self.settings.print_zoom)
+
+                # Translate so that the grid starts at upper left paper edge
+                painter.translate(zeroidx_rect.x() - minidx_rect.x(),
+                                  zeroidx_rect.y() - minidx_rect.y())
+
+                # Draw grid cells
+                self.workflows.paint(painter, option, grid_rect, rows, columns)
+
+            self.settings.print_zoom = None
+
+            if i != len(tables) - 1:
+                printer.newPage()
+
+        self.grid.table = old_table
+
+    def on_fullscreen(self):
+        """Fullscreen toggle event handler"""
+
+        if self.windowState() == Qt.WindowFullScreen:
+            self.setWindowState(self._previous_window_state)
+        else:
+            self._previous_window_state = self.windowState()
+            self.setWindowState(Qt.WindowFullScreen)
+
+    def on_approve(self):
+        """Approve event handler"""
+
+        if ApproveWarningDialog(self).choice:
+            self.safe_mode = False
+
+    def on_clear_globals(self):
+        """Clear globals event handler"""
+
+        self.grid.model.code_array.result_cache.clear()
+
+        # Clear globals
+        self.grid.model.code_array.clear_globals()
+        self.grid.model.code_array.reload_modules()
+
+    def on_preferences(self):
+        """Preferences event handler (:class:`dialogs.PreferencesDialog`) """
+
+        data = PreferencesDialog(self).data
+
+        if data is not None:
+            max_file_history_changed = \
+                self.settings.max_file_history != data['max_file_history']
+
+            # Dialog has been approved --> Store data to settings
+            for key in data:
+                if key == "signature_key" and not data[key]:
+                    data[key] = genkey()
+                self.settings.__setattr__(key, data[key])
+
+            # Immediately adjust file history in menu
+            if max_file_history_changed:
+                self.menuBar().file_menu.history_submenu.update()
+
+    def on_dependencies(self):
+        """Dependancies installer (:class:`installer.InstallerDialog`) """
+
+        dial = DependenciesDialog(self)
+        dial.exec_()
+
+    def on_undo(self):
+        """Undo event handler"""
+
+        self.undo_stack.undo()
+
+    def on_redo(self):
+        """Undo event handler"""
+
+        self.undo_stack.redo()
+
+    def on_toggle_refresh_timer(self, toggled: bool):
+        """Toggles periodic timer for frozen cells
+
+        :param toggled: Toggle state
+
+        """
+
+        if toggled:
+            self.refresh_timer.start(self.settings.refresh_timeout)
+        else:
+            self.refresh_timer.stop()
+
+    def on_refresh_timer(self):
+        """Event handler for self.refresh_timer.timeout
+
+        Called for periodic updates of frozen cells.
+        Does nothing if either the entry_line or a cell editor is active.
+
+        """
+
+        if not self.entry_line.hasFocus() \
+           and self.grid.state() != self.grid.EditingState:
+            self.grid.refresh_frozen_cells()
+
+    def _toggle_widget(self, widget: QWidget, action_name: str, toggled: bool):
+        """Toggles widget visibility and updates toggle actions
+
+        :param widget: Widget to be toggled shown or hidden
+        :param action_name: Name of action from Action class
+        :param toggled: Toggle state
+
+        """
+
+        if toggled:
+            widget.show()
+        else:
+            widget.hide()
+
+        self.main_window_actions[action_name].setChecked(widget.isVisible())
+
+    def on_toggle_main_toolbar(self, toggled: bool):
+        """Main toolbar toggle event handler
+
+        :param toggled: Toggle state
+
+        """
+
+        self._toggle_widget(self.main_toolbar, "toggle_main_toolbar", toggled)
+
+    def on_toggle_macro_toolbar(self, toggled: bool):
+        """Macro toolbar toggle event handler
+
+        :param toggled: Toggle state
+
+        """
+
+        self._toggle_widget(self.macro_toolbar, "toggle_macro_toolbar",
+                            toggled)
+
+    def on_toggle_format_toolbar(self, toggled: bool):
+        """Format toolbar toggle event handler
+
+        :param toggled: Toggle state
+
+        """
+
+        self._toggle_widget(self.format_toolbar, "toggle_format_toolbar",
+                            toggled)
+
+    def on_toggle_find_toolbar(self, toggled: bool):
+        """Find toolbar toggle event handler
+
+        :param toggled: Toggle state
+
+        """
+
+        self._toggle_widget(self.find_toolbar, "toggle_find_toolbar", toggled)
+
+    def on_toggle_entry_line_dock(self, toggled: bool):
+        """Entryline toggle event handler
+
+        :param toggled: Toggle state
+
+        """
+
+        self._toggle_widget(self.entry_line_dock, "toggle_entry_line_dock",
+                            toggled)
+
+    def on_toggle_macro_dock(self, toggled: bool):
+        """Macro panel toggle event handler
+
+        :param toggled: Toggle state
+
+        """
+
+        self._toggle_widget(self.macro_dock, "toggle_macro_dock", toggled)
+
+    def on_manual(self):
+        """Show manual browser"""
+
+        dialog = ManualDialog(self)
+        dialog.show()
+
+    def on_tutorial(self):
+        """Show tutorial browser"""
+
+        dialog = TutorialDialog(self)
+        dialog.show()
+
+    def on_about(self):
+        """Show about message box"""
+
+        def devs_string(devs: list) -> str:
+            """Get string from devs list"""
+
+            devs_str = "".join(f"<li>{dev}</li>" for dev in devs)
+            return f"<ul>{devs_str}</ul>"
+
+        devs = ("Martin Manns", "Jason Sexauer", "Vova Kolobok", "mgunyho",
+                "Pete Morgan")
+        devs_str = devs_string(devs)
+
+        doc_devs = ("Martin Manns", "Bosko Markovic", "Pete Morgan")
+        doc_devs_str = devs_string(doc_devs)
+
+        copyright_owner = "Martin Manns"
+
+        about_msg = \
+            f"""<b>{APP_NAME}</b><><p>
+            A non-traditional Python spreadsheet application<p>
+            Version:&emsp;{VERSION}<p>
+            Created by:&emsp;{devs_str}<p>
+            Documented by:&emsp;{doc_devs_str}<p>
+            Copyright:&emsp;{copyright_owner}<p>
+            License:&emsp;{LICENSE}<p>
+            Web site:&emsp;<a href="{WEB_URL}">{WEB_URL}</a>
+            """
+
+        QMessageBox.about(self, f"About {APP_NAME}", about_msg)
+
+    def on_focus_changed(self, old: QWidget, now: QWidget):
+        """Handles grid clicks from entry line"""
+
+        if old == self.grid and now == self.entry_line:
+            self.grid.selection_mode = False
+
+    def on_gui_update(self, attributes: CellAttributes):
+        """GUI update that shall be called on each cell change
+
+        :param attributes: Attributes of current cell
+
+        """
+
+        widgets = self.widgets
+        menubar = self.menuBar()
+
+        is_bold = attributes.fontweight == QFont.Bold
+        self.main_window_actions.bold.setChecked(is_bold)
+
+        is_italic = attributes.fontstyle == QFont.StyleItalic
+        self.main_window_actions.italics.setChecked(is_italic)
+
+        underline_action = self.main_window_actions.underline
+        underline_action.setChecked(attributes.underline)
+
+        strikethrough_action = self.main_window_actions.strikethrough
+        strikethrough_action.setChecked(attributes.strikethrough)
+
+        renderer = attributes.renderer
+        widgets.renderer_button.set_current_action(renderer)
+        widgets.renderer_button.set_menu_checked(renderer)
+
+        freeze_action = self.main_window_actions.freeze_cell
+        freeze_action.setChecked(attributes.frozen)
+
+        lock_action = self.main_window_actions.lock_cell
+        lock_action.setChecked(attributes.locked)
+        self.entry_line.setReadOnly(attributes.locked)
+
+        button_action = self.main_window_actions.button_cell
+        button_action.setChecked(attributes.button_cell is not False)
+
+        rotation = f"rotate_{int(attributes.angle)}"
+        widgets.rotate_button.set_current_action(rotation)
+        widgets.rotate_button.set_menu_checked(rotation)
+        widgets.justify_button.set_current_action(attributes.justification)
+        widgets.justify_button.set_menu_checked(attributes.justification)
+        widgets.align_button.set_current_action(attributes.vertical_align)
+        widgets.align_button.set_menu_checked(attributes.vertical_align)
+
+        border_action = self.main_window_actions.border_group.checkedAction()
+        if border_action is not None:
+            icon = border_action.icon()
+            menubar.format_menu.border_submenu.setIcon(icon)
+            self.format_toolbar.border_menu_button.setIcon(icon)
+
+        border_width_action = \
+            self.main_window_actions.border_width_group.checkedAction()
+        if border_width_action is not None:
+            icon = border_width_action.icon()
+            menubar.format_menu.line_width_submenu.setIcon(icon)
+            self.format_toolbar.line_width_button.setIcon(icon)
+
+        if attributes.textcolor is None:
+            text_color = self.grid.palette().color(QPalette.Text)
+        else:
+            text_color = QColor(*attributes.textcolor)
+        widgets.text_color_button.color = text_color
+
+        if attributes.bordercolor_bottom is None:
+            line_color = self.grid.palette().color(QPalette.Mid)
+        else:
+            line_color = QColor(*attributes.bordercolor_bottom)
+        widgets.line_color_button.color = line_color
+
+        if attributes.bgcolor is None:
+            bgcolor = self.grid.palette().color(QPalette.Base)
+        else:
+            bgcolor = QColor(*attributes.bgcolor)
+        widgets.background_color_button.color = bgcolor
+
+        if attributes.textfont is None:
+            widgets.font_combo.font = QFont().family()
+        else:
+            widgets.font_combo.font = attributes.textfont
+        widgets.font_size_combo.size = attributes.pointsize
+
+        merge_cells_action = self.main_window_actions.merge_cells
+        merge_cells_action.setChecked(attributes.merge_area is not None)
diff --git a/pyspread/menus.py b/pyspread/menus.py
index 236378d74e7705c58a1133ede4b0a75e650a65f4..d9b700355ba201cf3cc71d72ade2f92823e1da99 100644
--- a/pyspread/menus.py
+++ b/pyspread/menus.py
@@ -144,6 +144,9 @@ class EditMenu(QMenu):
         self.addAction(actions.find)
         self.addAction(actions.replace)
         self.addSeparator()
+        self.addAction(actions.sort_ascending)
+        self.addAction(actions.sort_descending)
+        self.addSeparator()
         self.addAction(actions.toggle_selection_mode)
         self.addSeparator()
         self.addAction(actions.quote)
@@ -282,7 +285,8 @@ class MacroMenu(QMenu):
         self.addAction(actions.insert_image)
         if matplotlib_figure is not None:
             self.addAction(actions.insert_chart)
-
+        self.addSeparator()
+        self.addAction(actions.insert_sum)
 
 class HelpMenu(QMenu):
     """Help menu for the main menubar"""
diff --git a/pyspread/model/model.py b/pyspread/model/model.py
index 38d11e437a910bab92dc8992a249882c653e1bb6..952b176677777ae03328c798240ad7bc1235b452 100644
--- a/pyspread/model/model.py
+++ b/pyspread/model/model.py
@@ -53,6 +53,8 @@ import bz2
 from collections import defaultdict
 from copy import copy
 import datetime
+import decimal
+from decimal import Decimal  # Needed
 from importlib import reload
 from inspect import isgenerator
 import io
@@ -65,12 +67,19 @@ from typing import (
         Any, Dict, Iterable, List, NamedTuple, Sequence, Tuple, Union)
 
 import numpy
-from PyQt5.QtGui import QImage, QPixmap
+
+from PyQt5.QtGui import QImage, QPixmap  # Needed
+
 try:
     from matplotlib.figure import Figure
 except ImportError:
     Figure = None
 
+try:
+    from moneyed import Money
+except ImportError:
+    Money = None
+
 try:
     from pyspread.settings import Settings
     from pyspread.lib.attrdict import AttrDict
@@ -81,7 +90,7 @@ try:
 except ImportError:
     from settings import Settings
     from lib.attrdict import AttrDict
-    import lib.charts as charts
+    import lib.charts as charts  # Needed
     from lib.exception_handling import get_user_codeframe
     from lib.typechecks import is_stringlike
     from lib.selection import Selection
@@ -694,7 +703,7 @@ class DataArray:
                 merging_cell = \
                     self.cell_attributes.get_merging_cell(single_key)
                 if ((merging_cell is None or merging_cell == single_key) and
-                    isinstance(value, str)):
+                        isinstance(value, str)):
                     self.dict_grid[single_key] = value
             else:
                 # Value is empty --> delete cell
@@ -1436,7 +1445,7 @@ class CodeArray(DataArray):
     def reload_modules(self):
         """Reloads modules that are available in cells"""
 
-        modules = [bz2, base64, re, ast, sys, numpy, datetime]
+        modules = [bz2, base64, re, ast, sys, datetime, decimal]
 
         for module in modules:
             reload(module)
@@ -1452,9 +1461,17 @@ class CodeArray(DataArray):
                      'DefaultCellAttributeDict', 'ast', '__builtins__',
                      '__file__', 'sys', '__name__', 'QImage', 'defaultdict',
                      'copy', 'imap', 'ifilter', 'Selection', 'DictGrid',
-                     'numpy', 'CodeArray', 'DataArray', 'datetime', 'signal',
-                     'Any', 'Dict', 'Iterable', 'List', 'NamedTuple',
-                     'Sequence', 'Tuple', 'Union']
+                     'numpy', 'CodeArray', 'DataArray', 'datetime', 'Decimal',
+                     'decimal', 'signal', 'Any', 'Dict', 'Iterable', 'List',
+                     'NamedTuple', 'Sequence', 'Tuple', 'Union']
+
+        try:
+            from moneyed import Money
+        except ImportError:
+            Money = None
+
+        if Money is not None:
+            base_keys.append('Money')
 
         for key in list(globals().keys()):
             if key not in base_keys:
@@ -1484,6 +1501,11 @@ class CodeArray(DataArray):
 
         # Set up environment for evaluation
         globals().update(self._get_updated_environment())
+        for var in "XYZRCT":
+            try:
+                del globals()[var]
+            except KeyError:
+                pass
 
         # Create file-like string to capture output
         code_out = io.StringIO()
diff --git a/pyspread/pyspread.py b/pyspread/pyspread.py
index ecb1b2ce6c6bf338a6afa36070687ca476894658..471b77f49d387c1589a2af2a43770d340cc1efb3 100755
--- a/pyspread/pyspread.py
+++ b/pyspread/pyspread.py
@@ -38,57 +38,16 @@ import os
 import sys
 import traceback
 
-from PyQt5.QtCore import Qt, pyqtSignal, QEvent, QTimer, QRectF
-from PyQt5.QtWidgets import (QWidget, QMainWindow, QApplication,
-                             QMessageBox, QDockWidget, QUndoStack, QVBoxLayout,
-                             QStyleOptionViewItem, QSplitter)
-try:
-    from PyQt5.QtSvg import QSvgWidget
-except ImportError:
-    QSvgWidget = None
-from PyQt5.QtGui import QColor, QFont, QPalette, QPainter
-from PyQt5.QtPrintSupport import QPrinter, QPrintDialog
+from PyQt5.QtCore import Qt
+from PyQt5.QtWidgets import QApplication
 
 try:
-    from pyspread.__init__ import VERSION, APP_NAME
     from pyspread.cli import PyspreadArgumentParser
-    from pyspread.settings import Settings, WEB_URL
-    from pyspread.icons import Icon, IconPath
-    from pyspread.grid import Grid, TableChoice
-    from pyspread.grid_renderer import painter_save
-    from pyspread.entryline import Entryline
-    from pyspread.menus import MenuBar
-    from pyspread.toolbar import (MainToolBar, FindToolbar, FormatToolbar,
-                                  MacroToolbar)
-    from pyspread.actions import MainWindowActions
-    from pyspread.workflows import Workflows
-    from pyspread.widgets import Widgets
-    from pyspread.dialogs import (ApproveWarningDialog, PreferencesDialog,
-                                  ManualDialog, TutorialDialog,
-                                  PrintAreaDialog, PrintPreviewDialog)
-    from pyspread.installer import DependenciesDialog
-    from pyspread.panels import MacroPanel
-    from pyspread.lib.hashing import genkey
-    from pyspread.model.model import CellAttributes
+    from pyspread.main_window import MainWindow
+
 except ImportError:
-    from __init__ import VERSION, APP_NAME
     from cli import PyspreadArgumentParser
-    from settings import Settings, WEB_URL
-    from icons import Icon, IconPath
-    from grid import Grid, TableChoice
-    from grid_renderer import painter_save
-    from entryline import Entryline
-    from menus import MenuBar
-    from toolbar import MainToolBar, FindToolbar, FormatToolbar, MacroToolbar
-    from actions import MainWindowActions
-    from workflows import Workflows
-    from widgets import Widgets
-    from dialogs import (ApproveWarningDialog, PreferencesDialog, ManualDialog,
-                         TutorialDialog, PrintAreaDialog, PrintPreviewDialog)
-    from installer import DependenciesDialog
-    from panels import MacroPanel
-    from lib.hashing import genkey
-    from model.model import CellAttributes
+    from main_window import MainWindow
 
 
 LICENSE = "GNU GENERAL PUBLIC LICENSE Version 3"
@@ -98,712 +57,13 @@ QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
 QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
 
 
-class MainWindow(QMainWindow):
-    """Pyspread main window"""
-
-    gui_update = pyqtSignal(dict)
-
-    def __init__(self, filepath: str = None, default_settings: bool = False):
-        """
-        :param filepath: File path for inital file to be opened
-        :param default_settings: Ignore stored `QSettings` and use defaults
-
-        """
-
-        super().__init__()
-
-        self._loading = True  # For initial loading of pyspread
-        self.prevent_updates = False  # Prevents setData updates in grid
-
-        self.settings = Settings(self, reset_settings=default_settings)
-        self.workflows = Workflows(self)
-        self.undo_stack = QUndoStack(self)
-        self.refresh_timer = QTimer()
-
-        self._init_widgets()
-
-        self.main_window_actions = MainWindowActions(self)
-        self.main_window_toolbar_actions = MainWindowActions(self,
-                                                             shortcuts=False)
-
-        self._init_window()
-        self._init_toolbars()
-
-        self.settings.restore()
-        if self.settings.signature_key is None:
-            self.settings.signature_key = genkey()
-
-        # Print area for print requests
-        self.print_area = None
-
-        # Update recent files in the file menu
-        self.menuBar().file_menu.history_submenu.update()
-
-        # Update toolbar toggle checkboxes
-        self.update_action_toggles()
-
-        # Update the GUI so that everything matches the model
-        cell_attributes = self.grid.model.code_array.cell_attributes
-        attributes = cell_attributes[self.grid.current]
-        self.on_gui_update(attributes)
-
-        self._last_focused_grid = self.grid
-
-        self._loading = False
-        self._previous_window_state = self.windowState()
-
-        # Open initial file if provided by the command line
-        if filepath is not None:
-            if self.workflows.filepath_open(filepath):
-                self.workflows.update_main_window_title()
-            else:
-                msg = "File '{}' could not be opened.".format(filepath)
-                self.statusBar().showMessage(msg)
-
-    def _init_window(self):
-        """Initialize main window components"""
-
-        self.setWindowTitle(APP_NAME)
-        self.setWindowIcon(Icon.pyspread)
-
-        # Safe mode widget
-        self.safe_mode_widget = QSvgWidget(str(IconPath.safe_mode),
-                                           self.statusBar())
-        msg = "%s is in safe mode.\nExpressions are not evaluated." % APP_NAME
-        self.safe_mode_widget.setToolTip(msg)
-        self.statusBar().addPermanentWidget(self.safe_mode_widget)
-        self.safe_mode_widget.hide()
-
-        # Selection mode widget
-        self.selection_mode_widget = QSvgWidget(str(IconPath.selection_mode),
-                                                self.statusBar())
-        msg = "Selection mode active. Cells cannot be edited.\n" + \
-              "Selecting cells adds relative references into the entry " + \
-              "line. Additionally pressing `Meta` switches to absolute " + \
-              "references.\nEnd selection mode by clicking into the entry " + \
-              "line or with `Esc` when focusing the grid."
-        self.selection_mode_widget.setToolTip(msg)
-        self.statusBar().addPermanentWidget(self.selection_mode_widget)
-        self.selection_mode_widget.hide()
-
-        # Disable the approve fiel menu button
-        self.main_window_actions.approve.setEnabled(False)
-
-        self.setMenuBar(MenuBar(self))
-
-    def resizeEvent(self, event: QEvent):
-        """Overloaded, aborts on self._loading
-
-        :param event: Resize event
-
-        """
-
-        if self._loading:
-            return
-
-        super(MainWindow, self).resizeEvent(event)
-
-    def closeEvent(self, event: QEvent = None):
-        """Overloaded, allows saving changes or canceling close
-
-        :param event: Any QEvent
-
-        """
-
-        if event:
-            event.ignore()
-        self.workflows.file_quit()  # has @handle_changed_since_save decorator
-
-    def _init_widgets(self):
-        """Initialize widgets"""
-
-        self.widgets = Widgets(self)
-
-        self.entry_line = Entryline(self)
-
-        self.vsplitter = QSplitter(Qt.Vertical, self)
-        self.hsplitter_1 = QSplitter(Qt.Horizontal, self)
-        self.hsplitter_2 = QSplitter(Qt.Horizontal, self)
-
-        # Set up the table choice first
-        _no_tables = self.settings.shape[2]
-        self.table_choice = TableChoice(self, _no_tables)
-
-        # We have one main view that is used as default view
-        self.grid = Grid(self)
-        # Further views of the grid
-        self.grid_2 = Grid(self, self.grid.model)
-        self.grid_3 = Grid(self, self.grid.model)
-        self.grid_4 = Grid(self, self.grid.model)
-
-        self.grids = [self.grid, self.grid_2, self.grid_3, self.grid_4]
-
-        self.macro_panel = MacroPanel(self, self.grid.model.code_array)
-
-        self.main_panel = QWidget(self)
-
-        self.entry_line_dock = QDockWidget("Entry Line", self)
-        self.entry_line_dock.setObjectName("Entry Line Panel")
-        self.entry_line_dock.setWidget(self.entry_line)
-        self.addDockWidget(Qt.TopDockWidgetArea, self.entry_line_dock)
-        self.resizeDocks([self.entry_line_dock], [10], Qt.Horizontal)
-
-        self.macro_dock = QDockWidget("Macros", self)
-        self.macro_dock.setObjectName("Macro Panel")
-        self.macro_dock.setWidget(self.macro_panel)
-        self.addDockWidget(Qt.RightDockWidgetArea, self.macro_dock)
-
-        self._layout()
-
-        self.entry_line_dock.installEventFilter(self)
-        self.macro_dock.installEventFilter(self)
-
-        QApplication.instance().focusChanged.connect(self.on_focus_changed)
-        self.gui_update.connect(self.on_gui_update)
-        self.refresh_timer.timeout.connect(self.on_refresh_timer)
-
-        # Connect widgets only to first grid
-        self.widgets.text_color_button.colorChanged.connect(
-            self.grid.on_text_color)
-        self.widgets.background_color_button.colorChanged.connect(
-            self.grid.on_background_color)
-        self.widgets.line_color_button.colorChanged.connect(
-            self.grid.on_line_color)
-        self.widgets.font_combo.fontChanged.connect(self.grid.on_font)
-        self.widgets.font_size_combo.fontSizeChanged.connect(
-            self.grid.on_font_size)
-
-    def _layout(self):
-        """Layouts for main window"""
-
-        self.central_layout = QVBoxLayout(self.main_panel)
-        self.central_layout.addWidget(self.vsplitter)
-        self.central_layout.addWidget(self.grid.table_choice)
-
-        self.vsplitter.addWidget(self.hsplitter_1)
-        self.vsplitter.addWidget(self.hsplitter_2)
-
-        self.hsplitter_1.addWidget(self.grid)
-        self.hsplitter_1.addWidget(self.grid_2)
-        self.hsplitter_2.addWidget(self.grid_3)
-        self.hsplitter_2.addWidget(self.grid_4)
-
-        self.vsplitter.setSizes([1, 0])
-        self.hsplitter_1.setSizes([1, 0])
-        self.hsplitter_2.setSizes([1, 0])
-
-        self.main_panel.setLayout(self.central_layout)
-        self.setCentralWidget(self.main_panel)
-
-    def eventFilter(self, source: QWidget, event: QEvent) -> bool:
-        """Overloaded event filter for handling QDockWidget close events
-
-        Updates the menu if the macro panel is closed.
-
-        :param source: Source widget of event
-        :param event: Any QEvent
-
-        """
-
-        if event.type() == QEvent.Close and isinstance(source, QDockWidget):
-            if source.windowTitle() == "Macros":
-                self.main_window_actions.toggle_macro_dock.setChecked(False)
-            elif source.windowTitle() == "Entry Line":
-                self.main_window_actions.toggle_entry_line_dock.setChecked(
-                    False)
-
-        return super().eventFilter(source, event)
-
-    def _init_toolbars(self):
-        """Initialize the main window toolbars"""
-
-        self.main_toolbar = MainToolBar(self)
-        self.macro_toolbar = MacroToolbar(self)
-        self.find_toolbar = FindToolbar(self)
-        self.format_toolbar = FormatToolbar(self)
-
-        self.addToolBar(self.main_toolbar)
-        self.addToolBar(self.macro_toolbar)
-        self.addToolBar(self.find_toolbar)
-        self.addToolBarBreak()
-        self.addToolBar(self.format_toolbar)
-
-    def update_action_toggles(self):
-        """Updates the toggle menu check states"""
-
-        actions = self.main_window_actions
-
-        maintoolbar_visible = self.main_toolbar.isVisibleTo(self)
-        actions.toggle_main_toolbar.setChecked(maintoolbar_visible)
-
-        macrotoolbar_visible = self.macro_toolbar.isVisibleTo(self)
-        actions.toggle_macro_toolbar.setChecked(macrotoolbar_visible)
-
-        formattoolbar_visible = self.format_toolbar.isVisibleTo(self)
-        actions.toggle_format_toolbar.setChecked(formattoolbar_visible)
-
-        findtoolbar_visible = self.find_toolbar.isVisibleTo(self)
-        actions.toggle_find_toolbar.setChecked(findtoolbar_visible)
-
-        entryline_visible = self.entry_line_dock.isVisibleTo(self)
-        actions.toggle_entry_line_dock.setChecked(entryline_visible)
-
-        macrodock_visible = self.macro_dock.isVisibleTo(self)
-        actions.toggle_macro_dock.setChecked(macrodock_visible)
-
-    @property
-    def focused_grid(self):
-        """Returns grid with focus or self if none has focus"""
-
-        try:
-            return self._last_focused_grid
-        except AttributeError:
-            return self.grid
-
-    @property
-    def safe_mode(self) -> bool:
-        """Returns safe_mode state. In safe_mode cells are not evaluated."""
-
-        return self.grid.model.code_array.safe_mode
-
-    @safe_mode.setter
-    def safe_mode(self, value: bool):
-        """Sets safe mode.
-
-        This triggers the safe_mode icon in the statusbar.
-
-        If safe_mode changes from True to False then caches are cleared and
-        macros are executed.
-
-        :param value: Safe mode
-
-        """
-
-        if self.grid.model.code_array.safe_mode == bool(value):
-            return
-
-        self.grid.model.code_array.safe_mode = bool(value)
-
-        if value:  # Safe mode entered
-            self.safe_mode_widget.show()
-            # Enable approval menu entry
-            self.main_window_actions.approve.setEnabled(True)
-        else:  # Safe_mode disabled
-            self.safe_mode_widget.hide()
-            # Disable approval menu entry
-            self.main_window_actions.approve.setEnabled(False)
-            # Clear result cache
-            self.grid.model.code_array.result_cache.clear()
-            # Execute macros
-            self.macro_panel.on_apply()
-
-    def on_print(self):
-        """Print event handler"""
-
-        # Create printer
-        printer = QPrinter(mode=QPrinter.HighResolution)
-
-        # Get print area
-        self.print_area = PrintAreaDialog(self, self.grid,
-                                          title="Print area").area
-        if self.print_area is None:
-            return
-
-        # Create print dialog
-        dialog = QPrintDialog(printer, self)
-        if dialog.exec_() == QPrintDialog.Accepted:
-            self.on_paint_request(printer)
-
-    def on_preview(self):
-        """Print preview event handler"""
-
-        # Create printer
-        printer = QPrinter(mode=QPrinter.HighResolution)
-
-        # Get print area
-        self.print_area = PrintAreaDialog(self, self.grid,
-                                          title="Print area").area
-        if self.print_area is None:
-            return
-
-        # Create print preview dialog
-        dialog = PrintPreviewDialog(printer)
-
-        dialog.paintRequested.connect(self.on_paint_request)
-        dialog.exec_()
-
-    def on_paint_request(self, printer: QPrinter):
-        """Paints to printer
-
-        :param printer: Target printer
-
-        """
-
-        painter = QPainter(printer)
-        option = QStyleOptionViewItem()
-        painter.setRenderHints(QPainter.SmoothPixmapTransform
-                               | QPainter.SmoothPixmapTransform)
-
-        page_rect = printer.pageRect()
-
-        rows = list(self.workflows.get_paint_rows(self.print_area.top,
-                                                  self.print_area.bottom))
-        columns = list(self.workflows.get_paint_columns(self.print_area.left,
-                                                        self.print_area.right))
-        tables = list(self.workflows.get_paint_tables(self.print_area.first,
-                                                      self.print_area.last))
-        if not all((rows, columns, tables)):
-            return
-
-        old_table = self.grid.table
-
-        for i, table in enumerate(tables):
-            self.grid.table = table
-
-            zeroidx = self.grid.model.index(0, 0)
-            zeroidx_rect = self.grid.visualRect(zeroidx)
-
-            minidx = self.grid.model.index(min(rows), min(columns))
-            minidx_rect = self.grid.visualRect(minidx)
-
-            maxidx = self.grid.model.index(max(rows), max(columns))
-            maxidx_rect = self.grid.visualRect(maxidx)
-
-            grid_width = maxidx_rect.x() + maxidx_rect.width() \
-                - minidx_rect.x()
-            grid_height = maxidx_rect.y() + maxidx_rect.height() \
-                - minidx_rect.y()
-            grid_rect = QRectF(minidx_rect.x() - zeroidx_rect.x(),
-                               minidx_rect.y() - zeroidx_rect.y(),
-                               grid_width, grid_height)
-
-            self.settings.print_zoom = min(page_rect.width() / grid_width,
-                                           page_rect.height() / grid_height)
-
-            with painter_save(painter):
-                painter.scale(self.settings.print_zoom,
-                              self.settings.print_zoom)
-
-                # Translate so that the grid starts at upper left paper edge
-                painter.translate(zeroidx_rect.x() - minidx_rect.x(),
-                                  zeroidx_rect.y() - minidx_rect.y())
-
-                # Draw grid cells
-                self.workflows.paint(painter, option, grid_rect, rows, columns)
-
-            self.settings.print_zoom = None
-
-            if i != len(tables) - 1:
-                printer.newPage()
-
-        self.grid.table = old_table
-
-    def on_fullscreen(self):
-        """Fullscreen toggle event handler"""
-
-        if self.windowState() == Qt.WindowFullScreen:
-            self.setWindowState(self._previous_window_state)
-        else:
-            self._previous_window_state = self.windowState()
-            self.setWindowState(Qt.WindowFullScreen)
-
-    def on_approve(self):
-        """Approve event handler"""
-
-        if ApproveWarningDialog(self).choice:
-            self.safe_mode = False
-
-    def on_clear_globals(self):
-        """Clear globals event handler"""
-
-        self.grid.model.code_array.result_cache.clear()
-
-        # Clear globals
-        self.grid.model.code_array.clear_globals()
-        self.grid.model.code_array.reload_modules()
-
-    def on_preferences(self):
-        """Preferences event handler (:class:`dialogs.PreferencesDialog`) """
-
-        data = PreferencesDialog(self).data
-
-        if data is not None:
-            max_file_history_changed = \
-                self.settings.max_file_history != data['max_file_history']
-
-            # Dialog has been approved --> Store data to settings
-            for key in data:
-                if key == "signature_key" and not data[key]:
-                    data[key] = genkey()
-                self.settings.__setattr__(key, data[key])
-
-            # Immediately adjust file history in menu
-            if max_file_history_changed:
-                self.menuBar().file_menu.history_submenu.update()
-
-    def on_dependencies(self):
-        """Dependancies installer (:class:`installer.InstallerDialog`) """
-
-        dial = DependenciesDialog(self)
-        dial.exec_()
-
-    def on_undo(self):
-        """Undo event handler"""
-
-        self.undo_stack.undo()
-
-    def on_redo(self):
-        """Undo event handler"""
-
-        self.undo_stack.redo()
-
-    def on_toggle_refresh_timer(self, toggled: bool):
-        """Toggles periodic timer for frozen cells
-
-        :param toggled: Toggle state
-
-        """
-
-        if toggled:
-            self.refresh_timer.start(self.settings.refresh_timeout)
-        else:
-            self.refresh_timer.stop()
-
-    def on_refresh_timer(self):
-        """Event handler for self.refresh_timer.timeout
-
-        Called for periodic updates of frozen cells.
-        Does nothing if either the entry_line or a cell editor is active.
-
-        """
-
-        if not self.entry_line.hasFocus() \
-           and self.grid.state() != self.grid.EditingState:
-            self.grid.refresh_frozen_cells()
-
-    def _toggle_widget(self, widget: QWidget, action_name: str, toggled: bool):
-        """Toggles widget visibility and updates toggle actions
-
-        :param widget: Widget to be toggled shown or hidden
-        :param action_name: Name of action from Action class
-        :param toggled: Toggle state
-
-        """
-
-        if toggled:
-            widget.show()
-        else:
-            widget.hide()
-
-        self.main_window_actions[action_name].setChecked(widget.isVisible())
-
-    def on_toggle_main_toolbar(self, toggled: bool):
-        """Main toolbar toggle event handler
-
-        :param toggled: Toggle state
-
-        """
-
-        self._toggle_widget(self.main_toolbar, "toggle_main_toolbar", toggled)
-
-    def on_toggle_macro_toolbar(self, toggled: bool):
-        """Macro toolbar toggle event handler
-
-        :param toggled: Toggle state
-
-        """
-
-        self._toggle_widget(self.macro_toolbar, "toggle_macro_toolbar",
-                            toggled)
-
-    def on_toggle_format_toolbar(self, toggled: bool):
-        """Format toolbar toggle event handler
-
-        :param toggled: Toggle state
-
-        """
-
-        self._toggle_widget(self.format_toolbar, "toggle_format_toolbar",
-                            toggled)
-
-    def on_toggle_find_toolbar(self, toggled: bool):
-        """Find toolbar toggle event handler
-
-        :param toggled: Toggle state
-
-        """
-
-        self._toggle_widget(self.find_toolbar, "toggle_find_toolbar", toggled)
-
-    def on_toggle_entry_line_dock(self, toggled: bool):
-        """Entryline toggle event handler
-
-        :param toggled: Toggle state
-
-        """
-
-        self._toggle_widget(self.entry_line_dock, "toggle_entry_line_dock",
-                            toggled)
-
-    def on_toggle_macro_dock(self, toggled: bool):
-        """Macro panel toggle event handler
-
-        :param toggled: Toggle state
-
-        """
-
-        self._toggle_widget(self.macro_dock, "toggle_macro_dock", toggled)
-
-    def on_manual(self):
-        """Show manual browser"""
-
-        dialog = ManualDialog(self)
-        dialog.show()
-
-    def on_tutorial(self):
-        """Show tutorial browser"""
-
-        dialog = TutorialDialog(self)
-        dialog.show()
-
-    def on_about(self):
-        """Show about message box"""
-
-        def devs_string(devs: list) -> str:
-            """Get string from devs list"""
-
-            devs_str = "".join("<li>{}</li>".format(dev) for dev in devs)
-            return "<ul>{}</ul>".format(devs_str)
-
-        about_msg_template = \
-            """<b>{name}</b><><p>
-            A non-traditional Python spreadsheet application<p>
-            Version:&emsp;{version}<p>
-            Created by:&emsp;{devs}<p>
-            Documented by:&emsp;{doc_devs}<p>
-            Copyright:&emsp;{copyright_owner}<p>
-            License:&emsp;{license}<p>
-            Web site:&emsp;<a href="{web_url}">{web_url}</a>
-            """
-
-        devs = ("Martin Manns", "Jason Sexauer", "Vova Kolobok", "mgunyho",
-                "Pete Morgan")
-        devs_str = devs_string(devs)
-
-        doc_devs = ("Martin Manns", "Bosko Markovic", "Pete Morgan")
-        doc_devs_str = devs_string(doc_devs)
-
-        copyright_owner = "Martin Manns"
-
-        about_msg = about_msg_template.format(
-            name=APP_NAME,
-            version=VERSION,
-            license=LICENSE,
-            devs=devs_str,
-            doc_devs=doc_devs_str,
-            copyright_owner=copyright_owner,
-            web_url=WEB_URL)
-
-        QMessageBox.about(self, "About {}".format(APP_NAME), about_msg)
-
-    def on_focus_changed(self, old: QWidget, now: QWidget):
-        """Handles grid clicks from entry line"""
-
-        if old == self.grid and now == self.entry_line:
-            self.grid.selection_mode = False
-
-    def on_gui_update(self, attributes: CellAttributes):
-        """GUI update that shall be called on each cell change
-
-        :param attributes: Attributes of current cell
-
-        """
-
-        widgets = self.widgets
-        menubar = self.menuBar()
-
-        is_bold = attributes.fontweight == QFont.Bold
-        self.main_window_actions.bold.setChecked(is_bold)
-
-        is_italic = attributes.fontstyle == QFont.StyleItalic
-        self.main_window_actions.italics.setChecked(is_italic)
-
-        underline_action = self.main_window_actions.underline
-        underline_action.setChecked(attributes.underline)
-
-        strikethrough_action = self.main_window_actions.strikethrough
-        strikethrough_action.setChecked(attributes.strikethrough)
-
-        renderer = attributes.renderer
-        widgets.renderer_button.set_current_action(renderer)
-        widgets.renderer_button.set_menu_checked(renderer)
-
-        freeze_action = self.main_window_actions.freeze_cell
-        freeze_action.setChecked(attributes.frozen)
-
-        lock_action = self.main_window_actions.lock_cell
-        lock_action.setChecked(attributes.locked)
-        self.entry_line.setReadOnly(attributes.locked)
-
-        button_action = self.main_window_actions.button_cell
-        button_action.setChecked(attributes.button_cell is not False)
-
-        rotation = "rotate_{angle}".format(angle=int(attributes.angle))
-        widgets.rotate_button.set_current_action(rotation)
-        widgets.rotate_button.set_menu_checked(rotation)
-        widgets.justify_button.set_current_action(attributes.justification)
-        widgets.justify_button.set_menu_checked(attributes.justification)
-        widgets.align_button.set_current_action(attributes.vertical_align)
-        widgets.align_button.set_menu_checked(attributes.vertical_align)
-
-        border_action = self.main_window_actions.border_group.checkedAction()
-        if border_action is not None:
-            icon = border_action.icon()
-            menubar.format_menu.border_submenu.setIcon(icon)
-            self.format_toolbar.border_menu_button.setIcon(icon)
-
-        border_width_action = \
-            self.main_window_actions.border_width_group.checkedAction()
-        if border_width_action is not None:
-            icon = border_width_action.icon()
-            menubar.format_menu.line_width_submenu.setIcon(icon)
-            self.format_toolbar.line_width_button.setIcon(icon)
-
-        if attributes.textcolor is None:
-            text_color = self.grid.palette().color(QPalette.Text)
-        else:
-            text_color = QColor(*attributes.textcolor)
-        widgets.text_color_button.color = text_color
-
-        if attributes.bordercolor_bottom is None:
-            line_color = self.grid.palette().color(QPalette.Mid)
-        else:
-            line_color = QColor(*attributes.bordercolor_bottom)
-        widgets.line_color_button.color = line_color
-
-        if attributes.bgcolor is None:
-            bgcolor = self.grid.palette().color(QPalette.Base)
-        else:
-            bgcolor = QColor(*attributes.bgcolor)
-        widgets.background_color_button.color = bgcolor
-
-        if attributes.textfont is None:
-            widgets.font_combo.font = QFont().family()
-        else:
-            widgets.font_combo.font = attributes.textfont
-        widgets.font_size_combo.size = attributes.pointsize
-
-        merge_cells_action = self.main_window_actions.merge_cells
-        merge_cells_action.setChecked(attributes.merge_area is not None)
-
-
 def excepthook(exception_type, exception_value, exception_traceback):
     """Exception hook that prevents pyspread from crashing on exceptions"""
 
     traceback_msg = "".join(traceback.format_exception(exception_type,
                                                        exception_value,
                                                        exception_traceback))
-    print("Error: {}\n".format(traceback_msg))
+    print(f"Error: {traceback_msg}\n")
 
 
 def main():
@@ -812,7 +72,7 @@ def main():
     sys.excepthook = excepthook
 
     parser = PyspreadArgumentParser()
-    args, unknown = parser.parse_known_args()
+    args, _ = parser.parse_known_args()
 
     app = QApplication(sys.argv)
     main_window = MainWindow(args.file, default_settings=args.default_settings)
diff --git a/pyspread/settings.py b/pyspread/settings.py
index 2fa551f4e12fc25091d5cddfaaf337bce84d6d83..1116b2ee8732b59938838fdd3a44722077789e80 100644
--- a/pyspread/settings.py
+++ b/pyspread/settings.py
@@ -127,6 +127,26 @@ class Settings:
     find_dialog_state = None
     """Find dialog state - needs to be stored when dialog is closed"""
 
+    encodings = (
+        "ascii", "big5", "big5hkscs", "cp037", "cp424", "cp437",
+        "cp500", "cp720", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
+        "cp857", "cp858", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865",
+        "cp866", "cp869", "cp874", "cp875", "cp932", "cp949", "cp950",
+        "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252", "cp1253",
+        "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "euc-jp",
+        "euc-jis-2004", "euc-jisx0213", "euc-kr", "gb2312", "gbk", "gb18030",
+        "hz", "iso2022-jp", "iso2022-jp-1", "iso2022-jp-2", "iso2022-jp-2004",
+        "iso2022-jp-3", "iso2022-jp-ext", "iso2022-kr", "latin-1", "iso8859-2",
+        "iso8859-3", "iso8859-4", "iso8859-5", "iso8859-6", "iso8859-7",
+        "iso8859-8", "iso8859-9", "iso8859-10", "iso8859-13", "iso8859-14",
+        "iso8859-15", "iso8859-16", "johab", "koi8-r", "koi8-u",
+        "mac-cyrillic", "mac-greek", "mac-iceland", "mac-latin2", "mac-roman",
+        "mac-turkish", "ptcp154", "shift-jis", "shift-jis-2004",
+        "shift-jisx0213", "utf-32", "utf-32-be", "utf-32-le", "utf-16",
+        "utf-16-be", "utf-16-le", "utf-7", "utf-8", "utf-8-sig",
+    )
+    """Encodings for importing files (e.g. CSV or SVG)"""
+
     sniff_size = 65536
     """Number of bytes for csv sniffer
        sniff_size should be larger than 1st+2nd line
@@ -155,8 +175,7 @@ class Settings:
         """
 
         if not hasattr(self, key):
-            raise AttributeError("{self} has no attribute {key}.".format(
-                                 self=self, key=key))
+            raise AttributeError(f"{self} has no attribute {key}.")
         super().__setattr__(key, value)
 
     def add_to_file_history(self, filename: Path):
@@ -171,6 +190,8 @@ class Settings:
         self.file_history = self.file_history[:self.max_file_history]
 
     def reset(self):
+        """Reset to defaults"""
+
         cls_attrs = (attr for attr in dir(self)
                      if (not attr.startswith("__")
                          and attr not in ("reset", "parent", "save",
@@ -261,7 +282,8 @@ class Settings:
             if attr is None:
                 attr = setting_name
             if mapper is None:
-                def mapper(x): return x
+                def mapper(x):
+                    return x
             setattr(self, attr, mapper(value))
 
         # Application state
diff --git a/pyspread/share/applications/io.gitlab.pyspread.pyspread.desktop b/pyspread/share/applications/io.gitlab.pyspread.pyspread.desktop
new file mode 100755
index 0000000000000000000000000000000000000000..435a688c1e6e760d4754bf6689ac4b20da479400
--- /dev/null
+++ b/pyspread/share/applications/io.gitlab.pyspread.pyspread.desktop
@@ -0,0 +1,20 @@
+#!/usr/bin/env xdg-open
+[Desktop Entry]
+Categories=Office;Spreadsheet;
+Comment[en_US]=pyspread is a non-traditional spreadsheet application that is based on and written in the programming language Python
+Comment=pyspread is a non-traditional spreadsheet application that is based on and written in the programming language Python
+Exec=pyspread
+GenericName[en_US]=Python based Spreadsheet
+GenericName=Python based Spreadsheet
+Keywords=python;spreadsheet;matplotlib;math;
+Icon=pyspread
+MimeType=application/x-pyspread-spreadsheet;application/x-pyspread-bz-spreadsheet
+Name[en_US]=pyspread
+Name=pyspread
+StartupNotify=false
+Terminal=false
+Type=Application
+X-DBUS-ServiceName=
+X-DBUS-StartupType=
+X-KDE-SubstituteUID=false
+X-KDE-Username=
diff --git a/pyspread/share/doc/manual/advanced_topics.md b/pyspread/share/doc/manual/advanced_topics.md
index 89912bf4058fb47b47dbf6553c61056283c9cc15..25bcdc5e4eb416f1ccda50ef82008c7be136e61f 100644
--- a/pyspread/share/doc/manual/advanced_topics.md
+++ b/pyspread/share/doc/manual/advanced_topics.md
@@ -7,6 +7,10 @@ title: Advanced topics
 
 # Advanced topics
 
+## Accessing the current cell from a macro
+
+The variables X, Y, Z, R, C and T are set to None inside the macro panel. In order to access the row, column or table of the cell that is calling a function inside the macro panel or inside an external library, the respective variables have to be provided as parameters.
+
 ## Conditional formatting
 
 For conditionally formatting the background color of a cell, enter
diff --git a/pyspread/share/doc/manual/basic_concepts.md b/pyspread/share/doc/manual/basic_concepts.md
index 840f63a807b7e62504461a9a4f92f85a12ebabcb..3ed5577a20418b2288d2f865c22a48eb775cc972 100644
--- a/pyspread/share/doc/manual/basic_concepts.md
+++ b/pyspread/share/doc/manual/basic_concepts.md
@@ -135,7 +135,7 @@ Since Python expressions are evaluated in *pyspread*, a *pyspread* spreadsheet i
 
 The risk is the the same that all office applications poese, which is why many provide precautions. The concept in *pyspread* is that you - the user - are trustworthy and no-one else. When starting *pyspread* the first time, a secret key is generated that is stored in the local configuration file (`~/.config/pyspread/pyspread.conf` on many Linux systems). You can manually edit the secret key in the Preferences Dialog (select `Preferences...` in the `File` menu).
 
-If you save a file then a signature is saved with it (suffix .pys.sig). Only if the signature is valid for the stored secret key, you can re-open the file directly. Otherwise, e.g. if anyone else opens the file, it is displayed in `Safe mode`, i.e. each cell displays the cell code and no cell code is evaluated. The user can approve the file by selecting `Approve file` in the `File` menu. Afterwards, cell code is evaluated. When the user then saves the file, it is newly signed. Then it can be re-opened without safe mode.
+If you save a file then a signature is saved with it (suffix `.pys.sig`). Only if the signature is valid for the stored secret key, you can re-open the file directly. Otherwise, e.g. if anyone else opens the file, it is displayed in `Safe mode`, i.e. each cell displays the cell code and no cell code is evaluated. The user can approve the file by selecting `Approve file` in the `File` menu. Afterwards, cell code is evaluated. When the user then saves the file, it is newly signed. Then it can be re-opened without safe mode.
 
 Never approve foreign pys-files unless you have thoroughly checked each cell. Each cell may delete valuable files. Malicious cells are likely to be hidden in the middle of a million rows. If unsure, inspect the pysu / pys-file. pysu files are plain text files. pys files are bzip2-ed text files. Both are easy to read and understand. It may also be a good idea to run pyspread (and any other office application) with a special user or sandbox that has restricted privileges.
 
diff --git a/pyspread/share/doc/manual/file_menu.md b/pyspread/share/doc/manual/file_menu.md
index fa0422856ca84db7e9d45082e20cb11ab411f162..45d134c88f7addb266fdfac7bae9e7cf2d3299b5 100644
--- a/pyspread/share/doc/manual/file_menu.md
+++ b/pyspread/share/doc/manual/file_menu.md
@@ -70,13 +70,15 @@ Macro text
 
 A csv file can be imported via `File → Import`.
 
-After selecting a file, the CSV file import dialog opens. In this dialog, CSV import options can be set. Furthermore, target Python types can be specified, so that import of dates becomes possible. The grid of the import dialog only shows the first few rows of the csv files in order to give an impression how import data will look like in *pyspread*.
+If the selected file is not encoded in UTF-8, an encoding has to be chosen in a dialog. If the file is encoded in UTF-8 or if the chosen encoding can be read, the CSV file import dialog opens. In this dialog, CSV import options can be set. Furthermore, target Python types can be specified, so that import of dates becomes possible. The grid of the import dialog only shows the first few rows of the csv files in order to give an impression how import data will look like in *pyspread*.
 
-Importing a file always activates safe mode because code in the CSV file might prove harmful.
+Be careful when importing a file that contains code, because code in the CSV file might prove harmful.
+
+For importing money data, it is recommended to use the decimal or the Money datatype. The latter supports specific currencies and requires the optional dependency [py-moneyed](https://pypi.org/project/py-moneyed/).
 
 ## File → Export
 
-*pyspread* can export spreadsheets to `.csv` files.
+*pyspread* can export spreadsheets to `csv` files and `svg` files.
 
 When exporting a file then a dialog is displayed, in which the area to be exported can be chosen.
 
diff --git a/pyspread/share/icons/actions/macro-insert-sum.svg b/pyspread/share/icons/actions/macro-insert-sum.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e3e382da6f1d778d6a324b15e7bec82dddcfd9f0
--- /dev/null
+++ b/pyspread/share/icons/actions/macro-insert-sum.svg
@@ -0,0 +1,699 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="52.632317"
+   height="72.57431"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
+   version="1.0"
+   sodipodi:docname="macro-insert-sum.svg"
+   inkscape:output_extension="org.inkscape.output.svgz.inkscape"
+   inkscape:export-filename="/home/david/insert-table.png"
+   inkscape:export-xdpi="33.75"
+   inkscape:export-ydpi="33.75"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient8516">
+      <stop
+         style="stop-color:#497fc6;stop-opacity:1;"
+         offset="0"
+         id="stop8512" />
+      <stop
+         style="stop-color:#90b3d9;stop-opacity:1"
+         offset="1"
+         id="stop8514" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient4309">
+      <stop
+         style="stop-color:#bbbbbb;stop-opacity:1;"
+         offset="0"
+         id="stop4311" />
+      <stop
+         style="stop-color:#dddddd;stop-opacity:1;"
+         offset="1"
+         id="stop4313" />
+    </linearGradient>
+    <linearGradient
+       y2="20"
+       x2="60.125"
+       y1="12"
+       x1="60.125"
+       gradientUnits="userSpaceOnUse"
+       id="XMLID_39_">
+      <stop
+         id="stop62"
+         style="stop-color:#A3C8FF"
+         offset="0" />
+      <stop
+         id="stop64"
+         style="stop-color:#BFD9FF"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="XMLID_43_"
+       gradientUnits="userSpaceOnUse"
+       x1="153"
+       y1="175.55659"
+       x2="153"
+       y2="172.4442"
+       gradientTransform="translate(-143,-64)">
+      <stop
+         offset="0"
+         style="stop-color:#FFFFFF"
+         id="stop92" />
+      <stop
+         offset="1"
+         style="stop-color:#DDDDDD"
+         id="stop94" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#XMLID_39_"
+       id="linearGradient4487"
+       gradientUnits="userSpaceOnUse"
+       x1="14"
+       y1="52"
+       x2="14"
+       y2="32"
+       gradientTransform="matrix(1,0,0,0.8,23.999998,-5.6)" />
+    <linearGradient
+       id="linearGradient3711"
+       gradientUnits="userSpaceOnUse"
+       x1="-84.002403"
+       y1="-383.9971"
+       x2="-23.516129"
+       y2="-383.9975"
+       gradientTransform="rotate(90,-90.0007,50.0022)">
+      <stop
+         offset="0"
+         style="stop-color:white;stop-opacity:1;"
+         id="stop3713" />
+      <stop
+         offset="1"
+         style="stop-color:white;stop-opacity:0;"
+         id="stop3715" />
+    </linearGradient>
+    <linearGradient
+       gradientTransform="rotate(90,-90.0007,50.0022)"
+       y2="-383.9971"
+       x2="-12.0029"
+       y1="-383.9971"
+       x1="-84.002403"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient26907">
+      <stop
+         id="stop26909"
+         style="stop-color:#888a85;stop-opacity:1;"
+         offset="0" />
+      <stop
+         id="stop26911"
+         style="stop-color:#2e3436;stop-opacity:1;"
+         offset="1" />
+    </linearGradient>
+    <radialGradient
+       r="24"
+       fy="100"
+       fx="-60"
+       cy="84"
+       cx="-44"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,-1.1332086,-170.74569)"
+       gradientUnits="userSpaceOnUse"
+       id="radialGradient2281"
+       xlink:href="#linearGradient3030"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="72"
+       x2="14.697635"
+       y1="96"
+       x1="26.697636"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,-63.641028,-170.74569)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient2249"
+       xlink:href="#linearGradient3260"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="96.001434"
+       x2="11.68106"
+       y1="52"
+       x1="6.6976352"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,-63.641028,-170.74569)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient2247"
+       xlink:href="#linearGradient3260"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="108.0104"
+       x2="11.68106"
+       y1="60.539303"
+       x1="11.68106"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,-63.630923,-170.74569)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient2245"
+       xlink:href="#linearGradient3202"
+       inkscape:collect="always" />
+    <radialGradient
+       r="20"
+       fy="96"
+       fx="-40"
+       cy="84"
+       cx="-44"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,-1.1332086,-170.74569)"
+       gradientUnits="userSpaceOnUse"
+       id="radialGradient2243"
+       xlink:href="#XMLID_4_"
+       inkscape:collect="always" />
+    <radialGradient
+       r="24"
+       fy="100"
+       fx="-60"
+       cy="84"
+       cx="-44"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,-1.1332086,-170.74569)"
+       gradientUnits="userSpaceOnUse"
+       id="radialGradient2239"
+       xlink:href="#linearGradient3030"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="19.281664"
+       x2="80"
+       y1="15.336544"
+       x1="73.742638"
+       spreadMethod="reflect"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient2237"
+       xlink:href="#linearGradient3260"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="18.50366"
+       x2="76.284439"
+       y1="18.50366"
+       x1="64.341988"
+       gradientTransform="scale(1.039383,0.9621093)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient2235"
+       xlink:href="#linearGradient3207"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="19.281664"
+       x2="80"
+       y1="15.336544"
+       x1="73.742638"
+       spreadMethod="reflect"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient2233"
+       xlink:href="#linearGradient5412"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="19.281664"
+       x2="80"
+       y1="15.336544"
+       x1="73.742638"
+       spreadMethod="reflect"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient2231"
+       xlink:href="#linearGradient3260"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="19.281664"
+       x2="80"
+       y1="15.336544"
+       x1="73.742638"
+       spreadMethod="reflect"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient2229"
+       xlink:href="#linearGradient3260"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="104.80668"
+       x2="-62.424866"
+       y1="76.708466"
+       x1="-13.757333"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,-1.1332086,-170.74569)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient2227"
+       xlink:href="#XMLID_4_"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="18.50366"
+       x2="76.284439"
+       y1="18.50366"
+       x1="64.341988"
+       gradientTransform="scale(1.039383,0.9621093)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient3005"
+       xlink:href="#linearGradient3207"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="19.281664"
+       x2="80"
+       y1="15.336544"
+       x1="73.742638"
+       spreadMethod="reflect"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient3003"
+       xlink:href="#linearGradient5412"
+       inkscape:collect="always" />
+    <linearGradient
+       gradientTransform="translate(-36.000006,-20.000008)"
+       y2="105.10625"
+       x2="98.097946"
+       y1="77.512512"
+       x1="97.622581"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient4259"
+       xlink:href="#linearGradient3225"
+       inkscape:collect="always" />
+    <linearGradient
+       id="linearGradient3225">
+      <stop
+         id="stop3227"
+         offset="0"
+         style="stop-color:#ffffff;stop-opacity:1;" />
+      <stop
+         id="stop3229"
+         offset="1"
+         style="stop-color:#ffffff;stop-opacity:0;" />
+    </linearGradient>
+    <linearGradient
+       y2="72"
+       x2="14.697635"
+       y1="96"
+       x1="26.697636"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,13.44087,-51.663795)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient4262"
+       xlink:href="#linearGradient3260"
+       inkscape:collect="always" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient3260">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop3262" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop3264" />
+    </linearGradient>
+    <linearGradient
+       y2="96.001434"
+       x2="11.68106"
+       y1="52"
+       x1="6.6976352"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,13.44087,-51.663795)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient4265"
+       xlink:href="#linearGradient3260"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="108.0104"
+       x2="11.68106"
+       y1="60.539303"
+       x1="11.68106"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,13.450975,-51.663795)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient4268"
+       xlink:href="#linearGradient3202"
+       inkscape:collect="always" />
+    <linearGradient
+       id="linearGradient3202">
+      <stop
+         id="stop3204"
+         offset="0"
+         style="stop-color:#cbff9c;stop-opacity:1;" />
+      <stop
+         id="stop3206"
+         offset="1"
+         style="stop-color:#65c171;stop-opacity:0" />
+    </linearGradient>
+    <radialGradient
+       r="20"
+       fy="96"
+       fx="-40"
+       cy="84"
+       cx="-44"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,75.948689,-51.663795)"
+       gradientUnits="userSpaceOnUse"
+       id="radialGradient4271"
+       xlink:href="#XMLID_4_"
+       inkscape:collect="always" />
+    <radialGradient
+       id="XMLID_4_"
+       cx="48"
+       cy="-0.2148"
+       r="55.147999"
+       gradientTransform="matrix(0.9792,0,0,0.9725,133.0002,20.8762)"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         offset="0"
+         style="stop-color:#72D13D"
+         id="stop3082" />
+      <stop
+         offset="0.3553"
+         style="stop-color:#35AC1C"
+         id="stop3084" />
+      <stop
+         offset="0.6194"
+         style="stop-color:#0F9508"
+         id="stop3086" />
+      <stop
+         offset="0.7574"
+         style="stop-color:#008C00"
+         id="stop3088" />
+      <stop
+         offset="1"
+         style="stop-color:#007A00"
+         id="stop3090" />
+    </radialGradient>
+    <linearGradient
+       y2="19.281664"
+       x2="80"
+       y1="15.336544"
+       x1="73.742638"
+       spreadMethod="reflect"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient4289"
+       xlink:href="#linearGradient3260"
+       inkscape:collect="always" />
+    <radialGradient
+       r="24"
+       fy="100"
+       fx="-60"
+       cy="84"
+       cx="-44"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,75.948689,-51.663795)"
+       gradientUnits="userSpaceOnUse"
+       id="radialGradient4275"
+       xlink:href="#linearGradient3030"
+       inkscape:collect="always" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient3030">
+      <stop
+         style="stop-color:#000000;stop-opacity:0.77902622"
+         offset="0"
+         id="stop3032" />
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="1"
+         id="stop3034" />
+    </linearGradient>
+    <linearGradient
+       y2="19.281664"
+       x2="80"
+       y1="15.336544"
+       x1="73.742638"
+       spreadMethod="reflect"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient4291"
+       xlink:href="#linearGradient3260"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="0"
+       x2="28"
+       y1="57.5"
+       x1="28"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient5412">
+      <stop
+         id="stop5414"
+         style="stop-color:#fff14d;stop-opacity:1;"
+         offset="0" />
+      <stop
+         id="stop5416"
+         style="stop-color:#f8ffa0;stop-opacity:0;"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3207">
+      <stop
+         id="stop3209"
+         offset="0"
+         style="stop-color:#ffffff;stop-opacity:1;" />
+      <stop
+         id="stop3211"
+         offset="1"
+         style="stop-color:#252525;stop-opacity:0;" />
+    </linearGradient>
+    <linearGradient
+       y2="19.281664"
+       x2="80"
+       y1="15.336544"
+       x1="73.742638"
+       spreadMethod="reflect"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient4297"
+       xlink:href="#linearGradient3260"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="19.281664"
+       x2="80"
+       y1="15.336544"
+       x1="73.742638"
+       spreadMethod="reflect"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient4299"
+       xlink:href="#linearGradient3260"
+       inkscape:collect="always" />
+    <linearGradient
+       y2="104.80668"
+       x2="-62.424866"
+       y1="76.708466"
+       x1="-13.757333"
+       gradientTransform="matrix(0.9969724,0,0,0.9969724,75.948689,-51.663795)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient4284"
+       xlink:href="#XMLID_4_"
+       inkscape:collect="always" />
+    <radialGradient
+       id="XMLID_4_-9"
+       cx="48"
+       cy="-0.2148"
+       r="55.147999"
+       gradientTransform="matrix(0.9792,0,0,0.9725,133.0002,20.8762)"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         offset="0"
+         style="stop-color:#ff0101;stop-opacity:1;"
+         id="stop3082-2" />
+      <stop
+         offset="1"
+         style="stop-color:#800000;stop-opacity:1;"
+         id="stop3090-2" />
+    </radialGradient>
+    <linearGradient
+       id="linearGradient3202-9">
+      <stop
+         id="stop3204-7"
+         offset="0"
+         style="stop-color:#ff8787;stop-opacity:1;" />
+      <stop
+         id="stop3206-3"
+         offset="1"
+         style="stop-color:#ff8787;stop-opacity:0;" />
+    </linearGradient>
+    <radialGradient
+       id="XMLID_4_-5"
+       cx="48"
+       cy="-0.2148"
+       r="55.147999"
+       gradientTransform="matrix(0.9792,0,0,0.9725,133.0002,20.8762)"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         offset="0"
+         style="stop-color:#ff0101;stop-opacity:1;"
+         id="stop3082-4" />
+      <stop
+         offset="1"
+         style="stop-color:#800000;stop-opacity:1;"
+         id="stop3090-7" />
+    </radialGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3601"
+       id="linearGradient3486"
+       gradientUnits="userSpaceOnUse"
+       x1="122.97147"
+       y1="9.1007338"
+       x2="122.97147"
+       y2="174.11676"
+       gradientTransform="translate(0,-12)" />
+    <linearGradient
+       id="linearGradient3601"
+       inkscape:collect="always">
+      <stop
+         id="stop3603"
+         offset="0"
+         style="stop-color:#bbd6ff;stop-opacity:1" />
+      <stop
+         id="stop3605"
+         offset="1"
+         style="stop-color:#0057ae;stop-opacity:1" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3682"
+       id="linearGradient2475"
+       gradientUnits="userSpaceOnUse"
+       x1="29.122221"
+       y1="33.438889"
+       x2="14.296363"
+       y2="6.3463993"
+       gradientTransform="translate(2.2879405e-4)" />
+    <linearGradient
+       id="linearGradient3682">
+      <stop
+         style="stop-color:#497fc6;stop-opacity:1;"
+         offset="0"
+         id="stop3684" />
+      <stop
+         style="stop-color:#90b3d9;stop-opacity:1;"
+         offset="1"
+         id="stop3686" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3601"
+       id="linearGradient8518"
+       gradientUnits="userSpaceOnUse"
+       x1="69.424118"
+       y1="56.825779"
+       x2="142.2137"
+       y2="218.76871" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient8662"
+       id="radialGradient2494"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.362513,0,0,0.58519671,40.223966,72.84508)"
+       cx="24.837126"
+       cy="36.421127"
+       fx="24.837126"
+       fy="36.421127"
+       r="15.644737" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient8662">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop8664" />
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="1"
+         id="stop8666" />
+    </linearGradient>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.2402344"
+     inkscape:cx="-89.096061"
+     inkscape:cy="46.362204"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     width="128px"
+     height="128px"
+     showgrid="false"
+     showborder="false"
+     inkscape:grid-points="true"
+     inkscape:window-width="1280"
+     inkscape:window-height="942"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:pagecheckerboard="0"
+     showguides="false"
+     fit-margin-top="5"
+     fit-margin-left="5"
+     fit-margin-right="5"
+     fit-margin-bottom="5">
+    <inkscape:grid
+       id="GridFromPre046Settings"
+       type="xygrid"
+       originx="-47.748714"
+       originy="-35.739565"
+       spacingx="4"
+       spacingy="4"
+       color="#3f3fff"
+       empcolor="#3f3fff"
+       opacity="0.15"
+       empopacity="0.38"
+       empspacing="2" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-47.748714,-35.739565)">
+    <ellipse
+       style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.2;fill:url(#radialGradient2494);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.21884;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none"
+       id="path2492"
+       inkscape:r_cx="true"
+       inkscape:r_cy="true"
+       cx="74.064873"
+       cy="94.158623"
+       rx="21.316158"
+       ry="9.1552515" />
+    <text
+       xml:space="preserve"
+       style="font-weight:bold;font-size:74.9859px;line-height:1.25;font-family:'Bliss 2';-inkscape-font-specification:'Bliss 2, Bold';font-variation-settings:normal;letter-spacing:0px;word-spacing:0px;display:inline;opacity:1;mix-blend-mode:normal;fill:url(#linearGradient2475);fill-opacity:1;stroke:#2a5387;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1"
+       x="49.586143"
+       y="97.404579"
+       id="text2962-7"><tspan
+         sodipodi:role="line"
+         id="tspan2960-5"
+         x="49.586143"
+         y="97.404579"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Bitstream Vera Sans';-inkscape-font-specification:'Bitstream Vera Sans';font-variation-settings:normal;fill:url(#linearGradient2475);fill-opacity:1;stroke:#2a5387;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1">Σ</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-weight:bold;font-size:74.9857px;line-height:1.25;font-family:'Bliss 2';-inkscape-font-specification:'Bliss 2, Bold';font-variation-settings:normal;letter-spacing:0px;word-spacing:0px;display:inline;opacity:1;mix-blend-mode:normal;fill:url(#linearGradient3486);fill-opacity:1;stroke:url(#linearGradient8518);stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1"
+       x="49.58633"
+       y="97.404289"
+       id="text2962"><tspan
+         sodipodi:role="line"
+         id="tspan2960"
+         x="49.58633"
+         y="97.404289"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Bitstream Vera Sans';-inkscape-font-specification:'Bitstream Vera Sans';font-variation-settings:normal;fill:url(#linearGradient3486);fill-opacity:1;stroke:url(#linearGradient8518);stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1">Σ</tspan></text>
+    <rect
+       y="0"
+       x="0"
+       height="132.12903"
+       width="0"
+       id="rect3244"
+       style="opacity:0.73;fill:#e5ff00;fill-opacity:1;stroke:none;stroke-width:8;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+  </g>
+</svg>
diff --git a/pyspread/share/icons/pyspread.ico b/pyspread/share/icons/hicolor/64x64/pyspread.ico
similarity index 95%
rename from pyspread/share/icons/pyspread.ico
rename to pyspread/share/icons/hicolor/64x64/pyspread.ico
index 7dc1cd8d1d634b9dceaa0fd161631b2eccac9fd5..5fc057a662d82dd2a7d07611ab5583544c4e2fde 100644
Binary files a/pyspread/share/icons/pyspread.ico and b/pyspread/share/icons/hicolor/64x64/pyspread.ico differ
diff --git a/pyspread/share/icons/hicolor/64x64/pyspread.png b/pyspread/share/icons/hicolor/64x64/pyspread.png
new file mode 100644
index 0000000000000000000000000000000000000000..4647ea691d2a37e0755512bac27d5d7dfe46c4bb
Binary files /dev/null and b/pyspread/share/icons/hicolor/64x64/pyspread.png differ
diff --git a/pyspread/share/icons/pyspread.svg b/pyspread/share/icons/hicolor/svg/pyspread.svg
similarity index 100%
rename from pyspread/share/icons/pyspread.svg
rename to pyspread/share/icons/hicolor/svg/pyspread.svg
diff --git a/pyspread/share/metainfo/io.gitlab.pyspread.pyspread.metainfo.xml b/pyspread/share/metainfo/io.gitlab.pyspread.pyspread.metainfo.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1d1030a627e148390ec33b969792c8b529451797
--- /dev/null
+++ b/pyspread/share/metainfo/io.gitlab.pyspread.pyspread.metainfo.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="desktop-application">
+  <id>io.gitlab.pyspread.pyspread</id>
+  <metadata_license>FSFAP</metadata_license>
+  <name>pyspread</name>
+  <summary>pyspread is a non-traditional spreadsheet application that is based on and written in the programming language Python</summary>
+
+  <icon type="stock">pyspread</icon>
+  
+  <project_license>GPL-3.0-or-later</project_license>
+
+  <description>
+    <p>
+      <em>pyspread</em> is a non-traditional spreadsheet application that is based on and written in the programming language Python. The goal of <em>pyspread</em> is to be the most pythonic spreadsheet.
+    </p>
+    <p>
+      <em>pyspread</em> expects Python expressions in its grid cells, which makes a spreadsheet specific language obsolete. Each cell returns a Python object that can be accessed from other cells. These objects can represent anything including lists or matrices.
+    </p>
+    <p>
+      <em>pyspread</em> is free software. It is released under the GPL v3 licence.
+    </p>
+  </description>
+  <categories>
+      <category>Office</category>
+      <category>Spreadsheet</category>
+  </categories>
+  <url type="homepage">https://pyspread.gitlab.io</url>
+  <url type="bugtracker">https://gitlab.com/pyspread/pyspread/issues</url>
+  <url type="help">https://pyspread.gitlab.io/docs.html</url>
+  <launchable type="desktop-id">io.gitlab.pyspread.pyspread.desktop</launchable>
+  
+  <releases>
+    <release version="2.0.2" date="2021-12-16">
+      <description>
+        <p>This is a bugfix release for pyspread 2.0 for Python 3.10 compatibility.</p>
+      </description>
+    </release>
+    <release version="2.0.1" date="2021-11-27" />
+    <release version="2.0" date="2021-11-19" />
+  </releases>
+  
+  <provides>
+    <mediatype>application/x-pyspread-spreadsheet</mediatype>
+    <mediatype>application/x-pyspread-bz-spreadsheet</mediatype>
+  </provides>
+  <content_rating type="oars-1.0" />
+  <screenshots>
+    <screenshot type="default">
+      <caption>Pyspread example with text, an image and a chart</caption>
+      <image>https://pyspread.gitlab.io/images/screenshot_sinus_large.png</image>
+    </screenshot>
+  </screenshots>
+</component>
diff --git a/pyspread/test/test_cli.py b/pyspread/test/test_cli.py
index d94482520a885f83d6a25b5b13f3b6fcac4118a4..d80ec96057cf70afc3a08a72dad0150fecf3049a 100755
--- a/pyspread/test/test_cli.py
+++ b/pyspread/test/test_cli.py
@@ -32,7 +32,7 @@ from contextlib import contextmanager
 from os.path import abspath, dirname, join
 import sys
 from unittest.mock import patch
-from pathlib import PosixPath
+from pathlib import Path, PosixPath
 
 import pytest
 
diff --git a/pyspread/test/test_grid.py b/pyspread/test/test_grid.py
index bb3815f317620a447594203a3d577d0101e5a0c5..46122602aff96ff830f15f051ff368e937b8d72c 100644
--- a/pyspread/test/test_grid.py
+++ b/pyspread/test/test_grid.py
@@ -35,7 +35,7 @@ import pytest
 
 from PyQt5.QtCore import QItemSelectionModel, QItemSelection
 from PyQt5.QtWidgets import QApplication, QAbstractItemView
-from PyQt5.QtGui import QFont, QColor, QFontDatabase, QFontInfo
+from PyQt5.QtGui import QFont, QColor
 
 
 PYSPREADPATH = abspath(join(dirname(__file__) + "/.."))
@@ -588,9 +588,9 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_text_renderer_pressed(True)
+        self.grid.on_text_renderer_pressed()
         assert self.cell_attributes[(2, 0, 0)]["renderer"] == "text"
-        self.grid.on_text_renderer_pressed(True)
+        self.grid.on_text_renderer_pressed()
         assert self.cell_attributes[(2, 0, 0)]["renderer"] == "text"
 
         self.grid.clearSelection()
@@ -600,9 +600,9 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_image_renderer_pressed(True)
+        self.grid.on_image_renderer_pressed()
         assert self.cell_attributes[(2, 0, 0)]["renderer"] == "image"
-        self.grid.on_text_renderer_pressed(True)
+        self.grid.on_text_renderer_pressed()
         assert self.cell_attributes[(2, 0, 0)]["renderer"] == "text"
 
         self.grid.clearSelection()
@@ -612,9 +612,9 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_markup_renderer_pressed(True)
+        self.grid.on_markup_renderer_pressed()
         assert self.cell_attributes[(2, 0, 0)]["renderer"] == "markup"
-        self.grid.on_text_renderer_pressed(True)
+        self.grid.on_text_renderer_pressed()
         assert self.cell_attributes[(2, 0, 0)]["renderer"] == "text"
 
         self.grid.clearSelection()
@@ -624,9 +624,9 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_matplotlib_renderer_pressed(True)
+        self.grid.on_matplotlib_renderer_pressed()
         assert self.cell_attributes[(2, 0, 0)]["renderer"] == "matplotlib"
-        self.grid.on_text_renderer_pressed(True)
+        self.grid.on_text_renderer_pressed()
         assert self.cell_attributes[(2, 0, 0)]["renderer"] == "text"
 
         self.grid.clearSelection()
@@ -648,7 +648,7 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_rotate_0(True)
+        self.grid.on_rotate_0()
         assert self.cell_attributes[(2, 0, 0)]["angle"] == 0.0
 
         self.grid.clearSelection()
@@ -658,9 +658,9 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_rotate_90(True)
+        self.grid.on_rotate_90()
         assert self.cell_attributes[(2, 0, 0)]["angle"] == 90.0
-        self.grid.on_rotate_0(True)
+        self.grid.on_rotate_0()
         assert self.cell_attributes[(2, 0, 0)]["angle"] == 0.0
 
         self.grid.clearSelection()
@@ -670,9 +670,9 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_rotate_180(True)
+        self.grid.on_rotate_180()
         assert self.cell_attributes[(2, 0, 0)]["angle"] == 180.0
-        self.grid.on_rotate_0(True)
+        self.grid.on_rotate_0()
         assert self.cell_attributes[(2, 0, 0)]["angle"] == 0.0
 
         self.grid.clearSelection()
@@ -682,9 +682,9 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_rotate_270(True)
+        self.grid.on_rotate_270()
         assert self.cell_attributes[(2, 0, 0)]["angle"] == 270.0
-        self.grid.on_rotate_0(True)
+        self.grid.on_rotate_0()
         assert self.cell_attributes[(2, 0, 0)]["angle"] == 0.0
 
         self.grid.clearSelection()
@@ -694,7 +694,7 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_justify_left(True)
+        self.grid.on_justify_left()
         assert self.cell_attributes[(2, 0, 0)]["justification"] \
             == "justify_left"
 
@@ -705,10 +705,10 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_justify_fill(True)
+        self.grid.on_justify_fill()
         assert self.cell_attributes[(2, 0, 0)]["justification"] \
             == "justify_fill"
-        self.grid.on_justify_left(True)
+        self.grid.on_justify_left()
         assert self.cell_attributes[(2, 0, 0)]["justification"] \
             == "justify_left"
 
@@ -719,10 +719,10 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_justify_center(True)
+        self.grid.on_justify_center()
         assert self.cell_attributes[(2, 0, 0)]["justification"] \
             == "justify_center"
-        self.grid.on_justify_left(True)
+        self.grid.on_justify_left()
         assert self.cell_attributes[(2, 0, 0)]["justification"] \
             == "justify_left"
 
@@ -733,10 +733,10 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_justify_right(True)
+        self.grid.on_justify_right()
         assert self.cell_attributes[(2, 0, 0)]["justification"] \
             == "justify_right"
-        self.grid.on_justify_left(True)
+        self.grid.on_justify_left()
         assert self.cell_attributes[(2, 0, 0)]["justification"] \
             == "justify_left"
 
@@ -747,7 +747,7 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_align_top(True)
+        self.grid.on_align_top()
         assert self.cell_attributes[(2, 0, 0)]["vertical_align"] == "align_top"
 
     def test_on_align_middle(self):
@@ -755,10 +755,10 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_align_middle(True)
+        self.grid.on_align_middle()
         assert self.cell_attributes[(2, 0, 0)]["vertical_align"] \
             == "align_center"
-        self.grid.on_align_top(True)
+        self.grid.on_align_top()
         assert self.cell_attributes[(2, 0, 0)]["vertical_align"] == "align_top"
 
     def test_on_align_bottom(self):
@@ -766,10 +766,10 @@ class TestGrid:
 
         self.grid.selectRow(2)
 
-        self.grid.on_align_bottom(True)
+        self.grid.on_align_bottom()
         assert self.cell_attributes[(2, 0, 0)]["vertical_align"] \
             == "align_bottom"
-        self.grid.on_align_top(True)
+        self.grid.on_align_top()
         assert self.cell_attributes[(2, 0, 0)]["vertical_align"] == "align_top"
 
     def test_on_text_color(self):
@@ -1190,10 +1190,6 @@ class TestGridTableModel:
 class TestGridCellDelegate:
     """Unit tests for GridCellDelegate in grid.py"""
 
-    pass
-
 
 class TestTableChoice:
     """Unit tests for TableChoice in grid.py"""
-
-    pass
diff --git a/pyspread/test/test_workflows.py b/pyspread/test/test_workflows.py
index f523595c14a5e2bb94bc79fae3183b3b863a2f6e..95a4e1c6effb1917c70085f07e561dd2b2e14deb 100755
--- a/pyspread/test/test_workflows.py
+++ b/pyspread/test/test_workflows.py
@@ -34,7 +34,7 @@ import sys
 
 import pytest
 
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import Qt, QItemSelectionModel
 from PyQt5.QtWidgets import QApplication
 
 try:
@@ -156,3 +156,45 @@ class TestWorkflows:
         if msg:
             assert str(testfile) in main_window.statusBar().currentMessage()
         tmpfile.remove()
+
+    def test_edit_sort_ascending(self):
+        """Unit test for test_edit_sort_ascending"""
+
+        main_window.grid.model.code_array[0, 0, 0] = "1"
+        main_window.grid.model.code_array[1, 0, 0] = "3"
+        main_window.grid.model.code_array[2, 0, 0] = "2"
+        main_window.grid.model.code_array[0, 1, 0] = "12"
+        main_window.grid.model.code_array[1, 1, 0] = "33"
+        main_window.grid.model.code_array[2, 1, 0] = "24"
+
+        for row in range(3):
+            for column in range(2):
+                index = main_window.grid.model.index(row, column)
+                main_window.grid.selectionModel().select(
+                    index, QItemSelectionModel.Select)
+
+        self.workflows.edit_sort_ascending()
+        assert main_window.grid.model.code_array((0, 0, 0)) == "1"
+        assert main_window.grid.model.code_array((1, 0, 0)) == "2"
+        assert main_window.grid.model.code_array((2, 1, 0)) == "33"
+
+    def test_edit_sort_descending(self):
+        """Unit test for test_edit_sort_descending"""
+
+        main_window.grid.model.code_array[0, 0, 0] = "1"
+        main_window.grid.model.code_array[1, 0, 0] = "3"
+        main_window.grid.model.code_array[2, 0, 0] = "2"
+        main_window.grid.model.code_array[0, 1, 0] = "12"
+        main_window.grid.model.code_array[1, 1, 0] = "33"
+        main_window.grid.model.code_array[2, 1, 0] = "24"
+
+        for row in range(3):
+            for column in range(2):
+                index = main_window.grid.model.index(row, column)
+                main_window.grid.selectionModel().select(
+                    index, QItemSelectionModel.Select)
+
+        self.workflows.edit_sort_descending()
+        assert main_window.grid.model.code_array((0, 0, 0)) == "3"
+        assert main_window.grid.model.code_array((1, 0, 0)) == "2"
+        assert main_window.grid.model.code_array((2, 1, 0)) == "12"
diff --git a/pyspread/toolbar.py b/pyspread/toolbar.py
index c242a097c157e9cedd25c3200cb062fe31952d7e..1e40a1354f74b2b896ab28616b37af233d80f67e 100644
--- a/pyspread/toolbar.py
+++ b/pyspread/toolbar.py
@@ -155,6 +155,11 @@ class MainToolBar(ToolBarBase):
 
         self.addSeparator()
 
+        self.addAction(actions.sort_ascending)
+        self.addAction(actions.sort_descending)
+
+        self.addSeparator()
+
         self.addAction(actions.toggle_spell_checker)
 
         self.addWidget(self.get_manager_button())
@@ -185,6 +190,10 @@ class MacroToolbar(ToolBarBase):
         if matplotlib_figure is not None:
             self.addAction(actions.insert_chart)
 
+        self.addSeparator()
+
+        self.addAction(actions.insert_sum)
+
         self.addWidget(self.get_manager_button())
 
 
diff --git a/pyspread/widgets.py b/pyspread/widgets.py
index dbf4485b80f5258091a78c4e821cf434c5d65b46..5e2a84fb9f1579841c8c04dae284edde0999ccc9 100644
--- a/pyspread/widgets.py
+++ b/pyspread/widgets.py
@@ -31,7 +31,10 @@
  * :class:`TextColorButton`
  * :class:`LineColorButton`
  * :class:`BackgroundColorButton`
+ * :class:`MenuComboBox`
+ * :class:`TypeMenuComboBox`
  * :class:`FontChoiceCombo`
+ * :class:`FontSizeCombo`
  * :class:`Widgets`
  * :class:`FindEditor`
  * :class:`CellButton`
@@ -57,11 +60,11 @@ from PyQt5.QtGui import QPalette, QColor, QFont, QIntValidator, QCursor, QIcon
 try:
     from pyspread.actions import Action
     from pyspread.icons import Icon
-    from pyspread.settings import WEB_URL
+    from pyspread.lib.csv import typehandlers, currencies
 except ImportError:
     from actions import Action
     from icons import Icon
-    from settings import WEB_URL
+    from lib.csv import typehandlers, currencies
 
 
 class MultiStateBitmapButton(QToolButton):
@@ -86,6 +89,8 @@ class MultiStateBitmapButton(QToolButton):
 
     @property
     def current_action_idx(self) -> int:
+        """Index of current action"""
+
         return self._current_action_idx
 
     @current_action_idx.setter
@@ -246,6 +251,7 @@ class ColorButton(QToolButton):
     colorChanged = pyqtSignal()
     title = "Select Color"
     default_color = None
+    _color = None
 
     def __init__(self, color: QColor, icon: QIcon = None,
                  max_size: QSize = QSize(28, 28)):
@@ -258,6 +264,8 @@ class ColorButton(QToolButton):
 
         super().__init__()
 
+        self.set_max_size(max_size)
+
         if icon is not None:
             self.setIcon(icon)
 
@@ -279,7 +287,7 @@ class ColorButton(QToolButton):
 
         """
 
-        if hasattr(self, "_color") and self._color == color:
+        if self._color == color:
             return
 
         self._color = color
@@ -319,8 +327,8 @@ class ColorButton(QToolButton):
         dlg.setOptions(QColorDialog.DontUseNativeDialog)
 
         pos = self.mapFromGlobal(QCursor.pos())
-        pos.setX(pos.x() + (self.rect().width() / 2))
-        pos.setY(pos.y() + (self.rect().height() / 2))
+        pos.setX(pos.x() + int(self.rect().width() / 2))
+        pos.setY(pos.y() + int(self.rect().height() / 2))
         dlg.move(self.mapToGlobal(pos))
 
         if dlg.exec_():
@@ -391,6 +399,94 @@ class BackgroundColorButton(ColorButton):
         self.default_color = self.palette().color(QPalette.Base)
 
 
+class MenuComboBox(QComboBox):
+    """ComboBox that uses a menu instead of a list"""
+
+    text_tpl = "{} ({})"  # Template for ComboBox item text
+
+    def __init__(self, items: dict):
+        """
+
+        :param items: Menu items
+
+        The dict items needs to be given in the following format:
+        {
+            "<label_1>": None,  # For leaf item
+            "<label_2>": {"<label_2.1>": None},  # For submenu
+         }
+
+        """
+
+        super().__init__()
+
+        self.action2index = {}  # Maps menu action to ComboBox item index
+
+        self.menu = QMenu(self)
+        self.menu.triggered.connect(self.on_menu_selected)
+
+        self._fill(items, self.menu)
+
+    def _fill(self, items: dict, menu: QMenu, parent_item_text: str = ""):
+        """Fills the menu and the combobox
+
+        :param items: Menu items for respective (sub)menu
+        :param items: Menu or submenu
+        :param parent_item_text: Text of parent item in submenu
+
+        """
+
+        for item in items:
+            if parent_item_text:
+                text = self.text_tpl.format(parent_item_text, item)
+            else:
+                text = item
+
+            if items[item] is None:
+                # Leaf item
+                action = menu.addAction(item)
+                self.addItem(text)
+                self.action2index[action] = self.count() - 1
+
+            else:
+                # Submenu
+                submenu = self.menu.addMenu(item)
+                self._fill(items[item], submenu, parent_item_text=text)
+
+    def showPopup(self):
+        """Show combo menu"""
+
+        rect = self.rect()
+
+        pos = QPoint(rect.x(), rect.y()+rect.height())
+        self.menu.popup(self.mapToGlobal(pos))
+
+    def hidePopup(self):
+        """Hide combo menu"""
+
+        self.menu.hideTearOffMenu()
+        super().hidePopup()
+
+    def on_menu_selected(self, action):
+        """Event handler for menu"""
+
+        self.setCurrentIndex(self.action2index[action])
+
+
+class TypeMenuComboBox(MenuComboBox):
+    """MenuComboBox that comprises types and currencies for CSV import"""
+
+    def __init__(self):
+        items = {}
+        for typehandler in typehandlers:
+            if typehandler == "Money":
+                items[typehandler] = dict((currency.code, None)
+                                          for currency in currencies)
+            else:
+                items[typehandler] = None
+
+        super().__init__(items)
+
+
 class FontChoiceCombo(QFontComboBox):
     """Font choice combo box"""
 
@@ -404,7 +500,7 @@ class FontChoiceCombo(QFontComboBox):
 
         """
 
-        super().__init__()
+        super().__init__(main_window)
 
         self.setMaximumWidth(150)
 
@@ -432,7 +528,7 @@ class FontChoiceCombo(QFontComboBox):
 
         return Icon.font_dialog
 
-    def on_font(self, font: QFont):
+    def on_font(self):
         """Font choice event handler"""
 
         self.fontChanged.emit()
@@ -468,6 +564,8 @@ class FontSizeCombo(QComboBox):
 
     @property
     def size(self) -> int:
+        """Size of current text"""
+
         return int(self.currentText())
 
     @size.setter
@@ -487,7 +585,7 @@ class FontSizeCombo(QComboBox):
 
         return Icon.font_dialog
 
-    def on_text(self, size: int):
+    def on_text(self):
         """Font size choice event handler"""
 
         try:
diff --git a/pyspread/workflows.py b/pyspread/workflows.py
index f0e702fa86fc034f2cedc55f03415edac0ae72da..5369639bd731fd04e89062e4abceedfdc56a93cd 100644
--- a/pyspread/workflows.py
+++ b/pyspread/workflows.py
@@ -31,6 +31,7 @@ from contextlib import contextmanager
 import csv
 from itertools import cycle
 import io
+import numpy
 import os.path
 from pathlib import Path
 from shutil import move
@@ -38,7 +39,8 @@ from tempfile import NamedTemporaryFile
 from typing import Iterable, Tuple
 
 from PyQt5.QtCore import (
-    Qt, QMimeData, QModelIndex, QBuffer, QRect, QRectF, QItemSelectionModel)
+    Qt, QMimeData, QModelIndex, QBuffer, QRect, QRectF, QItemSelectionModel,
+    QSize)
 from PyQt5.QtGui import QTextDocument, QImage, QPainter, QBrush, QPen
 from PyQt5.QtWidgets import (
     QApplication, QMessageBox, QInputDialog, QStyleOptionViewItem, QTableView,
@@ -56,13 +58,13 @@ except ImportError:
     matplotlib_figure = None
 
 try:
-    import pyspread.commands as commands
+    from pyspread import commands
     from pyspread.dialogs \
         import (DiscardChangesDialog, FileOpenDialog, GridShapeDialog,
                 FileSaveDialog, ImageFileOpenDialog, ChartDialog,
                 CellKeyDialog, FindDialog, ReplaceDialog, CsvFileImportDialog,
                 CsvImportDialog, CsvExportDialog, CsvExportAreaDialog,
-                FileExportDialog)
+                FileExportDialog, SvgExportAreaDialog, SinglePageArea)
     from pyspread.interfaces.pys import PysReader, PysWriter
     from pyspread.lib.attrdict import AttrDict
     from pyspread.lib.hashing import sign, verify
@@ -79,7 +81,7 @@ except ImportError:
                 FileSaveDialog, ImageFileOpenDialog, ChartDialog,
                 CellKeyDialog, FindDialog, ReplaceDialog, CsvFileImportDialog,
                 CsvImportDialog, CsvExportDialog, CsvExportAreaDialog,
-                FileExportDialog)
+                FileExportDialog, SvgExportAreaDialog, SinglePageArea)
     from interfaces.pys import PysReader, PysWriter
     from lib.attrdict import AttrDict
     from lib.hashing import sign, verify
@@ -144,7 +146,7 @@ class Workflows:
                 choice = DiscardChangesDialog(self.main_window).choice
                 if choice is None:
                     return
-                elif not choice:
+                if not choice:
                     # We try to save to a file
                     if self.file_save() is False:
                         # File could not be saved --> Abort
@@ -172,7 +174,7 @@ class Workflows:
         if filepath == Path.home():
             title = "pyspread"
         else:
-            title = "{filename} - pyspread".format(filename=filepath.name)
+            title = f"{filepath.name} - pyspread"
         self.main_window.setWindowTitle(title)
 
     @handle_changed_since_save
@@ -295,9 +297,12 @@ class Workflows:
         # Process events before showing the modal progress dialog
         QApplication.instance().processEvents()
 
+        # Change into file directory
+        os.chdir(filepath.parent)
+
         # Load file into grid
         title = "File open progress"
-        label = "Opening {}...".format(filepath.name)
+        label = f"Opening {filepath.name}..."
 
         try:
             with fopen(filepath, "rb") as infile:
@@ -312,7 +317,7 @@ class Workflows:
                     self.main_window.safe_mode = False
                     return
                 except ProgressDialogCanceled:
-                    msg = "File open stopped by user at line {}.".format(i)
+                    msg = f"File open stopped by user at line {i}."
                     self.main_window.statusBar().showMessage(msg)
                     grid.model.reset()
                     self.main_window.safe_mode = False
@@ -401,12 +406,12 @@ class Workflows:
             with open(filepath, "rb") as infile:
                 signature = sign(infile.read(), signature_key)
         except OSError as err:
-            msg = "Error signing file: {}".format(err)
+            msg = f"Error signing file: {err}"
             self.main_window.statusBar().showMessage(msg)
             return
 
         if signature is None or not signature:
-            msg = 'Error signing file. '
+            msg = 'Error signing file.'
             self.main_window.statusBar().showMessage(msg)
             return
 
@@ -438,7 +443,7 @@ class Workflows:
         # Save grid to temporary file
 
         title = "File save progress"
-        label = "Saving {}...".format(filepath.name)
+        label = f"Saving {filepath.name}..."
 
         with NamedTemporaryFile(delete=False) as tempfile:
             filename = tempfile.name
@@ -466,7 +471,7 @@ class Workflows:
                 return False
         try:
             if filepath.exists() and not os.access(filepath, os.W_OK):
-                raise PermissionError("No write access to {}".format(filepath))
+                raise PermissionError(f"No write access to {filepath}")
             move(filename, filepath)
 
         except OSError as err:
@@ -482,8 +487,7 @@ class Workflows:
         self.main_window.settings.last_file_output_path = filepath
 
         # Change the main window title
-        window_title = "{filename} - pyspread".format(filename=filepath.name)
-        self.main_window.setWindowTitle(window_title)
+        self.main_window.setWindowTitle(f"{filepath.name} - pyspread")
 
         # Add to file history
         self.main_window.settings.add_to_file_history(filepath.as_posix())
@@ -542,7 +546,7 @@ class Workflows:
         filelines = self.count_file_lines(filepath)
         if not filelines:  # May not be None or 0
             title = "CSV Import Error"
-            text = "File {} seems to be empty.".format(filepath)
+            text = f"File {filepath} seems to be empty."
             QMessageBox.warning(self.main_window, title, text)
             return
 
@@ -627,10 +631,14 @@ class Workflows:
         command = None
 
         title = "csv import progress"
-        label = "Importing {}...".format(filepath.name)
+        label = f"Importing {filepath.name}..."
 
         try:
-            with open(filepath, newline='', encoding='utf-8') as csvfile:
+            if hasattr(dialect, "encoding"):
+                __encoding = dialect.encoding
+            else:
+                __encoding = csv_dlg.csv_encoding
+            with open(filepath, newline='', encoding=__encoding) as csvfile:
                 try:
                     reader = csv_reader(csvfile, dialect)
                     for i, line in file_progress_gen(self.main_window, reader,
@@ -668,7 +676,7 @@ class Workflows:
 
                 except ProgressDialogCanceled:
                     title = "CSV Import Stopped"
-                    text = "Import stopped by user at line {}.".format(i)
+                    text = f"Import stopped by user at line {i}."
                     QMessageBox.warning(self.main_window, title, text)
                     return
 
@@ -685,13 +693,14 @@ class Workflows:
         with self.main_window.entry_line.disable_updates():
             with self.busy_cursor():
                 with self.prevent_updates():
-                    self.main_window.undo_stack.push(command)
+                    if command is not None:
+                        self.main_window.undo_stack.push(command)
 
     def file_export(self):
         """Export csv and svg files"""
 
         # Determine what filters ae available
-        filters_list = ["CSV (*.csv)"]
+        filters_list = ["CSV (*.csv)", "SVG (*.svg)"]
 
         grid = self.main_window.focused_grid
 
@@ -703,8 +712,7 @@ class Workflows:
         if isinstance(res, QImage):
             filters_list.append("JPG of current cell (*.jpg)")
 
-        if isinstance(res, QImage) \
-           or isinstance(res, matplotlib.figure.Figure):
+        if isinstance(res, (QImage, matplotlib.figure.Figure)):
             filters_list.append("PNG of current cell (*.png)")
 
         if isinstance(res, matplotlib.figure.Figure):
@@ -723,6 +731,13 @@ class Workflows:
             self._csv_export(filepath)
             return
 
+        if "SVG" in dial.selected_filter:
+            # Extend filepath suffix if needed
+            if filepath.suffix != dial.suffix:
+                filepath = filepath.with_suffix(dial.suffix)
+            self.svg_export(filepath)
+            return
+
         # Extend filepath suffix if needed
         if filepath.suffix != dial.suffix:
             filepath = filepath.with_suffix(dial.suffix)
@@ -773,6 +788,51 @@ class Workflows:
         except OSError as error:
             self.main_window.statusBar().showMessage(str(error))
 
+    def svg_export(self, filepath: Path, svg_area: SinglePageArea = None):
+        """Export to svg file filepath
+
+        :param filepath: Path of file to be exported
+        :param svg_area: Area of the grid to be exported
+
+        """
+
+        with self.print_zoom():
+            grid = self.main_window.grid
+
+            generator = QSvgGenerator()
+            generator.setFileName(str(filepath))
+
+            if svg_area is None:
+                # Get area for svg export
+                svg_area = SvgExportAreaDialog(self.main_window, grid,
+                                               title="Svg export area").area
+            if svg_area is None:
+                return
+
+            rows = self.get_paint_rows(svg_area.top, svg_area.bottom)
+            columns = self.get_paint_columns(svg_area.left, svg_area.right)
+            total_height = self.get_total_height(svg_area.top, svg_area.bottom)
+            total_width = self.get_total_width(svg_area.left, svg_area.right)
+
+            x_offset = grid.columnViewportPosition(0)
+            y_offset = grid.rowViewportPosition(0)
+
+            top_left_idx = grid.model.index(svg_area.top, svg_area.left)
+            top_left_visual_rect = grid.visualRect(top_left_idx)
+
+            generator.setSize(QSize(total_width, total_height))
+            paint_rect = QRectF(top_left_visual_rect.x() - x_offset,
+                                top_left_visual_rect.y() - y_offset,
+                                total_width,
+                                total_height)
+            generator.setViewBox(paint_rect)
+            option = QStyleOptionViewItem()
+
+            painter = QPainter(generator)
+            self.paint(painter, option, paint_rect, rows, columns)
+
+            painter.end()
+
     def _qimage_export(self, filepath: Path, file_format: str):
         """Export to png file filepath
 
@@ -787,7 +847,7 @@ class Workflows:
 
         try:
             if not qimage.save(filepath, file_format):
-                msg = "Could not save {}".format(filepath)
+                msg = f"Could not save {filepath}"
                 self.main_window.statusBar().showMessage(msg)
         except Exception as error:
             self.main_window.statusBar().showMessage(str(error))
@@ -948,7 +1008,8 @@ class Workflows:
                     if visual_rect.y() - y_offset < 0:
                         height += visual_rect.y() - y_offset
 
-                    option.rect = QRect(x, y, width, height)
+                    option.rect = QRect(int(x), int(y),
+                                        int(width), int(height))
                     option.rectf = QRectF(x, y, width, height)
 
                     max_width = max(max_width, x + width)
@@ -960,10 +1021,6 @@ class Workflows:
 
                     grid.itemDelegate().paint(painter, option, idx)
 
-        # Draw outer boundary rect
-        painter.setPen(QPen(QBrush(Qt.gray), 2))
-        painter.drawRect(paint_rect)
-
     @handle_changed_since_save
     def file_quit(self):
         """Program exit workflow"""
@@ -1019,7 +1076,10 @@ class Workflows:
             data.append([])
             for column in range(left, right + 1):
                 if (row, column) in selection:
-                    code = grid.model.code_array((row, column, table))
+                    try:
+                        code = grid.model.code_array((row, column, table))
+                    except IndexError:
+                        code = None
                     if code is None:
                         code = ""
                     code = code.replace("\n", "\u000C")  # Replace LF by FF
@@ -1178,7 +1238,9 @@ class Workflows:
                             command.mergeWith(cmd)
                 else:
                     break
-        undo_stack.push(command)
+
+        if command is not None:
+            undo_stack.push(command)
 
     def _paste_to_current(self, data: str):
         """Pastes data into grid starting from the current cell
@@ -1217,7 +1279,8 @@ class Workflows:
                         command.mergeWith(cmd)
                 else:
                     break
-        undo_stack.push(command)
+        if command is not None:
+            undo_stack.push(command)
 
     def edit_paste(self):
         """Edit -> Paste workflow
@@ -1261,9 +1324,9 @@ class Workflows:
         code = "\n".join(codelines)
 
         model = grid.model
-        description = "Insert svg image into cell {}".format(index)
+        description = f"Insert svg image into cell {index}"
 
-        grid.on_image_renderer_pressed(True)
+        grid.on_image_renderer_pressed()
         with self.main_window.entry_line.disable_updates():
             command = commands.SetCellCode(code, model, index, description)
             self.main_window.undo_stack.push(command)
@@ -1306,9 +1369,9 @@ class Workflows:
         code = "\n".join(code_lines)
 
         model = grid.model
-        description = "Insert image into cell {}".format(index)
+        description = f"Insert image into cell {index}"
 
-        grid.on_image_renderer_pressed(True)
+        grid.on_image_renderer_pressed()
         with self.main_window.entry_line.disable_updates():
             command = commands.SetCellCode(code, model, index, description)
             self.main_window.undo_stack.push(command)
@@ -1327,7 +1390,7 @@ class Workflows:
         items = [fmt for fmt in formats if any(m in fmt for m in mimetypes)]
         if not items:
             return
-        elif len(items) == 1:
+        if len(items) == 1:
             item = items[0]
         else:
             item, ok = QInputDialog.getItem(self.main_window, "Paste as",
@@ -1367,7 +1430,7 @@ class Workflows:
             html = mime_data.html()
             command = commands.SetCellCode(html, model, index, description)
             self.main_window.undo_stack.push(command)
-            grid.on_markup_renderer_pressed(True)
+            grid.on_markup_renderer_pressed()
 
         elif item == "text/plain":
             # Normal code
@@ -1561,46 +1624,117 @@ class Workflows:
 
         """
 
+        find_string = replace_dialog.search_text_editor.text()
         replace_string = replace_dialog.replace_text_editor.text()
 
+        word = replace_dialog.word_checkbox.isChecked()
+        case = replace_dialog.case_checkbox.isChecked()
+        regexp = replace_dialog.regex_checkbox.isChecked()
+
         command = None
 
-        replaced = []
-        next_match = 0, 0, 0
+        grid = self.main_window.focused_grid
+        code_array = grid.model.code_array
+        keys = code_array.keys()
+
+        matches = []
 
         with self.busy_cursor():
             with self.main_window.entry_line.disable_updates():
                 with self.prevent_updates():
-                    while True:
-                        # TODO: ABORT ON USER REQUEST
-                        find_string, next_match = \
-                            self._get_next_match(replace_dialog,
-                                                 start_key=(next_match[0]+1,
-                                                            next_match[1],
-                                                            next_match[2]))
-                        if not next_match or next_match in replaced:
-                            break
-                        replaced.append(next_match)
+                    for key in keys:
+                        code = code_array(key)
+
+                        if code_array.string_match(code, find_string, word,
+                                                   case, regexp) is not None:
+                            matches.append(key)
 
-                        msg = "Replace all {} by {}".format(find_string,
-                                                            replace_string)
-                        _command = self._get_replace_command(next_match,
+                    for match in matches:
+                        msg = f"Replace all {find_string} by {replace_string}"
+                        _command = self._get_replace_command(match,
                                                              find_string,
                                                              replace_string,
                                                              max_=-1,
                                                              description=msg)
-
                         if command is None:
                             command = _command
                         else:
                             command.mergeWith(_command)
 
+                    if command is not None:
+                        self.main_window.undo_stack.push(command)
+
+        msg = f"{find_string} replaced by {replace_string} in {len(matches)} "\
+              f"cell{'s' if len(matches) != 1 else ''}."
+        self.main_window.statusBar().showMessage(msg)
+
+    def _sort(self, ascending: bool = True):
+        """Edit -> Sort ascending
+
+        :param ascending: True for ascending sort, False for descending sort
+
+        """
+
+        grid = self.main_window.focused_grid
+        model = grid.model
+        table = grid.current[2]
+        selection = grid.selection
+
+        (top, left), (bottom, right) = selection.get_bbox()
+        if top == bottom:
+            return
+
+        data = grid.model.code_array[top:bottom+1, left:right+1, table].copy()
+        if ascending:
+            data[data == None] = numpy.inf  # `is` does not work here
+        else:
+            data[data == None] = -numpy.inf  # `is` does not work here
+
+        try:
+            if ascending:
+                sorted_idx = data[:, grid.current[1]-left].argsort()
+            else:
+                sorted_idx = data[:, grid.current[1]-left].argsort()[::-1]
+        except TypeError as err:
+            msg = f"Could not sort selection: {err}"
+            self.main_window.statusBar().showMessage(msg)
+            return
+
+        old_code = {}
+        for key in selection.cell_generator(model.shape, table):
+            old_code[key] = grid.model.code_array(key)
+
+        command = None
+        if ascending:
+            description = f"Sort {grid.selection} ascending"
+        else:
+            description = f"Sort {grid.selection} descending"
+
+        for row, column in selection.cell_generator(model.shape):
+            code = old_code[(sorted_idx[row-top]+top, column, table)]
+            index = model.index(row, column)
+            _command = commands.SetCellCode(code, model, index, description)
+            if command is None:
+                command = _command
+            else:
+                command.mergeWith(_command)
+
+        if command is not None:
             self.main_window.undo_stack.push(command)
 
-        msg_tpl = "{} replaced by {} in {} cells."
-        msg = msg_tpl.format(find_string, replace_string, len(replaced))
+        msg = "Selection sorted."
         self.main_window.statusBar().showMessage(msg)
 
+    def edit_sort_ascending(self):
+        """Edit -> Sort ascending"""
+
+        self._sort()
+
+    def edit_sort_descending(self):
+        """Edit -> Sort descending"""
+
+        self._sort(ascending=False)
+
     def edit_resize(self):
         """Edit -> Resize workflow"""
 
@@ -1632,7 +1766,7 @@ class Workflows:
 
         grid.current = 0, 0, 0
 
-        description = "Resize grid to {}".format(shape)
+        description = f"Resize grid to {shape}"
 
         with self.main_window.entry_line.disable_updates():
             command = commands.SetGridSize(grid, old_shape, shape, description)
@@ -1688,8 +1822,7 @@ class Workflows:
         selection = grid.selection
 
         # Format content is shifted so that the top left corner is 0,0
-        (top, left), (bottom, right) = \
-            selection.get_grid_bbox(grid.model.shape)
+        (top, left), (_, _) = selection.get_grid_bbox(grid.model.shape)
 
         table_cell_attributes = cell_attributes.for_table(grid.table)
         for __selection, _, attrs in table_cell_attributes:
@@ -1755,7 +1888,29 @@ class Workflows:
                                                  selected_idx, description)
                 self.main_window.undo_stack.push(command)
 
-    # Macro menu
+    # Macro menufilepath
+
+    def _read_svg_str(self, filepath, encoding):
+        """Returns svg string from filepath
+
+        :param filepath: Path of SVG file to read
+
+        """
+
+        try:
+            with open(filepath, "r", encoding=encoding) as svgfile:
+                return svgfile.read()
+        except UnicodeError:
+            encoding, ok = QInputDialog().getItem(
+                self, f"{filepath} not encoded in utf-8",
+                f"Encoding of {filepath}",
+                self.main_window.settings.encodings)
+            if ok:
+                return self._read_svg_str(filepath, encoding)
+        except OSError as err:
+            msg_tpl = "Error opening file {filepath}: {err}."
+            msg = msg_tpl.format(filepath=filepath, err=err)
+            self.main_window.statusBar().showMessage(msg)
 
     def macro_insert_image(self):
         """Insert image workflow"""
@@ -1773,14 +1928,10 @@ class Workflows:
         grid.selectionModel().select(index, QItemSelectionModel.Select)
 
         if filepath.suffix == ".svg":
-            try:
-                with open(filepath, "r", encoding='utf-8') as svgfile:
-                    svg = svgfile.read()
-            except OSError as err:
-                msg_tpl = "Error opening file {filepath}: {err}."
-                msg = msg_tpl.format(filepath=filepath, err=err)
-                self.main_window.statusBar().showMessage(msg)
+            svg = self._read_svg_str(filepath, encoding='utf-8')
+            if not svg:
                 return
+
             self._paste_svg(svg, index)
         else:
             try:
@@ -1822,11 +1973,42 @@ class Workflows:
             index = grid.currentIndex()
             grid.clearSelection()
             grid.selectionModel().select(index, QItemSelectionModel.Select)
-            grid.on_matplotlib_renderer_pressed(True)
+            grid.on_matplotlib_renderer_pressed()
 
-            description = "Insert chart into cell {}".format(index)
+            description = f"Insert chart into cell {index}"
             command = commands.SetCellCode(code, model, index, description)
 
             self.main_window.undo_stack.push(command)
 
         self.cell2dialog.pop(current)
+
+    def macro_insert_sum(self):
+        """Sum up selection area
+
+        The sum is inserted into the cell below the bottom right cell of the
+        selection.
+
+        """
+
+        grid = self.main_window.focused_grid
+        selection = grid.selection
+        shape = grid.model.shape
+
+        (top, left), (bottom, right) = selection.get_grid_bbox(shape)
+
+        if bottom >= shape[0] - 1:
+            self.main_window.statusBar().showMessage(
+                f"ValueError: Target cell is beyond grid limits")
+            return
+
+        key = bottom + 1, right, grid.table
+
+        code = f"numpy.sum(eval({repr(selection)}" + \
+               f".get_absolute_access_string({shape}, Z)))"
+
+        grid.current = key
+        index = grid.currentIndex()
+        description = f"Insert sum of {selection} into cell {key}"
+        command = commands.SetCellCode(code, grid.model, index, description)
+
+        self.main_window.undo_stack.push(command)
diff --git a/setup.py b/setup.py
index 4f0b58ad25e69b66b43c21948af5976baae941d0..2396f4f4b701b508ad28403428fa1df9e5d748ec 100644
--- a/setup.py
+++ b/setup.py
@@ -68,6 +68,7 @@ setup(
         'pyenchant': ['pyenchant (>=1.1)'],
         'pip': ['pip (>=18)'],
         'python-dateutil': ['python-dateutil (>=2.7.0)'],
+        'py-moneyed': ['py-moneyed (>=2.0)'],
     },
     classifiers=[
         'Development Status :: 5 - Production/Stable',
@@ -83,6 +84,7 @@ setup(
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
         'Topic :: Office/Business :: Financial :: Spreadsheet',
         'Topic :: Scientific/Engineering',
     ],