[hamradio-commits] [pyqso] 01/04: Imported Upstream version 0.3

Iain R. Learmonth irl at moszumanska.debian.org
Fri Jul 8 01:57:07 UTC 2016


This is an automated email from the git hooks/post-receive script.

irl pushed a commit to branch master
in repository pyqso.

commit 8e76632f9ca23cdb8b90fffba012f0e40a83aba1
Author: Iain R. Learmonth <irl at debian.org>
Date:   Fri Jul 8 02:51:12 2016 +0100

    Imported Upstream version 0.3
---
 .travis.yml                        |   14 +-
 CHANGELOG.md                       |   21 +
 Makefile                           |   10 +-
 README.md                          |   31 +-
 bin/pyqso                          |  253 +++--
 docs/Makefile                      |    2 +-
 docs/source/conf.py                |   30 +-
 docs/source/getting_started.rst    |    2 +-
 docs/source/images/dx_cluster.png  |  Bin 62219 -> 39147 bytes
 docs/source/images/edit_record.png |  Bin 44291 -> 47239 bytes
 docs/source/images/logbook.png     |  Bin 140698 -> 125221 bytes
 docs/source/images/summary.png     |  Bin 0 -> 52682 bytes
 docs/source/index.rst              |    3 +-
 docs/source/introduction.rst       |   16 +-
 docs/source/log_management.rst     |    2 +-
 docs/source/preferences.rst        |   33 +-
 docs/source/record_management.rst  |    4 +-
 docs/source/shortcuts.rst          |   16 +
 docs/source/toolbox.rst            |    8 +-
 pyqso/adif.py                      | 1079 ++++++++++--------
 pyqso/auxiliary_dialogs.py         |   74 +-
 pyqso/awards.py                    |  172 +--
 pyqso/callsign_lookup.py           |  548 ++++++---
 pyqso/dx_cluster.py                |  478 +++++---
 pyqso/grey_line.py                 |  131 ++-
 pyqso/log.py                       |  774 +++++++------
 pyqso/log_name_dialog.py           |   67 +-
 pyqso/logbook.py                   | 2195 ++++++++++++++++++++----------------
 pyqso/menu.py                      |  512 +++++----
 pyqso/preferences_dialog.py        |  887 +++++++++------
 pyqso/record_dialog.py             | 1143 ++++++++++---------
 pyqso/telnet_connection_dialog.py  |  132 ++-
 pyqso/toolbar.py                   |  226 ++--
 pyqso/toolbox.py                   |   63 +-
 setup.py                           |   25 +-
 tox.ini                            |    3 +
 36 files changed, 5114 insertions(+), 3840 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index b43b4be..86b71a8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,17 @@
+sudo: required
+dist: trusty
+
 language: python
 
 python:
-  - "2.7"
-
+  - "3.4"
+  
 virtualenv:
   system_site_packages: true
 
 before_install:
  - sudo apt-get update -qq
- - sudo apt-get install -y python2.7 gir1.2-gtk-3.0 python-gi-cairo python-mpltoolkits.basemap python-numpy python-matplotlib python-libhamlib2 python-sphinx
+ - sudo apt-get install -yq xvfb gir1.2-gtk-3.0 python3-gi-cairo python-mpltoolkits.basemap python3-numpy python3-matplotlib python3-sphinx python-libhamlib2 python3-flake8
  - "export DISPLAY=:99.0"
  - "sh -e /etc/init.d/xvfb start"
 
@@ -17,8 +20,11 @@ install:
 
 before_script:
   - export PYTHONPATH=`pwd`:$PYTHONPATH
+  - echo $PYTHONPATH
+  - flake8 pyqso
+  - flake8 bin
   
 script:
-  - make unittest
+  - make test
   - make docs
   - sudo make clean
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f23c39..d0a0ff9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,24 @@
 # Change Log
 
+## [0.3] - 2016-05-28
+### Added
+- Support for callsign lookups using the HamQTH.com database.
+- Added a table of keyboard shortcuts to the documentation.
+- More helpful messages regarding missing dependencies.
+- Added the option of merging the COMMENT field with the NOTES field when importing records from an ADIF file.
+- Bookmarking of Telnet-based DX cluster servers.
+
+### Changed
+- Ported the codebase over to Python 3 using 2to3 (thanks to Neil Johnson).
+- The Summary page now also contains the total number of QSOs in the logbook.
+- Improvements to docstrings.
+- Various code cleanups (thanks to András Veres-Szentkirályi).
+- Brought the list of valid modes up-to-date.
+- Updated the list of bands and their frequency ranges.
+- Configuration files are now written to ~/.config to keep the user's home directory uncluttered.
+- The codebase is now compliant with the PEP 8 Python coding conventions (except for E501,F403,E226,E402,W503).
+- Updated the documentation.
+
 ## [0.2] - 2015-03-07
 ### Added
 - Travis CI configuration file for automated building and testing.
@@ -46,5 +65,7 @@
 - QSO filtering and sorting.
 - Duplicate record removal.
 
+[Unreleased]: https://github.com/ctjacobs/pyqso/compare/v0.3...master
+[0.3]: https://github.com/ctjacobs/pyqso/compare/v0.2...v0.3
 [0.2]: https://github.com/ctjacobs/pyqso/compare/v0.1...v0.2
 [0.1]: https://github.com/ctjacobs/pyqso/compare/v0.1b...v0.1
diff --git a/Makefile b/Makefile
index bfe6dac..74a08ba 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 #!/bin/sh
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,21 +17,21 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-.PHONY: input clean install docs unittest
+.PHONY: input clean install docs test
 
 input: 	clean install docs
 
 install:
 	@echo "*** Installing PyQSO"
-	python setup.py install
+	python3 setup.py install
 
 docs:
 	@echo "*** Building the documentation"
 	cd docs; make html; cd ..
 
-unittest:
+test:
 	@echo "*** Running the unit tests"
-	python -m unittest discover --start-directory=pyqso --pattern=*.py --verbose
+	python3 -m unittest discover --start-directory=pyqso --pattern=*.py --verbose
 
 clean:
 	@echo "*** Cleaning docs directory"
diff --git a/README.md b/README.md
index 2b10795..52ee443 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-    Copyright (C) 2013 Christian T. Jacobs.
+    Copyright (C) 2013-2016 Christian T. Jacobs.
 
     This file is part of PyQSO.
 
@@ -36,7 +36,7 @@ Note: 'sudo' may be needed for this. Once installed, the following command will
 
 Alternatively, PyQSO can be run (without installing) with:
 
-   `python bin/pyqso`
+   `python3 bin/pyqso`
 
 from PyQSO's base directory.
 
@@ -49,7 +49,7 @@ The PyQSO documentation is stored in the `docs` directory. It can be built with
 
 which will produce an HTML version of the documentation in `docs/build/html` that can be opened in a web browser.
 
-Alternatively, a ready-built version of the PyQSO documentation can be found on [Read the Docs](http://pyqso.readthedocs.org/en/latest/).
+Alternatively, a ready-built version of the PyQSO documentation can be found on [Read the Docs](http://pyqso.readthedocs.io/).
 
 Dependencies
 ------------
@@ -57,25 +57,24 @@ Dependencies
 PyQSO depends on the following Debian packages:
 
 * gir1.2-gtk-3.0
-* python2.7
-* python-gi-cairo (for log printing purposes)
+* python3
+* python3-gi-cairo (for log printing purposes)
 
-The following extra packages are necessary to enable the grey line tool:
+The following extra packages are necessary to enable the grey line tool and the plotting of logbook statistics:
 
-* python-mpltoolkits.basemap
-* python-numpy
-* python-matplotlib (version 1.3.0 or later)
-
-The following extra package is necessary to enable Hamlib support:
-
-* python-libhamlib2
+* python3-matplotlib (version 1.3.0 or later)
+* python3-mpltoolkits.basemap
+* python3-numpy
+* libxcb-render0-dev
+* python3-cairocffi
 
 The following extra package is necessary to build the documentation:
 
-* python-sphinx
+* python3-sphinx
+
+There currently does not exist a Python 3-compatible Debian package for [Hamlib](http://www.hamlib.org). This library must be built manually to enable Hamlib support.
 
 Contact
 -------
 
-If you have any comments or questions about PyQSO, please send them via email to <c.jacobs10 at imperial.ac.uk>.
-
+If you have any comments or questions about PyQSO, please send them via email to Christian Jacobs (2E0ICL) at <christian at christianjacobs.uk>.
diff --git a/bin/pyqso b/bin/pyqso
index eaff9f2..88d553a 100755
--- a/bin/pyqso
+++ b/bin/pyqso
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2012 Christian T. Jacobs.
+#    Copyright (C) 2012-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,9 +17,9 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject, Gdk, GdkPixbuf
+from gi.repository import Gtk, GdkPixbuf
 import argparse
-import ConfigParser
+import configparser
 import os
 import os.path
 import sys
@@ -31,7 +31,8 @@ sys.path.insert(0, pyqso_path)
 
 import logging
 logging.basicConfig(level=logging.INFO)
-      
+logging.info("PyQSO version 0.3")
+
 # PyQSO modules
 from pyqso.adif import *
 from pyqso.logbook import *
@@ -39,83 +40,96 @@ from pyqso.menu import *
 from pyqso.toolbar import *
 from pyqso.toolbox import *
 from pyqso.preferences_dialog import *
-   
+
+
 class PyQSO(Gtk.Window):
-   """ The PyQSO application class. """
-   
-   def __init__(self, logbook_path):
-      """ Set up the main (root) window, start the event loop, and open a logbook (if the logbook's path is specified by the user in the command line). """
-         
-      # Call the constructor of the super class (Gtk.Window)
-      Gtk.Window.__init__(self, title="PyQSO 0.2")
-
-      # Get any application-specific preferences from the configuration file      
-      config = ConfigParser.ConfigParser()
-      # Check that the configuration file actually exists (and is readable)
-      # otherwise, we will resort to the defaults.
-      have_config = (config.read(os.path.expanduser("~/.pyqso.ini")) != [])
-
-      self.set_size_request(800, 600) # Default to an 800 x 600 resolution.
-      self.set_position(Gtk.WindowPosition.CENTER)
-      possible_icon_paths = [os.path.join(pyqso_path, "icons", "log_64x64.png")]
-      for icon_path in possible_icon_paths:
-         try:
-            self.set_icon_from_file(icon_path)
-         except Exception, error:
-            print error.message
-
-      # Kills the application if the close button is clicked on the main window itself. 
-      self.connect("delete-event", Gtk.main_quit)
-      
-      vbox_outer = Gtk.VBox()
-      self.add(vbox_outer)
-
-      self.statusbar = Gtk.Statusbar()
-      context_id = self.statusbar.get_context_id("Status")
-      self.statusbar.push(context_id, "No logbook is currently open.")
-      
-      # Create a Logbook so we can add/remove/edit logs and records,
-      # once connected to the SQLite database.
-      self.logbook = Logbook(self)
-      self.logbook.set_scrollable(True)
-
-      self.toolbox = Toolbox(self)
-
-      # Set up menu and tool bars
-      # These classes depend on the Logbook and Toolbox class,
-      # so pack the logbook and toolbox after the menu and toolbar.
-      self.menu = Menu(self)
-      self.toolbar = Toolbar(self)
-
-      vbox_outer.pack_start(self.menu, False, False, 0)
-      vbox_outer.pack_start(self.toolbar, False, False, 0)
-      vbox_outer.pack_start(self.logbook, True, True, 0)
-      vbox_outer.pack_start(self.toolbox, True, True, 0)
-      vbox_outer.pack_start(self.statusbar, False, False, 0)
-
-      self.show_all()
-
-      if(have_config):
-         if(config.get("general", "show_toolbox") == "False"):
+
+    """ The PyQSO application class. """
+
+    def __init__(self, logbook_path=None):
+        """ Set up the main (root) window, start the event loop, and open a logbook (if the logbook's path is specified by the user in the command line).
+
+        :arg str logbook_path: An optional argument containing the path of the logbook file to open. If no value is provided, this defaults to None and no logbook is opened.
+        """
+
+        # Call the constructor of the super class (Gtk.Window)
+        Gtk.Window.__init__(self, title="PyQSO")
+
+        # Check that the directory for holding PyQSO configuration files exists. If it doesn't, create it now.
+        try:
+            os.makedirs(os.path.expanduser('~/.config/pyqso'), exist_ok=True)
+        except Exception as e:
+            logging.error("An error occurred whilst creating a directory for PyQSO configuration files. Try creating the directory '~/.config/pyqso' manually.")
+            logging.exception(e)
+
+        # Get any application-specific preferences from the configuration file
+        config = configparser.ConfigParser()
+
+        # Check that the configuration file actually exists (and is readable)
+        # otherwise, we will resort to the defaults.
+        have_config = (config.read(os.path.expanduser("~/.config/pyqso/preferences.ini")) != [])
+
+        self.set_size_request(800, 600)  # Default to an 800 x 600 resolution.
+        self.set_position(Gtk.WindowPosition.CENTER)
+        possible_icon_paths = [os.path.join(pyqso_path, "icons", "log_64x64.png")]
+        for icon_path in possible_icon_paths:
+            try:
+                self.set_icon_from_file(icon_path)
+            except Exception as error:
+                print(error.message)
+
+        # Kills the application if the close button is clicked on the main window itself.
+        self.connect("delete-event", Gtk.main_quit)
+
+        vbox_outer = Gtk.VBox()
+        self.add(vbox_outer)
+
+        self.statusbar = Gtk.Statusbar()
+        context_id = self.statusbar.get_context_id("Status")
+        self.statusbar.push(context_id, "No logbook is currently open.")
+
+        # Create a Logbook so we can add/remove/edit logs and records,
+        # once connected to the SQLite database.
+        self.logbook = Logbook(self)
+        self.logbook.set_scrollable(True)
+
+        self.toolbox = Toolbox(self)
+
+        # Set up menu and tool bars
+        # These classes depend on the Logbook and Toolbox class,
+        # so pack the logbook and toolbox after the menu and toolbar.
+        self.menu = Menu(self)
+        self.toolbar = Toolbar(self)
+
+        vbox_outer.pack_start(self.menu, False, False, 0)
+        vbox_outer.pack_start(self.toolbar, False, False, 0)
+        vbox_outer.pack_start(self.logbook, True, True, 0)
+        vbox_outer.pack_start(self.toolbox, True, True, 0)
+        vbox_outer.pack_start(self.statusbar, False, False, 0)
+
+        self.show_all()
+
+        if(have_config):
+            if(config.get("general", "show_toolbox") == "False"):
+                self.toolbox.toggle_visible_callback()
+        else:
+            # Hide the Toolbox by default
             self.toolbox.toggle_visible_callback()
-      else:            
-         # Hide the Toolbox by default
-         self.toolbox.toggle_visible_callback()
-
-      if(logbook_path is not None):
-         self.logbook.open(widget=None, path=logbook_path)
-
-      return
-
-   def show_about(self, widget):
-      """ Show the About dialog, which includes license information. This method returns None after the user destroys the dialog. """
-      about = Gtk.AboutDialog()
-      about.set_modal(True)
-      about.set_transient_for(parent=self)
-      about.set_program_name("PyQSO")
-      about.set_version("0.2")
-      about.set_authors(["Christian T. Jacobs (M6RDG)"])
-      about.set_license("""This program is free software: you can redistribute it and/or modify
+
+        if(logbook_path is not None):
+            self.logbook.open(widget=None, path=logbook_path)
+
+        return
+
+    def show_about(self, widget):
+        """ Show the About dialog, which includes license information. """
+        about = Gtk.AboutDialog()
+        about.set_modal(True)
+        about.set_transient_for(parent=self)
+        about.set_program_name("PyQSO")
+        about.set_version("0.3")
+        about.set_authors(["Christian T. Jacobs (2E0ICL)"])
+        about.set_license("""This program 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.
@@ -127,45 +141,44 @@ GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.""")
-      about.set_comments("PyQSO: A contact logging tool for amateur radio operators.")
-      possible_icon_paths = [os.path.join(pyqso_path, "icons", "log_64x64.png")]
-      for icon_path in possible_icon_paths:
-         try:
-            about.set_logo(GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_path, 64, 64, False))
-         except Exception, error:
-            print error.message
-      about.run()
-      about.destroy()
-      return      
-
-   def show_preferences(self, widget):
-      """ Show the Preferences dialog. Any changes made by the user after clicking the 'Ok' button are saved in the .cfg file. This method returns None after the user destroys the dialog. """
-      preferences = PreferencesDialog(self)
-      response = preferences.run()
-      if(response == Gtk.ResponseType.OK):
-         preferences.commit()
-      preferences.destroy()
-      return
+        about.set_comments("PyQSO: A contact logging tool for amateur radio operators.")
+        possible_icon_paths = [os.path.join(pyqso_path, "icons", "log_64x64.png")]
+        for icon_path in possible_icon_paths:
+            try:
+                about.set_logo(GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_path, 64, 64, False))
+            except Exception as error:
+                print(error.message)
+        about.run()
+        about.destroy()
+        return
+
+    def show_preferences(self, widget):
+        """ Show the Preferences dialog. Any changes made by the user after clicking the 'Ok' button are saved in the .cfg file. """
+        preferences = PreferencesDialog(self)
+        response = preferences.run()
+        if(response == Gtk.ResponseType.OK):
+            preferences.commit()
+        preferences.destroy()
+        return
 
 if(__name__ == "__main__"):
-   # Get any command line arguments
-   parser = argparse.ArgumentParser(prog="pyqso")
-   parser.add_argument("-d", "--debug", action="store_true", default=False, help="Enable debugging. All debugging messages will be written to pyqso.debug.")
-   parser.add_argument("-l", "--logbook", action="store", type=str, metavar="/path/to/my_logbook_file.db", default=None, help="Path to a Logbook file. If this file does not already exist, then it will be created.")
-   args = parser.parse_args()
-
-   # Output debugging messages to a file
-   if(args.debug):
-      # Get the root logger
-      logger = logging.getLogger()
-      logger.setLevel(logging.DEBUG)
-      # Add a file handler
-      handler = logging.FileHandler("pyqso.debug", mode="w")
-      formatter = logging.Formatter(fmt="%(asctime)s %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
-      handler.setFormatter(formatter)
-      logger.addHandler(handler)
-      
-   signal.signal(signal.SIGINT, signal.SIG_DFL) # Exit PyQSO if a SIGINT signal is captured.
-   application = PyQSO(args.logbook) # Populate the main window and show it
-   Gtk.main() # Start up the event loop!
-
+    # Get any command line arguments
+    parser = argparse.ArgumentParser(prog="pyqso")
+    parser.add_argument("-d", "--debug", action="store_true", default=False, help="Enable debugging. All debugging messages will be written to pyqso.debug.")
+    parser.add_argument("-l", "--logbook", action="store", type=str, metavar="/path/to/my_logbook_file.db", default=None, help="Path to a Logbook file. If this file does not already exist, then it will be created.")
+    args = parser.parse_args()
+
+    # Output debugging messages to a file
+    if(args.debug):
+        # Get the root logger
+        logger = logging.getLogger()
+        logger.setLevel(logging.DEBUG)
+        # Add a file handler
+        handler = logging.FileHandler("pyqso.debug", mode="w")
+        formatter = logging.Formatter(fmt="%(asctime)s %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
+        handler.setFormatter(formatter)
+        logger.addHandler(handler)
+
+    signal.signal(signal.SIGINT, signal.SIG_DFL)  # Exit PyQSO if a SIGINT signal is captured.
+    application = PyQSO(args.logbook)  # Populate the main window and show it
+    Gtk.main()  # Start up the event loop!
diff --git a/docs/Makefile b/docs/Makefile
index ea0fe8b..d572ecf 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -19,7 +19,7 @@ ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) sou
 # the i18n builder cannot share the environment and doctrees with the others
 I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
 
-.PHONY: help clean aptdoc html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+.PHONY: help clean apidoc html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
 
 help:
 	@echo "Please use \`make <target>' where <target> is one of"
diff --git a/docs/source/conf.py b/docs/source/conf.py
index b2a7956..1db6649 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -51,16 +51,16 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'PyQSO'
-copyright = u'2015, Christian T. Jacobs'
+copyright = u'2015-2016, Christian T. Jacobs (2E0ICL)'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
 # The short X.Y version.
-version = '0.2'
+version = '0.3'
 # The full version, including alpha/beta/rc tags.
-release = '0.2'
+release = '0.3'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
@@ -189,22 +189,22 @@ htmlhelp_basename = 'PyQSOdoc'
 # -- Options for LaTeX output ---------------------------------------------
 
 latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
+    # The paper size ('letterpaper' or 'a4paper').
+    #'papersize': 'letterpaper',
 
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
+    # The font size ('10pt', '11pt' or '12pt').
+    #'pointsize': '10pt',
 
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
+    # Additional stuff for the LaTeX preamble.
+    #'preamble': '',
 }
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-  ('index', 'PyQSO.tex', u'PyQSO Documentation',
-   u'Christian T. Jacobs', 'manual'),
+    ('index', 'PyQSO.tex', u'PyQSO Documentation',
+     u'Christian T. Jacobs (2E0ICL)', 'manual'),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
@@ -234,7 +234,7 @@ latex_documents = [
 # (source start file, name, description, authors, manual section).
 man_pages = [
     ('index', 'pyqso', u'PyQSO Documentation',
-     [u'Christian T. Jacobs'], 1)
+     [u'Christian T. Jacobs (2E0ICL)'], 1)
 ]
 
 # If true, show URL addresses after external links.
@@ -247,9 +247,9 @@ man_pages = [
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-  ('index', 'PyQSO', u'PyQSO Documentation',
-   u'Christian T. Jacobs', 'PyQSO', 'A contact logging tool for amateur radio operators.',
-   'Miscellaneous'),
+    ('index', 'PyQSO', u'PyQSO Documentation',
+     u'Christian T. Jacobs (2E0ICL)', 'PyQSO', 'A contact logging tool for amateur radio operators.',
+     'Miscellaneous'),
 ]
 
 # Documents to append as an appendix to all manuals.
diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst
index ef9e0ad..cc1c7ad 100644
--- a/docs/source/getting_started.rst
+++ b/docs/source/getting_started.rst
@@ -38,7 +38,7 @@ Alternatively, PyQSO can be run (without installing) with:
 
 .. code-block:: bash
 
-   python bin/pyqso
+   python3 bin/pyqso
 
 from PyQSO's base directory.
 
diff --git a/docs/source/images/dx_cluster.png b/docs/source/images/dx_cluster.png
index 1623402..d3643f9 100644
Binary files a/docs/source/images/dx_cluster.png and b/docs/source/images/dx_cluster.png differ
diff --git a/docs/source/images/edit_record.png b/docs/source/images/edit_record.png
index d74eba0..068a2b5 100644
Binary files a/docs/source/images/edit_record.png and b/docs/source/images/edit_record.png differ
diff --git a/docs/source/images/logbook.png b/docs/source/images/logbook.png
index 77c6d6b..02a0cce 100644
Binary files a/docs/source/images/logbook.png and b/docs/source/images/logbook.png differ
diff --git a/docs/source/images/summary.png b/docs/source/images/summary.png
new file mode 100644
index 0000000..63128ef
Binary files /dev/null and b/docs/source/images/summary.png differ
diff --git a/docs/source/index.rst b/docs/source/index.rst
index a8539f2..46761fa 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -17,4 +17,5 @@ Contents:
    record_management
    toolbox
    preferences
-
+   shortcuts
+   pyqso
diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst
index 8e9498b..b90173c 100644
--- a/docs/source/introduction.rst
+++ b/docs/source/introduction.rst
@@ -4,7 +4,7 @@ Introduction
 Overview
 --------
 
-PyQSO is a logging tool for amateur radio operators. It provides a
+`PyQSO <http://christianjacobs.uk/pyqso>`_ is a logging tool for amateur radio operators. It provides a
 simple graphical interface through which users can manage information
 about the contacts/QSOs they make with other operators on the air. All
 information is stored in a light-weight SQL database. Other key features
@@ -15,8 +15,7 @@ include:
 
 -  Import and export logs in ADIF format.
 
--  Perform callsign lookups and auto-fill data fields using the qrz.com
-   database.
+-  Perform callsign lookups and auto-fill data fields using the qrz.com and hamqth.com online databases.
 
 -  Sort the logs by individual fields.
 
@@ -35,7 +34,7 @@ include:
 
 -  Basic support for the Hamlib library.
 
-The source code for PyQSO is available for download from the `GitHub repository <https://github.com/ctjacobs/pyqso>`_.
+The source code for PyQSO, written in Python (version 3.x), is available for download from the `GitHub repository <https://github.com/ctjacobs/pyqso>`_.
 
 Data storage model
 ------------------
@@ -58,10 +57,15 @@ log.
 Licensing
 ---------
 
-PyQSO is free software, released under the GNU General Public License. Please see the file called ``COPYING`` for more information.
+PyQSO is free software, released under the `GNU General Public License <http://www.gnu.org/licenses/gpl-3.0.en.html>`_. Please see the file called ``COPYING`` for more information. A copyright year range of the form YYYY-ZZZZ specifies every single year from YYYY to ZZZZ inclusive (for example, 2012-2016 means 2012, 2013, 2014, 2015, 2016).
+
+Contact
+-------
+
+If you have any comments or questions about PyQSO, please send them via email to Christian Jacobs (2E0ICL) at christian at christianjacobs.uk. Bug reports and feature requests can be made via the `issue tracker <https://github.com/ctjacobs/pyqso/issues>`_.
 
 Structure of this documentation
 -------------------------------
 
-The structure of this documentation is as follows. The section on `Getting Started <getting_started.html>`_ provides information on the PyQSO installation process through to creating a new logbook (or opening an existing one). The `Log Management <log_management.html>`_ section explains how to create a log in the logbook, as well as the basic operations that users can perform with existing logs, such as printing, importing from/exporting to ADIF format, and sorting. The `Record Managemen [...]
+The structure of this documentation is as follows. The section on `Getting Started <getting_started.html>`_ provides information on the PyQSO installation process through to creating a new logbook (or opening an existing one). The `Log Management <log_management.html>`_ section explains how to create a log in the logbook, as well as the basic operations that users can perform with existing logs, such as printing, importing from/exporting to ADIF format, and sorting. The `Record Managemen [...]
 
diff --git a/docs/source/log_management.rst b/docs/source/log_management.rst
index 357a739..5e3783c 100644
--- a/docs/source/log_management.rst
+++ b/docs/source/log_management.rst
@@ -22,7 +22,7 @@ name must not be a number.
 Note: When logs are stored in the database file, field/column names from
 the ADIF standard are used. However, please note that only the following
 subset of all the ADIF fields is considered: CALL, QSO\_DATE, TIME\_ON,
-FREQ, BAND, MODE, TX\_PWR, RST\_SENT, RST\_RCVD, QSL\_SENT, QSL\_RCVD,
+FREQ, BAND, MODE, SUBMODE, TX\_PWR, RST\_SENT, RST\_RCVD, QSL\_SENT, QSL\_RCVD,
 NOTES, NAME, ADDRESS, STATE, COUNTRY, DXCC, CQZ, ITUZ, IOTA. Visit the `ADIF website <http://adif.org/>`_ for more information about these fields.
 
 Renaming a log
diff --git a/docs/source/preferences.rst b/docs/source/preferences.rst
index 4347711..fc5a253 100644
--- a/docs/source/preferences.rst
+++ b/docs/source/preferences.rst
@@ -2,33 +2,44 @@ Preferences
 ===========
 
 PyQSO user preferences are stored in a configuration file located at
-``~/.pyqso.ini``, where ``~`` denotes the user's home directory.
+``~/.config/pyqso/preferences.ini``, where ``~`` denotes the user's home directory.
 
 General
 -------
 
-Under the ``General`` tab, the user can choose to show the toolbox (see
-the `Toolbox <toolbox.html>`_ section) when PyQSO is started.
+Under the ``General`` tab, the user can choose to:
 
+-  always show the toolbox (see the `Toolbox <toolbox.html>`_ section) when PyQSO is started
+
+-  display yearly logbook statistics on the Summary page when a logbook is opened (see figure:summary_)
+
+-  keep the ``Add Record`` dialog window open after a new QSO is added, in preparation for the next QSO
+
+   .. _figure:summary:
+   .. figure::  images/summary.png
+      :align:   center
+      
+      The Summary page which appears after a logbook is opened. This presents some basic logbook statistics.
+      
 View
 ----
 
-Not all the available fields have to be displayed in the logbook. The
-user can choose to hide a subset of them by unchecking them in the
-``View`` tab. PyQSO must be restarted in order for any changes to take
-effect.
+Not all the available fields have to be displayed in the logbook. The user can choose to hide a subset of them by unchecking them in the ``View`` tab. PyQSO must be restarted in order for any changes to take effect.
+
+ADIF
+----
+
+PyQSO currently supports the ``NOTES`` field in the ADIF specification, but not the ``COMMENTS`` field. When a user imports a log in ADIF format, they can choose to merge any existing text in the ``COMMENTS`` field with the ``NOTES`` field by checking the 'merge' checkbox. This way, no information in the ``COMMENTS`` field is discarded during the import process.
 
 Records
 -------
 
-The records tab allows users to choose if the UTC timezone is used when autocompleting the date and time fields, and whether the band should be automatically determined from the frequency field. Default values for the Power and Mode fields can also be specified here.
+The records tab allows users to choose if the UTC timezone is used when autocompleting the date and time fields, and whether the band should be automatically determined from the frequency field. Default values for the Power, Mode, and Submode fields can also be specified here.
 
 Callsign lookup
 ~~~~~~~~~~~~~~~
 
-The user can enter their login details to access the `qrz.com <http://qrz.com/>`_
-database and perform callsign lookups. Note that these details are currently stored in plain text
-(unencrypted) format.
+The user can enter their login details to access the `qrz.com <http://qrz.com/>`_ or `hamqth.com <http://hamqth.com/>`_ database and perform callsign lookups. Note that these details are currently stored in plain text (unencrypted) format.
 
 If the ``Ignore callsign prefixes and/or suffixes`` box is checked, then PyQSO will perform the callsign lookup whilst ignoring all prefixes (i.e. anything before a preceding "/" in the callsign) and the suffixes "P", "M", "A", "PM", "MM", "AM", and "QRP". For example, if the callsign to be looked up is EA3/MYCALL/P, only MYCALL will be looked up. If you get 'Callsign not found' errors, try enabling this option.
 
diff --git a/docs/source/record_management.rst b/docs/source/record_management.rst
index f7281bf..12f40d7 100644
--- a/docs/source/record_management.rst
+++ b/docs/source/record_management.rst
@@ -30,9 +30,9 @@ Callsign lookup
 ~~~~~~~~~~~~~~~
 
 PyQSO can also resolve station-related information (e.g. the operator's
-name, address, and ITU Zone) by clicking the ``Lookup on qrz.com``
+name, address, and ITU Zone) by clicking the ``Callsign lookup``
 button adjacent to the Callsign data entry box. Note that the user must
-first supply their `qrz.com <http://qrz.com/>`_ account information in the preferences dialog
+first supply their `qrz.com <http://qrz.com/>`_ or `hamqth.com <http://hamqth.com/>`_ account information in the preferences dialog
 window.
 
 Editing a record
diff --git a/docs/source/shortcuts.rst b/docs/source/shortcuts.rst
new file mode 100644
index 0000000..3bc856d
--- /dev/null
+++ b/docs/source/shortcuts.rst
@@ -0,0 +1,16 @@
+Keyboard shortcuts
+==================
+
+==============    ===========
+Description       Shortcut
+==============    ===========
+Open logbook      Ctrl + O
+Close logbook     Ctrl + C
+New log           Ctrl + N
+Print log         Ctrl + P
+Quit              Ctrl + Q
+Add record        Ctrl + R
+Edit record       Ctrl + E
+Delete record     Delete
+==============    ===========
+
diff --git a/docs/source/toolbox.rst b/docs/source/toolbox.rst
index 1e8289f..29fcb84 100644
--- a/docs/source/toolbox.rst
+++ b/docs/source/toolbox.rst
@@ -15,12 +15,14 @@ result of the many different Telnet-based software products that DX
 clusters run, PyQSO currently outputs the raw data received from the DX
 cluster rather than trying to parse it in some way.
 
-Click on the ``Connect to Telnet Server`` button and enter the DX server
+Click on ``Connect to Telnet Server`` then ``New...`` in the ``Connection`` menu, and enter the DX server
 details in the dialog that appears. If no port is specified, PyQSO will
 use the default value of 23. A username and password may also need to be
-supplied. Once connected, the server output will appear in the DX
+supplied. Frequently used servers can be bookmarked for next time; bookmarked server details are stored in ``~/.config/pyqso/bookmarks.ini``, where ``~`` denotes the user's home directory.
+
+Once connected, the server output will appear in the DX
 cluster frame (see figure:dx_cluster_). A command can also
-be sent to the server by typing it into the entry box and clicking the
+be sent to the server by typing it into the entry box beneath the server output, and clicking the
 adjacent ``Send Command`` button.
 
    .. _figure:dx_cluster:
diff --git a/pyqso/adif.py b/pyqso/adif.py
index 949bf33..da139ab 100644
--- a/pyqso/adif.py
+++ b/pyqso/adif.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python 
+#!/usr/bin/env python3
 
-#    Copyright (C) 2012 Christian T. Jacobs.
+#    Copyright (C) 2012-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -22,54 +22,58 @@ import logging
 import unittest
 from datetime import datetime
 import calendar
+import configparser
+from os.path import expanduser
 
 # ADIF field names and their associated data types available in PyQSO.
-AVAILABLE_FIELD_NAMES_TYPES = {"CALL": "S", 
-                              "QSO_DATE": "D",
-                              "TIME_ON": "T",
-                              "FREQ": "N",
-                              "BAND": "E",
-                              "MODE": "E",
-                              "TX_PWR": "N",
-                              "RST_SENT": "S",
-                              "RST_RCVD": "S",
-                              "QSL_SENT": "S",
-                              "QSL_RCVD": "S",
-                              "NOTES": "M",
-                              "NAME": "S",
-                              "ADDRESS": "S",
-                              "STATE": "S",
-                              "COUNTRY": "S",
-                              "DXCC": "N",
-                              "CQZ": "N",
-                              "ITUZ": "N",
-                              "IOTA": "C"}
+AVAILABLE_FIELD_NAMES_TYPES = {"CALL": "S",
+                               "QSO_DATE": "D",
+                               "TIME_ON": "T",
+                               "FREQ": "N",
+                               "BAND": "E",
+                               "MODE": "E",
+                               "SUBMODE": "E",
+                               "TX_PWR": "N",
+                               "RST_SENT": "S",
+                               "RST_RCVD": "S",
+                               "QSL_SENT": "S",
+                               "QSL_RCVD": "S",
+                               "NOTES": "M",
+                               "NAME": "S",
+                               "ADDRESS": "S",
+                               "STATE": "S",
+                               "COUNTRY": "S",
+                               "DXCC": "N",
+                               "CQZ": "N",
+                               "ITUZ": "N",
+                               "IOTA": "C"}
 # Note: The logbook uses the ADIF field names for the database column names.
 # This list is used to display the columns in a logical order.
-AVAILABLE_FIELD_NAMES_ORDERED = ["CALL", "QSO_DATE", "TIME_ON", "FREQ", "BAND", "MODE", "TX_PWR", 
+AVAILABLE_FIELD_NAMES_ORDERED = ["CALL", "QSO_DATE", "TIME_ON", "FREQ", "BAND", "MODE", "SUBMODE", "TX_PWR",
                                  "RST_SENT", "RST_RCVD", "QSL_SENT", "QSL_RCVD", "NOTES", "NAME",
                                  "ADDRESS", "STATE", "COUNTRY", "DXCC", "CQZ", "ITUZ", "IOTA"]
 # Define the more user-friendly versions of the field names.
-AVAILABLE_FIELD_NAMES_FRIENDLY = {"CALL":"Callsign",
-                                  "QSO_DATE":"Date",
-                                  "TIME_ON":"Time",
-                                  "FREQ":"Frequency (MHz)",
-                                  "BAND":"Band",
-                                  "MODE":"Mode",
-                                  "TX_PWR":"TX Power (W)",
-                                  "RST_SENT":"RST Sent",
-                                  "RST_RCVD":"RST Received",
-                                  "QSL_SENT":"QSL Sent",
-                                  "QSL_RCVD":"QSL Received",
-                                  "NOTES":"Notes",
-                                  "NAME":"Name",
-                                  "ADDRESS":"Address",
-                                  "STATE":"State",
-                                  "COUNTRY":"Country",
-                                  "DXCC":"DXCC",
-                                  "CQZ":"CQ Zone",
-                                  "ITUZ":"ITU Zone",
-                                  "IOTA":"IOTA Designator"}
+AVAILABLE_FIELD_NAMES_FRIENDLY = {"CALL": "Callsign",
+                                  "QSO_DATE": "Date",
+                                  "TIME_ON": "Time",
+                                  "FREQ": "Frequency (MHz)",
+                                  "BAND": "Band",
+                                  "MODE": "Mode",
+                                  "SUBMODE": "Submode",
+                                  "TX_PWR": "TX Power (W)",
+                                  "RST_SENT": "RST Sent",
+                                  "RST_RCVD": "RST Received",
+                                  "QSL_SENT": "QSL Sent",
+                                  "QSL_RCVD": "QSL Received",
+                                  "NOTES": "Notes",
+                                  "NAME": "Name",
+                                  "ADDRESS": "Address",
+                                  "STATE": "State",
+                                  "COUNTRY": "Country",
+                                  "DXCC": "DXCC",
+                                  "CQZ": "CQ Zone",
+                                  "ITUZ": "ITU Zone",
+                                  "IOTA": "IOTA Designator"}
 
 # A: AwardList
 # B: Boolean
@@ -83,343 +87,477 @@ AVAILABLE_FIELD_NAMES_FRIENDLY = {"CALL":"Callsign",
 # L: Location
 DATA_TYPES = ["A", "B", "N", "S", "I", "D", "T", "M", "G", "L", "E"]
 
-# All the modes listed in the ADIF specification
-MODES = ["", "AM", "AMTORFEC", "ASCI", "ATV", "CHIP64", "CHIP128", "CLO", "CONTESTI", "CW", "DSTAR", "DOMINO", "DOMINOF", "FAX", "FM", "FMHELL", "FSK31", "FSK441", "GTOR", "HELL", "HELL80", "HFSK", "ISCAT", "JT44", "JT4A", "JT4B", "JT4C", "JT4D", "JT4E", "JT4F", "JT4G", "JT65", "JT65A", "JT65B", "JT65C", "JT6M", "MFSK8", "MFSK16", "MT63", "OLIVIA", "PAC", "PAC2", "PAC3", "PAX", "PAX2", "PCW", "PKT", "PSK10", "PSK31", "PSK63", "PSK63F", "PSK125", "PSKAM10", "PSKAM31", "PSKAM50", "PSKFEC31 [...]
+# All the valid modes listed in the ADIF specification. This is a dictionary with the key-value pairs holding the MODE and SUBMODE(s) respectively.
+MODES = {"": ("",),
+         "AM": ("",),
+         "ATV": ("",),
+         "CHIP": ("", "CHIP64", "CHIP128"),
+         "CLO": ("",),
+         "CONTESTI": ("",),
+         "CW": ("", "PCW"),
+         "DIGITALVOICE": ("",),
+         "DOMINO": ("", "DOMINOEX", "DOMINOF"),
+         "DSTAR": ("",),
+         "FAX": ("",),
+         "FM": ("",),
+         "FSK441": ("",),
+         "HELL": ("", "FMHELL", "FSKHELL", "HELL80", "HFSK", "PSKHELL"),
+         "ISCAT": ("", "ISCAT-A", "ISCAT-B"),
+         "JT4": ("", "JT4A", "JT4B", "JT4C", "JT4D", "JT4E", "JT4F", "JT4G"),
+         "JT6M": ("",),
+         "JT9": ("",),
+         "JT44": ("",),
+         "JT65": ("", "JT65A", "JT65B", "JT65B2", "JT65C", "JT65C2"),
+         "MFSK": ("", "MFSK4", "MFSK8", "MFSK11", "MFSK16", "MFSK22", "MFSK31", "MFSK32", "MFSK64", "MFSK128"),
+         "MT63": ("",),
+         "OLIVIA": ("", "OLIVIA 4/125", "OLIVIA 4/250", "OLIVIA 8/250", "OLIVIA 8/500", "OLIVIA 16/500", "OLIVIA 16/1000", "OLIVIA 32/1000"),
+         "OPERA": ("", "OPERA-BEACON", "OPERA-QSO"),
+         "PAC": ("", "PAC2", "PAC3", "PAC4"),
+         "PAX": ("", "PAX2"),
+         "PKT": ("",),
+         "PSK": ("", "FSK31", "PSK10", "PSK31", "PSK63", "PSK63F", "PSK125", "PSK250", "PSK500", "PSK1000", "PSKAM10", "PSKAM31", "PSKAM50", "PSKFEC31", "QPSK31", "QPSK63", "QPSK125", "QPSK250", "QPSK500"),
+         "PSK2K": ("",),
+         "Q15": ("",),
+         "ROS": ("", "ROS-EME", "ROS-HF", "ROS-MF"),
+         "RTTY": ("", "ASCI"),
+         "RTTYM": ("",),
+         "SSB": ("", "LSB", "USB"),
+         "SSTV": ("",),
+         "THOR": ("",),
+         "THRB": ("", "THRBX"),
+         "TOR": ("", "AMTORFEC", "GTOR"),
+         "V4": ("",),
+         "VOI": ("",),
+         "WINMOR": ("",),
+         "WSPR": ("",)
+         }
+
+# A dictionary of all the deprecated MODE values.
+MODES_DEPRECATED = {"AMTORFEC": ("",),
+                    "ASCI": ("",),
+                    "CHIP64": ("",),
+                    "CHIP128": ("",),
+                    "DOMINOF": ("",),
+                    "FMHELL": ("",),
+                    "FSK31": ("",),
+                    "GTOR": ("",),
+                    "HELL80": ("",),
+                    "HFSK": ("",),
+                    "JT4A": ("",),
+                    "JT4B": ("",),
+                    "JT4C": ("",),
+                    "JT4D": ("",),
+                    "JT4E": ("",),
+                    "JT4F": ("",),
+                    "JT4G": ("",),
+                    "JT65A": ("",),
+                    "JT65B": ("",),
+                    "JT65C": ("",),
+                    "MFSK8": ("",),
+                    "MFSK16": ("",),
+                    "PAC2": ("",),
+                    "PAC3": ("",),
+                    "PAX2": ("",),
+                    "PCW": ("",),
+                    "PSK10": ("",),
+                    "PSK31": ("",),
+                    "PSK63": ("",),
+                    "PSK63F": ("",),
+                    "PSK125": ("",),
+                    "PSKAM10": ("",),
+                    "PSKAM31": ("",),
+                    "PSKAM50": ("",),
+                    "PSKFEC31": ("",),
+                    "PSKHELL": ("",),
+                    "QPSK31": ("",),
+                    "QPSK63": ("",),
+                    "QPSK125": ("",),
+                    "THRBX": ("",)
+                    }
+
+# Include all deprecated modes.
+MODES.update(MODES_DEPRECATED)
 
 # All the bands listed in the ADIF specification.
-BANDS = ["", "2190m", "560m", "160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m", "4m", "2m", "1.25m", "70cm", "33cm", "23cm", "13cm", "9cm", "6cm", "3cm", "1.25cm", "6mm", "4mm", "2.5mm", "2mm", "1mm"]
+BANDS = ["", "2190m", "630m", "560m", "160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m", "4m", "2m", "1.25m", "70cm", "33cm", "23cm", "13cm", "9cm", "6cm", "3cm", "1.25cm", "6mm", "4mm", "2.5mm", "2mm", "1mm"]
 # The lower and upper frequency bounds (in MHz) for each band in BANDS.
-BANDS_RANGES = [(None, None), (0.136, 0.137), (0.501, 0.504), (1.8, 2.0), (3.5, 4.0), (5.102, 5.404), (7.0, 7.3), (10.0, 10.15), (14.0, 14.35), (18.068, 18.168), (21.0, 21.45), (24.890, 24.99), (28.0, 29.7), (50.0, 54.0), (70.0, 71.0), (144.0, 148.0), (222.0, 225.0), (420.0, 450.0), (902.0, 928.0), (1240.0, 1300.0), (2300.0, 2450.0), (3300.0, 3500.0), (5650.0, 5925.0), (10000.0, 10500.0), (24000.0, 24250.0), (47000.0, 47200.0), (75500.0, 81000.0), (119980.0, 120020.0), (142000.0, 149000. [...]
+BANDS_RANGES = [(None, None), (0.136, 0.137), (0.472, 0.479), (0.501, 0.504), (1.8, 2.0), (3.5, 4.0), (5.102, 5.4065), (7.0, 7.3), (10.0, 10.15), (14.0, 14.35), (18.068, 18.168), (21.0, 21.45), (24.890, 24.99), (28.0, 29.7), (50.0, 54.0), (70.0, 71.0), (144.0, 148.0), (222.0, 225.0), (420.0, 450.0), (902.0, 928.0), (1240.0, 1300.0), (2300.0, 2450.0), (3300.0, 3500.0), (5650.0, 5925.0), (10000.0, 10500.0), (24000.0, 24250.0), (47000.0, 47200.0), (75500.0, 81000.0), (119980.0, 120020.0), ( [...]
+
+ADIF_VERSION = "3.0.4"
 
-ADIF_VERSION = "1.0"
 
 class ADIF:
-   """ The ADIF class supplies methods for reading, parsing, and writing log files in the Amateur Data Interchange Format (ADIF). For more information, visit http://adif.org/ """
-   
-   def __init__(self):
-      # Class for I/O of files using the Amateur Data Interchange Format (ADIF).
-      logging.debug("New ADIF instance created!")
-      
-   def read(self, path):
-      """ Read an ADIF file with a specified path (given in the 'path' argument), and then parse it.
-      The output is a list of dictionaries (one dictionary per QSO), with each dictionary containing field-value pairs,
-      e.g. {FREQ:145.500, BAND:2M, MODE:FM}. """
-      logging.debug("Reading in ADIF file with path: %s..." % path)
-
-      text = ""      
-      try:
-         f = open(path, 'r')
-         text = f.read()
-         f.close() # Close the file, otherwise "bad things" might happen!
-      except IOError as e:
-         logging.error("I/O error %d: %s" % (e.errno, e.strerror))
-      except:
-         logging.error("Unknown error occurred when reading the ADIF file.")
-
-      records = self._parse_adi(text)
-         
-      if(records == []):
-         logging.warning("No records found in the file. Empty file or wrong file type?")
-         
-      return records
-      
-   def _parse_adi(self, text):
-      """ Parse some raw text (defined in the 'text' argument) for ADIF field data.
-      Outputs a list of dictionaries (one dictionary per QSO). Each dictionary contains the field-value pairs,
-      e.g. {FREQ:145.500, BAND:2M, MODE:FM}. """
-
-      logging.debug("Parsing text from the ADIF file...")
-
-      records = []
-
-      # Separate the text at the <eor> or <eoh> markers.
-      tokens = re.split('(<eor>|<eoh>)', text, flags=re.IGNORECASE)
-      tokens.pop() # Anything after the final <eor> marker should be ignored.
-      
-      # The header might tell us the number of records, but let's not assume
-      # this and simply ignore it instead (if it exists).
-      if(re.search('<eoh>', text, flags=re.IGNORECASE) is not None):
-         # There is a header present, so let's ignore everything
-         # up to and including the <eoh> marker. Note that
-         # re.search has been used here to handle any case sensitivity.
-         # Previously we were checking for <eoh>. <EOH> is also valid
-         # but wasn't been detected before.
-         while len(tokens) > 0:
-            t = tokens.pop(0)
-            if(re.match('<eoh>', t, flags=re.IGNORECASE) is not None):
-               break
-            
-      n_eor = 0 
-      n_record = 0
-      records = []
-      for t in tokens:
-         if(re.match('<eor>', t, flags=re.IGNORECASE) is not None):
-            n_eor = n_eor + 1
-            continue
-         else:
-            n_record = n_record + 1
-            # Each record will have field names and corresponding
-            # data entries. Store this in a dictionary.
-            # Note: This is based on the code written by OK4BX.
-            # (http://web.bxhome.org/blog/ok4bx/2012/05/adif-parser-python)
-            fields_and_data_dictionary = {}
-            fields_and_data = re.findall('<(.*?):(\d*).*?>([^<\t\n\r\f\v]+)', t)
-            for fd in fields_and_data:
-               # Let's force all field names to be in upper case.
-               # This will help us later when comparing the field names
-               # against the available field names in the ADIF specification.
-               field_name = fd[0].upper()
-               field_data = fd[2][:int(fd[1])]
-
-               # Combo boxes are used later on and these are case sensitive,
-               # so adjust the field data accordingly.
-               if(field_name == "BAND"):
-                  field_data = field_data.lower()
-               elif(field_name == "MODE"):
-                  field_data = field_data.upper()
-               elif(field_name == "CALL"):
-                  # Also force all the callsigns to be in upper case.
-                  field_data = field_data.upper()
-               if(field_name in AVAILABLE_FIELD_NAMES_ORDERED):
-                  field_data_type = AVAILABLE_FIELD_NAMES_TYPES[field_name]
-                  if(self.is_valid(field_name, field_data, field_data_type)):
-                     # Only add the field if it is a standard ADIF field and it holds valid data.
-                     fields_and_data_dictionary[field_name] = field_data
-
-            records.append(fields_and_data_dictionary)
-      
-      assert n_eor == n_record
-
-      logging.debug("Finished parsing text.")
-      
-      return records
-
-      
-   def write(self, records, path):
-      """ Write an ADIF file containing all the QSOs in the 'records' list. The desired path is specified in the 'path' argument. 
-      This method returns None. """
-   
-      logging.debug("Writing records to an ADIF file...")
-      try:
-         f = open(path, 'w') # Open file for writing
-         
-         # First write a header containing program version, number of records, etc.
-         dt = datetime.now()
-         
-         f.write("""Amateur radio log file. Generated on %s. Contains %d record(s). 
-         
+
+    """ The ADIF class supplies methods for reading, parsing, and writing log files in the Amateur Data Interchange Format (ADIF).
+    For more information, visit http://adif.org/ """
+
+    def __init__(self):
+        """ Initialise class for I/O of files using the Amateur Data Interchange Format (ADIF). """
+        logging.debug("New ADIF instance created!")
+
+    def read(self, path):
+        """ Read an ADIF file and parse it.
+
+        :arg str path: The path to the ADIF file to read.
+        :returns: A list of dictionaries (one dictionary per QSO), with each dictionary containing field-value pairs, e.g. {FREQ:145.500, BAND:2M, MODE:FM}.
+        :rtype: list
+        :raises IOError: if the ADIF file does not exist or cannot be read (e.g. due to lack of read permissions).
+        """
+        logging.debug("Reading in ADIF file with path: %s..." % path)
+
+        text = ""
+        try:
+            f = open(path, mode='r', errors="replace")
+            text = f.read()
+            f.close()  # Close the file, otherwise "bad things" might happen!
+        except IOError as e:
+            logging.error("I/O error %d: %s" % (e.errno, e.strerror))
+        except Exception as e:
+            logging.error("An error occurred when reading the ADIF file.")
+            logging.exception(e)
+
+        records = self._parse_adi(text)
+
+        if(records == []):
+            logging.warning("No records found in the file. Empty file or wrong file type?")
+
+        return records
+
+    def _parse_adi(self, text):
+        """ Parse some raw text (defined in the 'text' argument) for ADIF field data.
+
+        :arg str text: The raw text from the ADIF file to parse.
+        :returns: A list of dictionaries (one dictionary per QSO). Each dictionary contains the field-value pairs, e.g. {FREQ:145.500, BAND:2M, MODE:FM}.
+        :rtype: list
+        """
+
+        logging.debug("Parsing text from the ADIF file...")
+
+        records = []
+
+        # ADIF-related configuration options
+        config = configparser.ConfigParser()
+        have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != [])
+        (section, option) = ("adif", "merge_comment")
+        if(have_config and config.has_option(section, option) and config.get(section, option) == "True"):
+            merge_comment = True
+        else:
+            merge_comment = False
+
+        # Separate the text at the <eor> or <eoh> markers.
+        tokens = re.split('(<eor>|<eoh>)', text, flags=re.IGNORECASE)
+        tokens.pop()  # Anything after the final <eor> marker should be ignored.
+
+        # The header might tell us the number of records, but let's not assume
+        # this and simply ignore it instead (if it exists).
+        if(re.search('<eoh>', text, flags=re.IGNORECASE) is not None):
+            # There is a header present, so let's ignore everything
+            # up to and including the <eoh> marker. Note that
+            # re.search has been used here to handle any case sensitivity.
+            # Previously we were checking for <eoh>. <EOH> is also valid
+            # but wasn't being detected before.
+            while len(tokens) > 0:
+                t = tokens.pop(0)
+                if(re.match('<eoh>', t, flags=re.IGNORECASE) is not None):
+                    break
+
+        n_eor = 0
+        n_record = 0
+        records = []
+        pattern = re.compile('<(.*?):(\d*).*?>([^<\t\n\r\f\v]+)')
+
+        for t in tokens:
+            if(re.match('<eor>', t, flags=re.IGNORECASE) is not None):
+                n_eor += 1
+                continue
+            else:
+                n_record += 1
+                # Each record will have field names and corresponding
+                # data entries. Store this in a dictionary.
+                # Note: This is based on the code written by OK4BX.
+                # (http://web.bxhome.org/blog/ok4bx/2012/05/adif-parser-python)
+                fields_and_data_dictionary = {}
+                fields_and_data = pattern.findall(t)
+                comment = None
+                for fd in fields_and_data:
+                    # Let's force all field names to be in upper case.
+                    # This will help us later when comparing the field names
+                    # against the available field names in the ADIF specification.
+                    field_name = fd[0].upper()
+                    field_data = fd[2][:int(fd[1])]
+
+                    # Combo boxes are used later on and these are case sensitive,
+                    # so adjust the field data accordingly.
+                    if(field_name == "BAND"):
+                        field_data = field_data.lower()
+                    elif(field_name == "CALL" or field_name == "MODE" or field_name == "SUBMODE"):
+                        field_data = field_data.upper()
+                    elif(field_name == "COMMENT"):
+                        # Keep a copy of the COMMENT field data, in case we want to merge
+                        # it with the NOTES field.
+                        comment = field_data
+                    if(field_name in AVAILABLE_FIELD_NAMES_ORDERED):
+                        field_data_type = AVAILABLE_FIELD_NAMES_TYPES[field_name]
+                        if(self.is_valid(field_name, field_data, field_data_type)):
+                            # Only add the field if it is a standard ADIF field and it holds valid data.
+                            fields_and_data_dictionary[field_name] = field_data
+
+                # Merge the COMMENT field with the NOTES field, if desired and applicable.
+                if(merge_comment):
+                    if("NOTES" in list(fields_and_data_dictionary.keys()) and comment):
+                        logging.debug("Merging COMMENT field with NOTES field...")
+                        fields_and_data_dictionary["NOTES"] += "\\n" + comment
+                        logging.debug("Merged fields.")
+                    elif(comment):
+                        # Create the NOTES entry, but only store the contents of the COMMENT field.
+                        logging.debug("The COMMENT field is present, but not the NOTES field. The NOTES field will be created and will only hold the COMMENT.")
+                        fields_and_data_dictionary["NOTES"] = comment
+                    else:
+                        pass
+                records.append(fields_and_data_dictionary)
+
+        assert n_eor == n_record
+
+        logging.debug("Finished parsing text.")
+
+        return records
+
+    def write(self, records, path):
+        """ Write an ADIF file containing all the QSOs in the 'records' list.
+
+        :arg list records: The list of QSO records to write.
+        :arg str path: The desired path of the ADIF file to write to.
+        :returns: None
+        :raises IOError: if the ADIF file cannot be written (e.g. due to lack of write permissions).
+        """
+
+        logging.debug("Writing records to an ADIF file...")
+        try:
+            f = open(path, mode='w', errors="replace")  # Open file for writing
+
+            # First write a header containing program version, number of records, etc.
+            dt = datetime.now()
+
+            f.write("""Amateur radio log file. Generated on %s. Contains %d record(s).
+
 <adif_ver:%d>%s
 <programid:5>PyQSO
-<programversion:3>0.2
+<programversion:3>0.3
 <eoh>\n""" % (dt, len(records), len(str(ADIF_VERSION)), ADIF_VERSION))
-         
-         # Then write each log to the file.
-         for r in records:
-            for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
-               if(not(field_name.lower() in r.keys() or field_name.upper() in r.keys())): 
-                  # If the field_name does not exist in the record, then skip past it.
-                  # Only write out the fields that exist and that have some data in them.
-                  continue
-               else:
-                  if( (r[field_name] != "NULL") and (r[field_name] != "") ):
-                     f.write("<%s:%d>%s\n" % (field_name.lower(), len(r[field_name]), r[field_name]))
-            f.write("<eor>\n")
-
-         logging.debug("Finished writing records to the ADIF file.")
-         f.close()
-
-      except IOError as e:
-         logging.error("I/O error %d: %s" % (e.errno, e.strerror))
-      except:
-         logging.error("Unknown error occurred when writing the ADIF file.")
-
-      return
-
-
-   def is_valid(self, field_name, data, data_type):
-      """ Validate the data in a field (with name 'field_name') with respect to the ADIF specification. 
-      This method returns either True or False to indicate whether the data is valid or not. """
-
-      logging.debug("Validating the following data in field '%s': %s" % (field_name, data))
-
-      # Allow an empty string, in case the user doesn't want
-      # to fill in this field.
-      if(data == ""):
-         return True
-
-      if(data_type == "N"):
-         # Allow a decimal point before and/or after any numbers,
-         # but don't allow a decimal point on its own.
-         m = re.match(r"-?(([0-9]+\.?[0-9]*)|([0-9]*\.?[0-9]+))", data)
-         if(m is None):
-            # Did not match anything.
-            return False
-         else:
-            # Make sure we match the whole string,
-            # otherwise there may be an invalid character after the match. 
-            return (m.group(0) == data)
-      
-      elif(data_type == "B"):
-         # Boolean
-         m = re.match(r"(Y|N)", data)
-         if(m is None):
-            return False
-         else:
-            return (m.group(0) == data)
-
-      elif(data_type == "D"):
-         # Date
-         pattern = re.compile(r"([0-9]{4})")
-         m_year = pattern.match(data, 0)
-         if((m_year is None) or (int(m_year.group(0)) < 1930)):
-            # Did not match anything.
-            return False
-         else:
-            pattern = re.compile(r"([0-9]{2})")
-            m_month = pattern.match(data, 4)
-            if((m_month is None) or int(m_month.group(0)) > 12 or int(m_month.group(0)) < 1):
-               # Did not match anything.
-               return False
+
+            # Then write each log to the file.
+            for r in records:
+                for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
+                    if(not(field_name.lower() in list(r.keys()) or field_name.upper() in list(r.keys()))):
+                        # If the field_name does not exist in the record, then skip past it.
+                        # Only write out the fields that exist and that have some data in them.
+                        continue
+                    else:
+                        if((r[field_name] != "NULL") and (r[field_name] != "")):
+                            f.write("<%s:%d>%s\n" % (field_name.lower(), len(r[field_name]), r[field_name]))
+                f.write("<eor>\n")
+
+            logging.debug("Finished writing records to the ADIF file.")
+            f.close()
+
+        except IOError as e:
+            logging.error("I/O error %d: %s" % (e.errno, e.strerror))
+        except Exception as e:  # All other exceptions.
+            logging.error("An error occurred when writing the ADIF file.")
+            logging.exception(e)
+
+        return
+
+    def is_valid(self, field_name, data, data_type):
+        """ Validate the data in a field with respect to the ADIF specification.
+
+        :arg str field_name: The name of the ADIF field.
+        :arg str data: The data of the ADIF field to validate.
+        :arg str data_type: The type of data to be validated. See http://www.adif.org/304/ADIF_304.htm#Data_Types for the full list with descriptions.
+        :returns: True or False to indicate whether the data is valid or not.
+        :rtype: bool
+        """
+
+        logging.debug("Validating the following data in field '%s': %s" % (field_name, data))
+
+        # Allow an empty string, in case the user doesn't want
+        # to fill in this field.
+        if(data == ""):
+            return True
+
+        if(data_type == "N"):
+            # Allow a decimal point before and/or after any numbers,
+            # but don't allow a decimal point on its own.
+            m = re.match(r"-?(([0-9]+\.?[0-9]*)|([0-9]*\.?[0-9]+))", data)
+            if(m is None):
+                # Did not match anything.
+                return False
+            else:
+                # Make sure we match the whole string,
+                # otherwise there may be an invalid character after the match.
+                return (m.group(0) == data)
+
+        elif(data_type == "B"):
+            # Boolean
+            m = re.match(r"(Y|N)", data)
+            if(m is None):
+                return False
             else:
-               pattern = re.compile(r"([0-9]{2})")
-               m_day = pattern.match(data, 6)
-               days_in_month = calendar.monthrange(int(m_year.group(0)), int(m_month.group(0)))
-               if((m_day is None) or int(m_day.group(0)) > days_in_month[1] or int(m_day.group(0)) < 1):
-                  # Did not match anything.
-                  return False
-               else:
-                  # Make sure we match the whole string,
-                  # otherwise there may be an invalid character after the match. 
-                  return (len(data) == 8)
-
-      elif(data_type == "T"):
-         # Time
-         pattern = re.compile(r"([0-9]{2})")
-         m_hour = pattern.match(data, 0)
-         if((m_hour is None) or (int(m_hour.group(0)) < 0) or (int(m_hour.group(0)) > 23)):
-            # Did not match anything.
-            return False
-         else:
+                return (m.group(0) == data)
+
+        elif(data_type == "D"):
+            # Date
+            pattern = re.compile(r"([0-9]{4})")
+            m_year = pattern.match(data, 0)
+            if((m_year is None) or (int(m_year.group(0)) < 1930)):
+                # Did not match anything.
+                return False
+            else:
+                pattern = re.compile(r"([0-9]{2})")
+                m_month = pattern.match(data, 4)
+                if((m_month is None) or int(m_month.group(0)) > 12 or int(m_month.group(0)) < 1):
+                    # Did not match anything.
+                    return False
+                else:
+                    pattern = re.compile(r"([0-9]{2})")
+                    m_day = pattern.match(data, 6)
+                    days_in_month = calendar.monthrange(int(m_year.group(0)), int(m_month.group(0)))
+                    if((m_day is None) or int(m_day.group(0)) > days_in_month[1] or int(m_day.group(0)) < 1):
+                        # Did not match anything.
+                        return False
+                    else:
+                        # Make sure we match the whole string,
+                        # otherwise there may be an invalid character after the match.
+                        return (len(data) == 8)
+
+        elif(data_type == "T"):
+            # Time
             pattern = re.compile(r"([0-9]{2})")
-            m_minutes = pattern.match(data, 2)
-            if((m_minutes is None) or int(m_minutes.group(0)) < 0 or int(m_minutes.group(0)) > 59):
-               # Did not match anything.
-               return False
+            m_hour = pattern.match(data, 0)
+            if((m_hour is None) or (int(m_hour.group(0)) < 0) or (int(m_hour.group(0)) > 23)):
+                # Did not match anything.
+                return False
+            else:
+                pattern = re.compile(r"([0-9]{2})")
+                m_minutes = pattern.match(data, 2)
+                if((m_minutes is None) or int(m_minutes.group(0)) < 0 or int(m_minutes.group(0)) > 59):
+                    # Did not match anything.
+                    return False
+                else:
+                    if(len(data) == 4):
+                        # HHMM format
+                        return True
+                    pattern = re.compile(r"([0-9]{2})")
+                    m_seconds = pattern.match(data, 4)
+                    if((m_seconds is None) or int(m_seconds.group(0)) < 0 or int(m_seconds.group(0)) > 59):
+                        # Did not match anything.
+                        return False
+                    else:
+                        # Make sure we match the whole string,
+                        # otherwise there may be an invalid character after the match.
+                        return (len(data) == 6)  # HHMMSS format
+
+        # FIXME: Need to make sure that the "S" and "M" data types accept ASCII-only characters
+        # in the range 32-126 inclusive.
+        elif(data_type == "S"):
+            # String
+            m = re.match(r"(.+)", data)
+            if(m is None):
+                return False
+            else:
+                return (m.group(0) == data)
+
+        elif(data_type == "I"):
+            # IntlString
+            m = re.match(r"(.+)", data, re.UNICODE)
+            if(m is None):
+                return False
+            else:
+                return (m.group(0) == data)
+
+        elif(data_type == "G"):
+            # IntlMultilineString
+            m = re.match(r"(.+(\r\n)*.*)", data, re.UNICODE)
+            if(m is None):
+                return False
+            else:
+                return (m.group(0) == data)
+
+        elif(data_type == "M"):
+            # MultilineString
+            # m = re.match(r"(.+(\r\n)*.*)", data)
+            # if(m is None):
+            #   return False
+            # else:
+            #   return (m.group(0) == data)
+            return True
+
+        elif(data_type == "L"):
+            # Location
+            pattern = re.compile(r"([EWNS]{1})", re.IGNORECASE)
+            m_directional = pattern.match(data, 0)
+            if(m_directional is None):
+                # Did not match anything.
+                return False
             else:
-               if(len(data) == 4):
-                  # HHMM format
-                  return True
-               pattern = re.compile(r"([0-9]{2})")
-               m_seconds = pattern.match(data, 4)
-               if((m_seconds is None) or int(m_seconds.group(0)) < 0 or int(m_seconds.group(0)) > 59):
-                  # Did not match anything.
-                  return False
-               else:
-                  # Make sure we match the whole string,
-                  # otherwise there may be an invalid character after the match. 
-                  return (len(data) == 6) # HHMMSS format
-
-      #FIXME: Need to make sure that the "S" and "M" data types accept ASCII-only characters
-      # in the range 32-126 inclusive.
-      elif(data_type == "S"):
-         # String
-         m = re.match(r"(.+)", data)
-         if(m is None):
-            return False
-         else:
-            return (m.group(0) == data)
-
-      elif(data_type == "I"):
-         # IntlString
-         m = re.match(ur"(.+)", data, re.UNICODE)
-         if(m is None):
-            return False
-         else:
-            return (m.group(0) == data)
-
-      elif(data_type == "G"):
-         # IntlMultilineString
-         m = re.match(ur"(.+(\r\n)*.*)", data, re.UNICODE)
-         if(m is None):
-            return False
-         else:
-            return (m.group(0) == data)
-
-      elif(data_type == "M"):
-         # MultilineString
-         #m = re.match(r"(.+(\r\n)*.*)", data)
-         #if(m is None):
-         #   return False
-         #else:
-         #   return (m.group(0) == data)
-         return True
-
-      elif(data_type == "L"):
-         # Location
-         pattern = re.compile(r"([EWNS]{1})", re.IGNORECASE)
-         m_directional = pattern.match(data, 0)
-         if(m_directional is None):
-            # Did not match anything.
-            return False
-         else:
-            pattern = re.compile(r"([0-9]{3})")
-            m_degrees = pattern.match(data, 1)
-            if((m_degrees is None) or int(m_degrees.group(0)) < 0 or int(m_degrees.group(0)) > 180):
-               # Did not match anything.
-               return False
+                pattern = re.compile(r"([0-9]{3})")
+                m_degrees = pattern.match(data, 1)
+                if((m_degrees is None) or int(m_degrees.group(0)) < 0 or int(m_degrees.group(0)) > 180):
+                    # Did not match anything.
+                    return False
+                else:
+                    pattern = re.compile(r"([0-9]{2}\.[0-9]{3})")
+                    m_minutes = pattern.match(data, 4)
+                    if((m_minutes is None) or float(m_minutes.group(0)) < 0 or float(m_minutes.group(0)) > 59.999):
+                        # Did not match anything.
+                        return False
+                    else:
+                        # Make sure we match the whole string,
+                        # otherwise there may be an invalid character after the match.
+                        return (len(data) == 10)
+
+        elif(data_type == "E" or data_type == "A"):
+            # Enumeration, AwardList.
+            if(field_name == "MODE"):
+                return (data in list(MODES.keys()))
+            elif(field_name == "BAND"):
+                return (data in BANDS)
             else:
-               pattern = re.compile(r"([0-9]{2}\.[0-9]{3})")
-               m_minutes = pattern.match(data, 4)
-               if((m_minutes is None) or float(m_minutes.group(0)) < 0 or float(m_minutes.group(0)) > 59.999):
-                  # Did not match anything.
-                  return False
-               else:
-                  # Make sure we match the whole string,
-                  # otherwise there may be an invalid character after the match. 
-                  return (len(data) == 10)
-
-
-      elif(data_type == "E" or data_type == "A"):
-         # Enumeration, AwardList.
-         if(field_name == "MODE"):
-            return (data in MODES)
-         elif(field_name == "BAND"):
-            return (data in BANDS)
-         else:
+                return True
+
+        else:
             return True
 
-      else:
-         return True
-      
-   
+
 class TestADIF(unittest.TestCase):
-   """ The unit tests for the ADIF module. """
 
-   def setUp(self):
-      """ Set up the ADIF object needed for the unit tests. """
-      self.adif = ADIF()
+    """ The unit tests for the ADIF module. """
+
+    def setUp(self):
+        """ Set up the ADIF object needed for the unit tests. """
+        self.adif = ADIF()
 
-   def test_adif_read(self):
-      """ Check that a single ADIF record can be read and parsed correctly. """
-      f = open("ADIF.test_read.adi", 'w')
-      f.write("""Some test ADI data.<eoh>
+    def test_adif_read(self):
+        """ Check that a single ADIF record can be read and parsed correctly. """
+        f = open("ADIF.test_read.adi", 'w')
+        f.write("""Some test ADI data.<eoh>
 
 <call:4>TEST<band:3>40m<mode:2>CW
 <qso_date:8:d>20130322<time_on:4>1955<eor>""")
-      f.close()
-    
-      records = self.adif.read("ADIF.test_read.adi")
-      expected_records = [{'TIME_ON': '1955', 'BAND': '40m', 'CALL': 'TEST', 'MODE': 'CW', 'QSO_DATE': '20130322'}]
-      print "Imported records: ", records
-      print "Expected records: ", expected_records
-      assert(len(records) == 1)
-      assert(len(records[0].keys()) == len(expected_records[0].keys()))
-      assert(records == expected_records)
-
-   def test_adif_read_multiple(self):
-      """ Check that multiple ADIF records can be read and parsed correctly. """
-      f = open("ADIF.test_read_multiple.adi", 'w')
-      f.write("""Some test ADI data.<eoh>
+        f.close()
+
+        records = self.adif.read("ADIF.test_read.adi")
+        expected_records = [{'TIME_ON': '1955', 'BAND': '40m', 'CALL': 'TEST', 'MODE': 'CW', 'QSO_DATE': '20130322'}]
+        print("Imported records: ", records)
+        print("Expected records: ", expected_records)
+        assert(len(records) == 1)
+        assert(len(list(records[0].keys())) == len(list(expected_records[0].keys())))
+        assert(records == expected_records)
+
+    def test_adif_read_multiple(self):
+        """ Check that multiple ADIF records can be read and parsed correctly. """
+        f = open("ADIF.test_read_multiple.adi", 'w')
+        f.write("""Some test ADI data.<eoh>
 
 <call:4>TEST<band:3>40m<mode:2>CW
 <qso_date:8:d>20130322<time_on:4>1955<eor>
@@ -428,87 +566,87 @@ class TestADIF(unittest.TestCase):
 <qso_date:8>20150227<time_on:4>0820<eor>
 
 <call:5>HELLO<band:2>2m<mode:2>FM<qso_date:8:d>20150227<time_on:4>0832<eor>""")
-      f.close()
-    
-      records = self.adif.read("ADIF.test_read_multiple.adi")
-      expected_records = [{'TIME_ON': '1955', 'BAND': '40m', 'CALL': 'TEST', 'MODE': 'CW', 'QSO_DATE': '20130322'}, {'TIME_ON': '0820', 'BAND': '20m', 'CALL': 'TEST2ABC', 'MODE': 'SSB', 'QSO_DATE': '20150227'}, {'TIME_ON': '0832', 'BAND': '2m', 'CALL': 'HELLO', 'MODE': 'FM', 'QSO_DATE': '20150227'}]
-      print "Imported records: ", records
-      print "Expected records: ", expected_records
-      assert(len(records) == 3)
-      for i in range(len(expected_records)):
-         assert(len(records[i].keys()) == len(expected_records[i].keys()))
-      assert(records == expected_records)
-
-   def test_adif_read_alphabet(self):
-      """ Check that none of the letters of the alphabet are ignored during parsing. """
-      f = open("ADIF.test_read_alphabet.adi", 'w')
-      f.write("""Some test ADI data.<eoh>
+        f.close()
+
+        records = self.adif.read("ADIF.test_read_multiple.adi")
+        expected_records = [{'TIME_ON': '1955', 'BAND': '40m', 'CALL': 'TEST', 'MODE': 'CW', 'QSO_DATE': '20130322'}, {'TIME_ON': '0820', 'BAND': '20m', 'CALL': 'TEST2ABC', 'MODE': 'SSB', 'QSO_DATE': '20150227'}, {'TIME_ON': '0832', 'BAND': '2m', 'CALL': 'HELLO', 'MODE': 'FM', 'QSO_DATE': '20150227'}]
+        print("Imported records: ", records)
+        print("Expected records: ", expected_records)
+        assert(len(records) == 3)
+        for i in range(len(expected_records)):
+            assert(len(list(records[i].keys())) == len(list(expected_records[i].keys())))
+        assert(records == expected_records)
+
+    def test_adif_read_alphabet(self):
+        """ Check that none of the letters of the alphabet are ignored during parsing. """
+        f = open("ADIF.test_read_alphabet.adi", 'w')
+        f.write("""Some test ADI data.<eoh>
 <call:64>ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ<eor>""")
-      f.close()
-    
-      records = self.adif.read("ADIF.test_read_alphabet.adi")
-      expected_records = [{'CALL': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'}]
-      print "Imported records: ", records
-      print "Expected records: ", expected_records
-      assert(len(records) == 1)
-      assert(len(records[0].keys()) == len(expected_records[0].keys()))
-      assert(records == expected_records)
-
-   def test_adif_read_capitalisation(self):
-      """ Check that the CALL field is capitalised correctly. """
-      f = open("ADIF.test_read_capitalisation.adi", 'w')
-      f.write("""Some test ADI data.<eoh>
+        f.close()
+
+        records = self.adif.read("ADIF.test_read_alphabet.adi")
+        expected_records = [{'CALL': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'}]
+        print("Imported records: ", records)
+        print("Expected records: ", expected_records)
+        assert(len(records) == 1)
+        assert(len(list(records[0].keys())) == len(list(expected_records[0].keys())))
+        assert(records == expected_records)
+
+    def test_adif_read_capitalisation(self):
+        """ Check that the CALL field is capitalised correctly. """
+        f = open("ADIF.test_read_capitalisation.adi", 'w')
+        f.write("""Some test ADI data.<eoh>
 <call:4>test<eor>""")
-      f.close()
-    
-      records = self.adif.read("ADIF.test_read_capitalisation.adi")
-      expected_records = [{'CALL': 'TEST'}]
-      print "Imported records: ", records
-      print "Expected records: ", expected_records
-      assert(len(records) == 1)
-      assert(len(records[0].keys()) == len(expected_records[0].keys()))
-      assert(records == expected_records)
-      
-   def test_adif_read_header_only(self):
-      """ Check that no records are read in if the ADIF file only contains header information. """
-      f = open("ADIF.test_read_header_only.adi", 'w')
-      f.write("""Some test ADI data.<eoh>""")
-      f.close()
-    
-      records = self.adif.read("ADIF.test_read_header_only.adi")
-      expected_records = []
-      print "Imported records: ", records
-      print "Expected records: ", expected_records
-      assert(len(records) == 0)
-      assert(records == expected_records)
-
-   def test_adif_read_no_header(self):
-      """ Check that an ADIF file can be parsed with no header information. """
-      f = open("ADIF.test_read_no_header.adi", 'w')
-      f.write("""<call:4>TEST<band:3>40m<mode:2>CW<qso_date:8:d>20130322<time_on:4>1955<eor>""")
-      f.close()
-    
-      records = self.adif.read("ADIF.test_read_no_header.adi")
-      expected_records = [{'TIME_ON': '1955', 'BAND': '40m', 'CALL': 'TEST', 'MODE': 'CW', 'QSO_DATE': '20130322'}]
-      print "Imported records: ", records
-      print "Expected records: ", expected_records
-      assert(len(records) == 1)
-      assert(len(records[0].keys()) == len(expected_records[0].keys()))
-      assert(records == expected_records)
-
-   def test_adif_write(self):
-      """ Check that records can be written to an ADIF file correctly. """
-      records = [{"CALL":"TEST123", "QSO_DATE":"20120402", "TIME_ON":"1234", "FREQ":"145.500", "BAND":"2m", "MODE":"FM", "RST_SENT":"59", "RST_RCVD":"59"},
-                 {"CALL":"TEST123", "QSO_DATE":"20130312", "TIME_ON":"0101", "FREQ":"145.750", "BAND":"2m", "MODE":"FM"}]
-      self.adif.write(records, "ADIF.test_write.adi")
-
-      f = open("ADIF.test_write.adi", 'r')
-      text = f.read()
-      print "File 'ADIF.test_write.adi' contains the following text:", text
-      assert("""        
-<adif_ver:3>1.0
+        f.close()
+
+        records = self.adif.read("ADIF.test_read_capitalisation.adi")
+        expected_records = [{'CALL': 'TEST'}]
+        print("Imported records: ", records)
+        print("Expected records: ", expected_records)
+        assert(len(records) == 1)
+        assert(len(list(records[0].keys())) == len(list(expected_records[0].keys())))
+        assert(records == expected_records)
+
+    def test_adif_read_header_only(self):
+        """ Check that no records are read in if the ADIF file only contains header information. """
+        f = open("ADIF.test_read_header_only.adi", 'w')
+        f.write("""Some test ADI data.<eoh>""")
+        f.close()
+
+        records = self.adif.read("ADIF.test_read_header_only.adi")
+        expected_records = []
+        print("Imported records: ", records)
+        print("Expected records: ", expected_records)
+        assert(len(records) == 0)
+        assert(records == expected_records)
+
+    def test_adif_read_no_header(self):
+        """ Check that an ADIF file can be parsed with no header information. """
+        f = open("ADIF.test_read_no_header.adi", 'w')
+        f.write("""<call:4>TEST<band:3>40m<mode:2>CW<qso_date:8:d>20130322<time_on:4>1955<eor>""")
+        f.close()
+
+        records = self.adif.read("ADIF.test_read_no_header.adi")
+        expected_records = [{'TIME_ON': '1955', 'BAND': '40m', 'CALL': 'TEST', 'MODE': 'CW', 'QSO_DATE': '20130322'}]
+        print("Imported records: ", records)
+        print("Expected records: ", expected_records)
+        assert(len(records) == 1)
+        assert(len(list(records[0].keys())) == len(list(expected_records[0].keys())))
+        assert(records == expected_records)
+
+    def test_adif_write(self):
+        """ Check that records can be written to an ADIF file correctly. """
+        records = [{"CALL": "TEST123", "QSO_DATE": "20120402", "TIME_ON": "1234", "FREQ": "145.500", "BAND": "2m", "MODE": "FM", "RST_SENT": "59", "RST_RCVD": "59"},
+                   {"CALL": "TEST123", "QSO_DATE": "20130312", "TIME_ON": "0101", "FREQ": "145.750", "BAND": "2m", "MODE": "FM"}]
+        self.adif.write(records, "ADIF.test_write.adi")
+
+        f = open("ADIF.test_write.adi", 'r')
+        text = f.read()
+        print("File 'ADIF.test_write.adi' contains the following text:", text)
+        assert("""
+<adif_ver:5>3.0.4
 <programid:5>PyQSO
-<programversion:3>0.2
+<programversion:3>0.3
 <eoh>
 <call:7>TEST123
 <qso_date:8>20120402
@@ -526,30 +664,30 @@ class TestADIF(unittest.TestCase):
 <band:2>2m
 <mode:2>FM
 <eor>
-""" in text) # Ignore the header line here, since it contains the date and time the ADIF file was written, which will change each time 'make unittest' is run.
-      f.close()
-
-   def test_adif_write_sqlite3_Row(self):
-      """ Check that records can be written to an ADIF file from a test database file. """
-      import sqlite3
-      import os.path
-      self.connection = sqlite3.connect(os.path.dirname(os.path.realpath(__file__))+"/unittest_resources/test.db")
-      self.connection.row_factory = sqlite3.Row
-
-      c = self.connection.cursor()
-      c.execute("SELECT * FROM test")
-      records = c.fetchall()
-      print records
-
-      self.adif.write(records, "ADIF.test_write_sqlite3_Row.adi")
-
-      f = open("ADIF.test_write_sqlite3_Row.adi", 'r')
-      text = f.read()
-      print "File 'ADIF.test_write_sqlite3_Row.adi' contains the following text:", text
-      assert("""        
-<adif_ver:3>1.0
+""" in text)  # Ignore the header line here, since it contains the date and time the ADIF file was written, which will change each time 'make unittest' is run.
+        f.close()
+
+    def test_adif_write_sqlite3_Row(self):
+        """ Check that records can be written to an ADIF file from a test database file. """
+        import sqlite3
+        import os.path
+        self.connection = sqlite3.connect(os.path.dirname(os.path.realpath(__file__))+"/unittest_resources/test.db")
+        self.connection.row_factory = sqlite3.Row
+
+        c = self.connection.cursor()
+        c.execute("SELECT * FROM test")
+        records = c.fetchall()
+        print(records)
+
+        self.adif.write(records, "ADIF.test_write_sqlite3_Row.adi")
+
+        f = open("ADIF.test_write_sqlite3_Row.adi", 'r')
+        text = f.read()
+        print("File 'ADIF.test_write_sqlite3_Row.adi' contains the following text:", text)
+        assert("""
+<adif_ver:5>3.0.4
 <programid:5>PyQSO
-<programversion:3>0.2
+<programversion:3>0.3
 <eoh>
 <call:7>TEST123
 <qso_date:8>20120402
@@ -567,18 +705,17 @@ class TestADIF(unittest.TestCase):
 <band:2>2m
 <mode:2>FM
 <eor>
-""" in text) # Ignore the header line here, since it contains the date and time the ADIF file was written, which will change each time 'make unittest' is run.
-      f.close()
+""" in text)  # Ignore the header line here, since it contains the date and time the ADIF file was written, which will change each time 'make unittest' is run.
+        f.close()
 
-      self.connection.close()
+        self.connection.close()
 
-   def test_adif_is_valid(self):
-      """ Check that ADIF field validation is working correctly for different data types. """
-      assert(self.adif.is_valid("CALL", "TEST123", "S") == True)
-      assert(self.adif.is_valid("QSO_DATE", "20120402", "D") == True)
-      assert(self.adif.is_valid("TIME_ON", "1230", "T") == True)
-      assert(self.adif.is_valid("TX_PWR", "5", "N") == True)
+    def test_adif_is_valid(self):
+        """ Check that ADIF field validation is working correctly for different data types. """
+        assert(self.adif.is_valid("CALL", "TEST123", "S"))
+        assert(self.adif.is_valid("QSO_DATE", "20120402", "D"))
+        assert(self.adif.is_valid("TIME_ON", "1230", "T"))
+        assert(self.adif.is_valid("TX_PWR", "5", "N"))
 
 if(__name__ == '__main__'):
-   unittest.main()
-
+    unittest.main()
diff --git a/pyqso/auxiliary_dialogs.py b/pyqso/auxiliary_dialogs.py
index d37a84d..34554d1 100644
--- a/pyqso/auxiliary_dialogs.py
+++ b/pyqso/auxiliary_dialogs.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python 
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,32 +17,56 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject
+from gi.repository import Gtk
 import logging
 
+
 def error(parent, message):
-   """ Display an error message. """
-   logging.error(message)
-   dialog = Gtk.MessageDialog(parent, Gtk.DialogFlags.DESTROY_WITH_PARENT,
-                               Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, message, title="Error")
-   dialog.run()
-   dialog.destroy()
-   return
+    """ Display an error message.
+
+    :arg parent: The Gtk parent window/dialog.
+    :arg str message: The message to display to the user.
+    """
+    logging.error(message)
+    _handle_gtk_dialog(parent, Gtk.MessageType.ERROR, message, "Error")
+
 
 def info(parent, message):
-   """ Display some information. """
-   logging.debug(message)
-   dialog = Gtk.MessageDialog(parent, Gtk.DialogFlags.DESTROY_WITH_PARENT,
-                               Gtk.MessageType.INFO, Gtk.ButtonsType.OK, message, title="Information")
-   dialog.run()
-   dialog.destroy()
-   return
-   
+    """ Display some information.
+
+    :arg parent: The Gtk parent window/dialog.
+    :arg str message: The message to display to the user.
+    """
+    logging.debug(message)
+    _handle_gtk_dialog(parent, Gtk.MessageType.INFO, message, "Information")
+
+
 def question(parent, message):
-   """ Ask the user a question. The dialog comes with 'Yes' and 'No' response buttons. """
-   dialog = Gtk.MessageDialog(parent, Gtk.DialogFlags.DESTROY_WITH_PARENT,
-                              Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, 
-                              message, title="Question")
-   response = dialog.run()
-   dialog.destroy()
-   return response
+    """ Ask the user a question. The dialog comes with 'Yes' and 'No' response buttons.
+
+    :arg parent: The Gtk parent window/dialog.
+    :arg str message: The message to display to the user.
+    :returns: The 'yes'/'no' response from the user.
+    :rtype: Gtk.ResponseType
+    """
+    return _handle_gtk_dialog(parent, Gtk.MessageType.QUESTION, message, "Question")
+
+
+def _handle_gtk_dialog(parent, msgtype, message, title):
+    """
+    Instantiate and present a dialog to the user.
+
+    :arg parent: The Gtk parent window/dialog.
+    :arg Gtk.MessageType msgtype: The type of message to present to the user (e.g. a question, or error message).
+    :arg str message: The message to display in the dialog.
+    :arg str title: The title to display at the top of the dialog.
+    :returns: The response from the user, based on which button they pushed.
+    :rtype: Gtk.ResponseType
+    """
+    bt = Gtk.ButtonsType
+    buttons = bt.YES_NO if msgtype == Gtk.MessageType.QUESTION else bt.OK
+    dialog = Gtk.MessageDialog(parent, Gtk.DialogFlags.DESTROY_WITH_PARENT,
+                               msgtype, buttons, message, title=title)
+    response = dialog.run()
+    dialog.destroy()
+    return response
diff --git a/pyqso/awards.py b/pyqso/awards.py
index bd50a83..9656ef1 100644
--- a/pyqso/awards.py
+++ b/pyqso/awards.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,88 +17,94 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject
+from gi.repository import Gtk
 import logging
 
+
 class Awards(Gtk.VBox):
-   """ A tool for tracking progress towards an award. Currently this only supports the DXCC award. For more information visit http://www.arrl.org/dxcc """
-   
-   def __init__(self, parent):
-      """ Set up a table for progress tracking purposes. """
-      #TODO: This only considers the DXCC award for now.
-      logging.debug("New Awards instance created!")
-         
-      Gtk.VBox.__init__(self, spacing=2)
-
-      self.parent = parent
-
-      self.bands = ["70cm", "2m", "6m", "10m", "12m", "15m", "17m", "20m", "30m", "40m", "80m", "160m"]
-      self.modes = ["Phone", "CW", "Digital", "Mixed"]
-      
-      data_types = [str] + [int]*len(self.bands)
-      self.awards = Gtk.ListStore(*data_types)
-
-      # The main table for the awards
-      self.treeview = Gtk.TreeView(self.awards)
-      # A separate, empty column just for the mode names
-      renderer = Gtk.CellRendererText()
-      column = Gtk.TreeViewColumn("Modes", renderer, text=0)
-      column.set_clickable(False)
-      self.treeview.append_column(column)
-      # Now for all the bands...
-      logging.debug("Initialising the columns in the awards table.")
-      for i in range(0, len(self.bands)):
-         renderer = Gtk.CellRendererText()
-         column = Gtk.TreeViewColumn(self.bands[i], renderer, text=i+1)
-         column.set_min_width(40)
-         column.set_clickable(False)
-         self.treeview.append_column(column)
-
-      # Add a label to inform the user that this only considers the DXCC award for now.
-      label = Gtk.Label(halign=Gtk.Align.START)
-      label.set_markup("<span size=\"x-large\">%s</span>" % "DXCC Award")
-      self.pack_start(label, False, False, 4)
-      # Show the table in the Awards tab
-      self.add(self.treeview)
-      self.show_all()
-
-      logging.debug("Awards table set up successfully.") 
-
-      self.count()
-
-      return
-
-   def count(self):
-      """ Update the table for progress tracking. """
-      logging.debug("Counting the band/mode combinations for the awards table...")
-      # Wipe everything and start again
-      self.awards.clear()
-      # For each mode, add a new list for holding the totals, and initialise the values to zero.
-      count = []
-      for i in range(0, len(self.bands)):
-         count.append([0]*len(self.bands))
-
-      for log in self.parent.logbook.logs:
-         records = log.get_all_records()
-         if(records is not None):
-            for r in records:
-               if(r["BAND"] is not None and r["MODE"] is not None):
-                  if(r["BAND"].lower() in self.bands and r["MODE"] != ""):
-                     band = self.bands.index(r["BAND"].lower())
-                     # Phone modes
-                     if(r["MODE"].upper() in ["FM", "AM", "SSB", "SSTV"]):
-                        count[0][band] += 1
-                     elif(r["MODE"].upper() == "CW"):
-                        count[1][band] += 1
-                     else: 
-                        #FIXME: This assumes that all the other modes in the ADIF list are digital modes. Is this the case?
-                        count[2][band] += 1
-                     count[3][band] += 1 # Keep the total of each column in the "Mixed" mode
-         else:
-            logging.error("Could not update the awards table for '%s' because of a database error." % log.name)
-      # Insert the rows containing the totals
-      for i in range(0, len(self.modes)):
-         self.awards.append([self.modes[i]] + count[i])
-      logging.debug("Awards table updated.") 
-      return
 
+    """ A tool for tracking progress towards an award. Currently this only supports the DXCC award.
+    For more information visit http://www.arrl.org/dxcc """
+
+    def __init__(self, parent):
+        """ Set up a table for progress tracking purposes.
+
+        :arg parent: The parent Gtk window.
+        """
+        # TODO: This only considers the DXCC award for now.
+        logging.debug("New Awards instance created!")
+
+        Gtk.VBox.__init__(self, spacing=2)
+
+        self.parent = parent
+
+        self.bands = ["70cm", "2m", "6m", "10m", "12m", "15m", "17m", "20m", "30m", "40m", "80m", "160m"]
+        self.modes = ["Phone", "CW", "Digital", "Mixed"]
+
+        data_types = [str] + [int]*len(self.bands)
+        self.awards = Gtk.ListStore(*data_types)
+
+        # The main table for the awards
+        self.treeview = Gtk.TreeView(self.awards)
+        # A separate, empty column just for the mode names
+        renderer = Gtk.CellRendererText()
+        column = Gtk.TreeViewColumn("Modes", renderer, text=0)
+        column.set_clickable(False)
+        self.treeview.append_column(column)
+        # Now for all the bands...
+        logging.debug("Initialising the columns in the awards table.")
+        for i in range(0, len(self.bands)):
+            renderer = Gtk.CellRendererText()
+            column = Gtk.TreeViewColumn(self.bands[i], renderer, text=i+1)
+            column.set_min_width(40)
+            column.set_clickable(False)
+            self.treeview.append_column(column)
+
+        # Add a label to inform the user that this only considers the DXCC award for now.
+        label = Gtk.Label(halign=Gtk.Align.START)
+        label.set_markup("<span size=\"x-large\">%s</span>" % "DXCC Award")
+        self.pack_start(label, False, False, 4)
+        # Show the table in the Awards tab
+        self.add(self.treeview)
+        self.show_all()
+
+        logging.debug("Awards table set up successfully.")
+
+        self.count()
+
+        return
+
+    def count(self):
+        """ Update the table for progress tracking. """
+
+        logging.debug("Counting the band/mode combinations for the awards table...")
+        # Wipe everything and start again
+        self.awards.clear()
+        # For each mode, add a new list for holding the totals, and initialise the values to zero.
+        count = []
+        for i in range(0, len(self.bands)):
+            count.append([0]*len(self.bands))
+
+        for log in self.parent.logbook.logs:
+            records = log.get_all_records()
+            if(records is not None):
+                for r in records:
+                    if(r["BAND"] is not None and r["MODE"] is not None):
+                        if(r["BAND"].lower() in self.bands and r["MODE"] != ""):
+                            band = self.bands.index(r["BAND"].lower())
+                            # Phone modes
+                            if(r["MODE"].upper() in ["FM", "AM", "SSB", "SSTV"]):
+                                count[0][band] += 1
+                            elif(r["MODE"].upper() == "CW"):
+                                count[1][band] += 1
+                            else:
+                                # FIXME: This assumes that all the other modes in the ADIF list are digital modes. Is this the case?
+                                count[2][band] += 1
+                            count[3][band] += 1  # Keep the total of each column in the "Mixed" mode
+            else:
+                logging.error("Could not update the awards table for '%s' because of a database error." % log.name)
+        # Insert the rows containing the totals
+        for i in range(0, len(self.modes)):
+            self.awards.append([self.modes[i]] + count[i])
+        logging.debug("Awards table updated.")
+        return
diff --git a/pyqso/callsign_lookup.py b/pyqso/callsign_lookup.py
index 571c0de..1c45983 100644
--- a/pyqso/callsign_lookup.py
+++ b/pyqso/callsign_lookup.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,179 +17,401 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject
 import logging
 import unittest
-import httplib
+import unittest.mock
+import http.client
 from xml.dom import minidom
 
-from auxiliary_dialogs import *
-
-class CallsignLookup():
-   """ Uses qrz.com to lookup details about a particular callsign. """
-
-   def __init__(self, parent):
-      self.parent = parent
-      self.connection = None
-      self.session_key = None
-      logging.debug("New CallsignLookup instance created!")
-      return
-
-   def connect(self, username, password):
-      """ Initiate a session with the qrz.com server. Hopefully this will return a session key. """
-      logging.debug("Connecting to the qrz.com server...")
-      try:
-         self.connection = httplib.HTTPConnection('xmldata.qrz.com')
-         request = '/xml/current/?username=%s;password=%s;agent=pyqso' % (username, password)
-         self.connection.request('GET', request)
-         response = self.connection.getresponse()
-      except:
-         error(parent=self.parent, message="Could not connect to the qrz.com server. Check connection to the internets?")
-         return False
-     
-      xml_data = minidom.parseString(response.read())
-      session_node = xml_data.getElementsByTagName('Session')[0] # There should only be one Session element
-      session_key_node = session_node.getElementsByTagName('Key')
-      if(len(session_key_node) > 0):
-         self.session_key = session_key_node[0].firstChild.nodeValue
-         logging.debug("Successfully connected to the qrz.com server...")
-         connected = True
-      else:
-         connected = False
-
-      # If there are any errors or warnings, print them out
-      session_error_node = session_node.getElementsByTagName('Error')
-      if(len(session_error_node) > 0):
-         session_error = session_error_node[0].firstChild.nodeValue
-         error(parent=self.parent, message=session_error)
-         logging.error(session_error)
-
-      return connected
-
-   def lookup(self, full_callsign, ignore_prefix_suffix = True):
-      """ Parse the XML tree that is returned from the qrz.com XML server to obtain the NAME, ADDRESS, STATE, COUNTRY, DXCC, CQZ, ITUZ, and IOTA field data (if present),
-      and return the data in the dictionary called fields_and_data. """
-      
-      logging.debug("Looking up callsign. The full callsign (with a prefix and/or suffix) is %s" % full_callsign)
-      
-      # Remove any prefix or suffix from the callsign before performing the lookup.
-      if(ignore_prefix_suffix):
-         callsign = self.strip(full_callsign)
-      else:
-         callsign = full_callsign
-                          
-      # Commence lookup.
-      fields_and_data = {"NAME":"", "ADDRESS":"", "STATE":"", "COUNTRY":"", "DXCC":"", "CQZ":"", "ITUZ":"", "IOTA":""}
-      if(self.session_key):
-         request = '/xml/current/?s=%s;callsign=%s' % (self.session_key, callsign)
-         self.connection.request('GET', request)
-         response = self.connection.getresponse()
-
-         xml_data = minidom.parseString(response.read())
-         callsign_node = xml_data.getElementsByTagName('Callsign')
-         if(len(callsign_node) > 0): 
-            callsign_node = callsign_node[0] # There should only be a maximum of one Callsign element
-
-            callsign_fname_node = callsign_node.getElementsByTagName('fname')
-            callsign_name_node = callsign_node.getElementsByTagName('name')
-            if(len(callsign_fname_node) > 0):
-               fields_and_data["NAME"] = callsign_fname_node[0].firstChild.nodeValue
-            if(len(callsign_name_node) > 0): # Add the surname, if present
-               fields_and_data["NAME"] = fields_and_data["NAME"] + " " + callsign_name_node[0].firstChild.nodeValue
-
-            callsign_addr1_node = callsign_node.getElementsByTagName('addr1')
-            callsign_addr2_node = callsign_node.getElementsByTagName('addr2')
-            if(len(callsign_addr1_node) > 0):
-               fields_and_data["ADDRESS"] = callsign_addr1_node[0].firstChild.nodeValue
-            if(len(callsign_addr2_node) > 0): # Add the second line of the address, if present
-               fields_and_data["ADDRESS"] = (fields_and_data["ADDRESS"] + ", " if len(callsign_addr1_node) > 0 else "") + callsign_addr2_node[0].firstChild.nodeValue
-
-            callsign_state_node = callsign_node.getElementsByTagName('state')
-            if(len(callsign_state_node) > 0):
-               fields_and_data["STATE"] = callsign_state_node[0].firstChild.nodeValue
-
-            callsign_country_node = callsign_node.getElementsByTagName('country')
-            if(len(callsign_country_node) > 0):
-               fields_and_data["COUNTRY"] = callsign_country_node[0].firstChild.nodeValue
-
-            callsign_ccode_node = callsign_node.getElementsByTagName('ccode')
-            if(len(callsign_ccode_node) > 0):
-               fields_and_data["DXCC"] = callsign_ccode_node[0].firstChild.nodeValue
-
-            callsign_cqzone_node = callsign_node.getElementsByTagName('cqzone')
-            if(len(callsign_cqzone_node) > 0):
-               fields_and_data["CQZ"] = callsign_cqzone_node[0].firstChild.nodeValue
-
-            callsign_ituzone_node = callsign_node.getElementsByTagName('ituzone')
-            if(len(callsign_ituzone_node) > 0):
-               fields_and_data["ITUZ"] = callsign_ituzone_node[0].firstChild.nodeValue
-
-            callsign_iota_node = callsign_node.getElementsByTagName('iota')
-            if(len(callsign_iota_node) > 0):
-               fields_and_data["IOTA"] = callsign_iota_node[0].firstChild.nodeValue
-         else:
-            # If there is no Callsign element, then print out the error message in the Session element
-            session_node = xml_data.getElementsByTagName('Session')
-            if(len(session_node) > 0): 
-               session_error_node = session_node[0].getElementsByTagName('Error')
-               if(len(session_error_node) > 0):
-                  session_error = session_error_node[0].firstChild.nodeValue
-                  error(parent=self.parent, message=session_error)
-            # Return empty strings for the field data
-         logging.debug("Callsign lookup complete. Returning data...")
-      return fields_and_data
-
-   def strip(self, full_callsign):
-      components = full_callsign.split("/") # We assume that prefixes or suffixes come before/after a forward slash character "/".
-      suffixes = ["P", "M", "A", "PM", "MM", "AM", "QRP"]
-      try:
-         if(len(components) == 3):
+from pyqso.auxiliary_dialogs import *
+
+
+class CallsignLookupQRZ():
+
+    """ Use qrz.com to lookup details about a particular callsign. """
+
+    def __init__(self, parent):
+        """ Initialise a new callsign lookup handler.
+
+        :arg parent: The parent Gtk dialog.
+        """
+        self.parent = parent
+        self.connection = None
+        self.session_key = None
+        return
+
+    def connect(self, username, password):
+        """ Initiate a session with the qrz.com server. Hopefully this will provide a session key.
+
+        :arg str username: The username of the qrz.com user account.
+        :arg str password: The password of the qrz.com user account.
+        :returns: True if a successful connection was made to the server, and False otherwise.
+        :rtype: bool
+        """
+        logging.debug("Connecting to the qrz.com server...")
+        try:
+            self.connection = http.client.HTTPConnection('xmldata.qrz.com')
+            request = '/xml/current/?username=%s;password=%s;agent=pyqso' % (username, password)
+            self.connection.request('GET', request)
+            response = self.connection.getresponse()
+        except:
+            error(parent=self.parent, message="Could not connect to the qrz.com server. Check connection to the internets?")
+            return False
+
+        xml_data = minidom.parseString(response.read())
+        session_node = xml_data.getElementsByTagName('Session')[0]  # There should only be one Session element
+        session_key_node = session_node.getElementsByTagName('Key')
+        if(len(session_key_node) > 0):
+            self.session_key = session_key_node[0].firstChild.nodeValue
+            logging.debug("Successfully connected to the qrz.com server...")
+            connected = True
+        else:
+            connected = False
+
+        # If there are any errors or warnings, print them out
+        session_error_node = session_node.getElementsByTagName('Error')
+        if(len(session_error_node) > 0):
+            session_error = session_error_node[0].firstChild.nodeValue
+            error(parent=self.parent, message="qrz.com session error: "+session_error)
+
+        return connected
+
+    def lookup(self, full_callsign, ignore_prefix_suffix=True):
+        """ Parse the XML tree that is returned from the qrz.com XML server to obtain the NAME, ADDRESS, STATE, COUNTRY, DXCC, CQZ, ITUZ, and IOTA field data (if present).
+
+        :arg str full_callsign: The callsign to look up (without any prefix/suffix stripping).
+        :arg bool ignore_prefix_suffix: True if callsign prefixes/suffixes should be removed prior to querying the server, False otherwise.
+        :returns: The data in a dictionary called fields_and_data.
+        :rtype: dict
+        """
+
+        logging.debug("Looking up callsign. The full callsign (with a prefix and/or suffix) is %s" % full_callsign)
+
+        # Remove any prefix or suffix from the callsign before performing the lookup.
+        if(ignore_prefix_suffix):
+            callsign = strip(full_callsign)
+        else:
+            callsign = full_callsign
+
+        # Commence lookup.
+        fields_and_data = {"NAME": "", "ADDRESS": "", "STATE": "", "COUNTRY": "", "DXCC": "", "CQZ": "", "ITUZ": "", "IOTA": ""}
+        if(self.session_key):
+            request = '/xml/current/?s=%s;callsign=%s' % (self.session_key, callsign)
+            self.connection.request('GET', request)
+            response = self.connection.getresponse()
+
+            xml_data = minidom.parseString(response.read())
+            callsign_node = xml_data.getElementsByTagName('Callsign')
+            if(len(callsign_node) > 0):
+                callsign_node = callsign_node[0]  # There should only be a maximum of one Callsign element
+
+                callsign_fname_node = callsign_node.getElementsByTagName('fname')
+                callsign_name_node = callsign_node.getElementsByTagName('name')
+                if(len(callsign_fname_node) > 0):
+                    fields_and_data["NAME"] = callsign_fname_node[0].firstChild.nodeValue
+                if(len(callsign_name_node) > 0):  # Add the surname, if present
+                    fields_and_data["NAME"] = fields_and_data["NAME"] + " " + callsign_name_node[0].firstChild.nodeValue
+
+                callsign_addr1_node = callsign_node.getElementsByTagName('addr1')
+                callsign_addr2_node = callsign_node.getElementsByTagName('addr2')
+                if(len(callsign_addr1_node) > 0):
+                    fields_and_data["ADDRESS"] = callsign_addr1_node[0].firstChild.nodeValue
+                if(len(callsign_addr2_node) > 0):  # Add the second line of the address, if present
+                    fields_and_data["ADDRESS"] = (fields_and_data["ADDRESS"] + ", " if len(callsign_addr1_node) > 0 else "") + callsign_addr2_node[0].firstChild.nodeValue
+
+                callsign_state_node = callsign_node.getElementsByTagName('state')
+                if(len(callsign_state_node) > 0):
+                    fields_and_data["STATE"] = callsign_state_node[0].firstChild.nodeValue
+
+                callsign_country_node = callsign_node.getElementsByTagName('country')
+                if(len(callsign_country_node) > 0):
+                    fields_and_data["COUNTRY"] = callsign_country_node[0].firstChild.nodeValue
+
+                callsign_ccode_node = callsign_node.getElementsByTagName('ccode')
+                if(len(callsign_ccode_node) > 0):
+                    fields_and_data["DXCC"] = callsign_ccode_node[0].firstChild.nodeValue
+
+                callsign_cqzone_node = callsign_node.getElementsByTagName('cqzone')
+                if(len(callsign_cqzone_node) > 0):
+                    fields_and_data["CQZ"] = callsign_cqzone_node[0].firstChild.nodeValue
+
+                callsign_ituzone_node = callsign_node.getElementsByTagName('ituzone')
+                if(len(callsign_ituzone_node) > 0):
+                    fields_and_data["ITUZ"] = callsign_ituzone_node[0].firstChild.nodeValue
+
+                callsign_iota_node = callsign_node.getElementsByTagName('iota')
+                if(len(callsign_iota_node) > 0):
+                    fields_and_data["IOTA"] = callsign_iota_node[0].firstChild.nodeValue
+            else:
+                # If there is no Callsign element, then print out the error message in the Session element
+                session_node = xml_data.getElementsByTagName('Session')
+                if(len(session_node) > 0):
+                    session_error_node = session_node[0].getElementsByTagName('Error')
+                    if(len(session_error_node) > 0):
+                        session_error = session_error_node[0].firstChild.nodeValue
+                        error(parent=self.parent, message=session_error)
+                # Return empty strings for the field data
+            logging.debug("Callsign lookup complete. Returning data...")
+        return fields_and_data
+
+
+class CallsignLookupHamQTH():
+
+    """ Use hamqth.com to lookup details about a particular callsign. """
+
+    def __init__(self, parent):
+        self.parent = parent
+        self.connection = None
+        self.session_id = None
+        return
+
+    def connect(self, username, password):
+        """ Initiate a session with the hamqth.com server. Hopefully this will provide a session key.
+
+        :arg str username: The username of the hamqth.com user account.
+        :arg str password: The password of the hamqth.com user account.
+        :returns: True if a successful connection was made to the server, and False otherwise.
+        :rtype: bool
+        """
+
+        logging.debug("Connecting to the hamqth.com server...")
+        try:
+            self.connection = http.client.HTTPConnection('www.hamqth.com')
+            request = '/xml.php?u=%s&p=%s' % (username, password)
+            self.connection.request('GET', request)
+            response = self.connection.getresponse()
+        except:
+            error(parent=self.parent, message="Could not connect to the hamqth.com server. Check connection to the internets?")
+            return False
+
+        xml_data = minidom.parseString(response.read())
+        session_node = xml_data.getElementsByTagName('session')[0]  # There should only be one Session element
+        session_id_node = session_node.getElementsByTagName('session_id')
+        if(len(session_id_node) > 0):
+            self.session_id = session_id_node[0].firstChild.nodeValue
+            logging.debug("Successfully connected to the hamqth.com server...")
+            connected = True
+        else:
+            connected = False
+
+        # If there are any errors or warnings, print them out
+        session_error_node = session_node.getElementsByTagName('error')
+        if(len(session_error_node) > 0):
+            session_error = session_error_node[0].firstChild.nodeValue
+            error(parent=self.parent, message="hamqth.com session error: "+session_error)
+
+        return connected
+
+    def lookup(self, full_callsign, ignore_prefix_suffix=True):
+        """ Parse the XML tree that is returned from the hamqth.com XML server to obtain the NAME, ADDRESS, STATE, COUNTRY, DXCC, CQZ, ITUZ, and IOTA field data (if present),
+
+        :arg str full_callsign: The callsign to look up (without any prefix/suffix stripping).
+        :arg bool ignore_prefix_suffix: True if callsign prefixes/suffixes should be removed prior to querying the server, False otherwise.
+        :returns: The data in a dictionary called fields_and_data.
+        :rtype: dict
+        """
+
+        logging.debug("Looking up callsign. The full callsign (with a prefix and/or suffix) is %s" % full_callsign)
+
+        # Remove any prefix or suffix from the callsign before performing the lookup.
+        if(ignore_prefix_suffix):
+            callsign = strip(full_callsign)
+        else:
+            callsign = full_callsign
+
+        # Commence lookup.
+        fields_and_data = {"NAME": "", "ADDRESS": "", "STATE": "", "COUNTRY": "", "DXCC": "", "CQZ": "", "ITUZ": "", "IOTA": ""}
+        if(self.session_id):
+            request = '/xml.php?id=%s&callsign=%s&prg=pyqso' % (self.session_id, callsign)
+            self.connection.request('GET', request)
+            response = self.connection.getresponse()
+
+            xml_data = minidom.parseString(response.read())
+            search_node = xml_data.getElementsByTagName('search')
+            if(len(search_node) > 0):
+                search_node = search_node[0]  # There should only be a maximum of one Callsign element
+
+                search_name_node = search_node.getElementsByTagName('nick')
+                if(len(search_name_node) > 0):
+                    fields_and_data["NAME"] = search_name_node[0].firstChild.nodeValue
+
+                search_addr1_node = search_node.getElementsByTagName('adr_street1')
+                search_addr2_node = search_node.getElementsByTagName('adr_street2')
+                if(len(search_addr1_node) > 0):
+                    fields_and_data["ADDRESS"] = search_addr1_node[0].firstChild.nodeValue
+                if(len(search_addr2_node) > 0):  # Add the second line of the address, if present
+                    fields_and_data["ADDRESS"] = (fields_and_data["ADDRESS"] + ", " if len(search_addr1_node) > 0 else "") + search_addr2_node[0].firstChild.nodeValue
+
+                search_state_node = search_node.getElementsByTagName('us_state')
+                if(len(search_state_node) > 0):
+                    fields_and_data["STATE"] = search_state_node[0].firstChild.nodeValue
+
+                search_country_node = search_node.getElementsByTagName('country')
+                if(len(search_country_node) > 0):
+                    fields_and_data["COUNTRY"] = search_country_node[0].firstChild.nodeValue
+
+                search_cqzone_node = search_node.getElementsByTagName('cq')
+                if(len(search_cqzone_node) > 0):
+                    fields_and_data["CQZ"] = search_cqzone_node[0].firstChild.nodeValue
+
+                search_ituzone_node = search_node.getElementsByTagName('itu')
+                if(len(search_ituzone_node) > 0):
+                    fields_and_data["ITUZ"] = search_ituzone_node[0].firstChild.nodeValue
+
+                search_iota_node = search_node.getElementsByTagName('grid')
+                if(len(search_iota_node) > 0):
+                    fields_and_data["IOTA"] = search_iota_node[0].firstChild.nodeValue
+            else:
+                # If there is no Callsign element, then print out the error message in the Session element
+                session_node = xml_data.getElementsByTagName('session')
+                if(len(session_node) > 0):
+                    session_error_node = session_node[0].getElementsByTagName('error')
+                    if(len(session_error_node) > 0):
+                        session_error = session_error_node[0].firstChild.nodeValue
+                        error(parent=self.parent, message=session_error)
+                # Return empty strings for the field data
+            logging.debug("Callsign lookup complete. Returning data...")
+        return fields_and_data
+
+
+def strip(full_callsign):
+    """ Remove any prefixes or suffixes from a callsign.
+
+    :arg str full_callsign: The callsign to be considered for prefix/suffix removal.
+    :returns: The callsign with prefixes/suffixes removed.
+    :rtype: str
+    """
+
+    components = full_callsign.split("/")  # We assume that prefixes or suffixes come before/after a forward slash character "/".
+    suffixes = ["P", "M", "A", "PM", "MM", "AM", "QRP"]
+    try:
+        if(len(components) == 3):
             # We have both a prefix and a suffix.
             callsign = components[1]
-            
-         elif(len(components) == 2):
+
+        elif(len(components) == 2):
             if(components[1].upper() in suffixes or components[1].lower() in suffixes):
-               # If the last part of the full_callsign is a valid suffix, then use the part before that.
-               callsign = components[0]
-               logging.debug("Suffix %s found. Callsign to lookup is %s" % (components[1], callsign))
+                # If the last part of the full_callsign is a valid suffix, then use the part before that.
+                callsign = components[0]
+                logging.debug("Suffix %s found. Callsign to lookup is %s" % (components[1], callsign))
             else:
-               # We have a prefix, so take the part after the first "/".
-               callsign = components[1]
-               logging.debug("Prefix %s found. Callsign to lookup is %s" % (components[0], callsign))
-               
-         elif(len(components) == 1):
+                # We have a prefix, so take the part after the first "/".
+                callsign = components[1]
+                logging.debug("Prefix %s found. Callsign to lookup is %s" % (components[0], callsign))
+
+        elif(len(components) == 1):
             # We have neither a prefix nor a suffix, so use the full_callsign.
             callsign = full_callsign
             logging.debug("No prefix or suffix found. Callsign to lookup is %s" % callsign)
-            
-         else:
+
+        else:
             raise ValueError
-      except ValueError:
-         callsign = full_callsign
-      return callsign
-      
+    except ValueError:
+        callsign = full_callsign
+    return callsign
+
+
 class TestCallsignLookup(unittest.TestCase):
-   """ The unit tests for the CallsignLookup class. """
-
-   def setUp(self):
-      """ Set up the CallsignLookup object needed for the unit tests. """
-      self.cl = CallsignLookup(parent=None)
-
-   def test_strip(self):
-      callsign = "EA3/MYCALL/MM"
-      assert self.cl.strip(callsign) == "MYCALL"
-      
-   def test_strip_prefix_only(self):
-      callsign = "EA3/MYCALL"
-      assert self.cl.strip(callsign) == "MYCALL"
-      
-   def test_strip_suffix_only(self):
-      callsign = "MYCALL/M"
-      assert self.cl.strip(callsign) == "MYCALL"
-
-   def test_strip_no_prefix_or_suffix(self):
-      callsign = "MYCALL"
-      assert self.cl.strip(callsign) == "MYCALL"
+
+    """ The unit tests for the CallsignLookup class. """
+
+    def setUp(self):
+        """ Set up the objects needed for the unit tests. """
+        self.qrz = CallsignLookupQRZ(parent=None)
+        self.hamqth = CallsignLookupHamQTH(parent=None)
+
+    def tearDown(self):
+        """ Destroy any unit test resources. """
+        pass
+
+    def test_strip(self):
+        """ Check that a callsign with a prefix and a suffix is stripped correctly. """
+        callsign = "EA3/MYCALL/MM"
+        assert strip(callsign) == "MYCALL"
+
+    def test_strip_prefix_only(self):
+        """ Check that a callsign with only a prefix is stripped correctly. """
+        callsign = "EA3/MYCALL"
+        assert strip(callsign) == "MYCALL"
+
+    def test_strip_suffix_only(self):
+        """ Check that a callsign with only a suffix is stripped correctly. """
+        callsign = "MYCALL/M"
+        assert strip(callsign) == "MYCALL"
+
+    def test_strip_no_prefix_or_suffix(self):
+        """ Check that a callsign with no prefix or suffix remains unmodified. """
+        callsign = "MYCALL"
+        assert strip(callsign) == "MYCALL"
+
+    def test_qrz_connect(self):
+        """ Check the example response from the qrz.com server, and make sure the session key has been correctly extracted. """
+
+        http.client.HTTPConnection = unittest.mock.Mock(spec=http.client.HTTPConnection)
+        http.client.HTTPResponse = unittest.mock.Mock(spec=http.client.HTTPResponse)
+        connection = http.client.HTTPConnection()
+        response = http.client.HTTPResponse()
+
+        response.read.return_value = b'<?xml version="1.0" encoding="utf-8" ?>\n<QRZDatabase version="1.33" xmlns="http://xmldata.qrz.com">\n<Session>\n<Key>3b1fd1d3ba495189984f93ff67bd45b6</Key>\n<Count>61</Count>\n<SubExp>non-subscriber</SubExp>\n<GMTime>Sun Nov 22 21:25:34 2015</GMTime>\n<Remark>cpu: 0.147s</Remark>\n</Session>\n</QRZDatabase>\n'
+        connection.getresponse.return_value = response
+
+        result = self.qrz.connect("hello", "world")
+        assert(result)
+        assert(self.qrz.session_key == "3b1fd1d3ba495189984f93ff67bd45b6")
+
+    def test_qrz_lookup(self):
+        """ Check the example callsign lookup response from the qrz.com server, and make sure the callsign information has been correctly extracted. """
+
+        http.client.HTTPConnection = unittest.mock.Mock(spec=http.client.HTTPConnection)
+        http.client.HTTPResponse = unittest.mock.Mock(spec=http.client.HTTPResponse)
+        connection = http.client.HTTPConnection()
+        response = http.client.HTTPResponse()
+
+        response.read.return_value = b'<?xml version="1.0" encoding="utf-8" ?>\n<QRZDatabase version="1.33" xmlns="http://xmldata.qrz.com">\n<Callsign>\n<call>MYCALL</call>\n<fname>FIRSTNAME</fname>\n<name>LASTNAME</name>\n<addr2>ADDRESS2</addr2>\n<country>COUNTRY</country>\n</Callsign>\n<Session>\n<Key>3b1fd1d3ba495189984f93ff67bd45b6</Key>\n<Count>61</Count>\n<SubExp>non-subscriber</SubExp>\n<Message>A subscription is required to access the complete record.</Message>\n<GMTime>Sun Nov 2 [...]
+        connection.getresponse.return_value = response
+
+        self.qrz.connection = connection
+        self.qrz.session_key = "3b1fd1d3ba495189984f93ff67bd45b6"
+        fields_and_data = self.qrz.lookup("MYCALL")
+        assert(fields_and_data["NAME"] == "FIRSTNAME LASTNAME")
+        assert(fields_and_data["ADDRESS"] == "ADDRESS2")
+        assert(fields_and_data["COUNTRY"] == "COUNTRY")
+
+    def test_hamqth_connect(self):
+        """ Check the example response from the hamqth.com server, and make sure the session ID has been correctly extracted. """
+
+        http.client.HTTPConnection = unittest.mock.Mock(spec=http.client.HTTPConnection)
+        http.client.HTTPResponse = unittest.mock.Mock(spec=http.client.HTTPResponse)
+        connection = http.client.HTTPConnection()
+        response = http.client.HTTPResponse()
+
+        response.read.return_value = b'<?xml version="1.0"?>\n<HamQTH version="2.6" xmlns="https://www.hamqth.com">\n<session>\n<session_id>09b0ae90050be03c452ad235a1f2915ad684393c</session_id>\n</session>\n</HamQTH>\n'
+        connection.getresponse.return_value = response
+
+        result = self.hamqth.connect("hello", "world")
+        assert(result)
+        assert(self.hamqth.session_id == "09b0ae90050be03c452ad235a1f2915ad684393c")
+
+    def test_hamqth_lookup(self):
+        """ Check the example callsign lookup response from the hamqth.com server, and make sure the callsign information has been correctly extracted. """
+
+        http.client.HTTPConnection = unittest.mock.Mock(spec=http.client.HTTPConnection)
+        http.client.HTTPResponse = unittest.mock.Mock(spec=http.client.HTTPResponse)
+        connection = http.client.HTTPConnection()
+        response = http.client.HTTPResponse()
+
+        response.read.return_value = b'<?xml version="1.0"?>\n<HamQTH version="2.6" xmlns="https://www.hamqth.com">\n<search>\n<callsign>MYCALL</callsign>\n<nick>NAME</nick>\n<country>COUNTRY</country>\n<itu>ITU</itu>\n<cq>CQ</cq>\n<grid>GRID</grid>\n<adr_street1>ADDRESS</adr_street1>\n</search>\n</HamQTH>\n'
+        connection.getresponse.return_value = response
+
+        self.hamqth.connection = connection
+        self.hamqth.session_id = "09b0ae90050be03c452ad235a1f2915ad684393c"
+        fields_and_data = self.hamqth.lookup("MYCALL")
+        assert(fields_and_data["NAME"] == "NAME")
+        assert(fields_and_data["ADDRESS"] == "ADDRESS")
+        assert(fields_and_data["COUNTRY"] == "COUNTRY")
+        assert(fields_and_data["CQZ"] == "CQ")
+        assert(fields_and_data["ITUZ"] == "ITU")
+        assert(fields_and_data["IOTA"] == "GRID")
 
 if(__name__ == '__main__'):
-   unittest.main()
+    unittest.main()
diff --git a/pyqso/dx_cluster.py b/pyqso/dx_cluster.py
index d32f99c..df14bc4 100644
--- a/pyqso/dx_cluster.py
+++ b/pyqso/dx_cluster.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -19,164 +19,336 @@
 
 from gi.repository import Gtk, GObject
 import logging
-import os
-import os.path
-import sys
 import telnetlib
+import unittest
+import unittest.mock
+import configparser
+import os.path
 
 from pyqso.telnet_connection_dialog import *
 
+BOOKMARKS_FILE = os.path.expanduser('~/.config/pyqso/bookmarks.ini')
+
+
 class DXCluster(Gtk.VBox):
-   """ A tool for connecting to a DX cluster (specifically Telnet-based DX clusters). """
-   
-   def __init__(self, parent):
-      """ Set up the DX cluster's Gtk.VBox, and set up a timer so that PyQSO can retrieve new data from the Telnet server every few seconds. """
-      logging.debug("Setting up the DX cluster...") 
-      Gtk.VBox.__init__(self, spacing=2)
-
-      self.connection = None
-      self.parent = parent
-
-      # Set up the toolbar
-      self.toolbar = Gtk.HBox(spacing=2)
-      self.buttons = {}
-      # Connect
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_CONNECT, Gtk.IconSize.BUTTON)
-      button = Gtk.Button()
-      button.add(icon)
-      button.set_tooltip_text('Connect to Telnet Server')
-      button.connect("clicked", self.telnet_connect)
-      self.toolbar.pack_start(button, False, False, 0)
-      self.buttons["CONNECT"] = button
-
-      # Disconnect
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_DISCONNECT, Gtk.IconSize.BUTTON)
-      button = Gtk.Button()
-      button.add(icon)
-      button.set_tooltip_text('Disconnect from Telnet Server')
-      button.connect("clicked", self.telnet_disconnect)
-      self.toolbar.pack_start(button, False, False, 0)
-      self.buttons["DISCONNECT"] = button
-
-      self.toolbar.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
-
-      self.command = Gtk.Entry()
-      self.toolbar.pack_start(self.command, False, False, 0)
-      self.send = Gtk.Button("Send Command")
-      self.send.connect("clicked", self.telnet_send_command)
-      self.toolbar.pack_start(self.send, False, False, 0)
-
-      self.pack_start(self.toolbar, False, False, 0)
-
-      # A TextView object to display the output from the Telnet server.
-      self.renderer = Gtk.TextView()
-      self.renderer.set_editable(False)
-      self.renderer.set_cursor_visible(False)
-      sw = Gtk.ScrolledWindow()
-      sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
-      sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
-      sw.add(self.renderer)
-      self.buffer = self.renderer.get_buffer()
-      self.pack_start(sw, True, True, 0)
-
-      self.set_connect_button_sensitive(True)
-
-      self.show_all()
-
-      logging.debug("DX cluster ready!") 
-
-      return
-
-   def telnet_connect(self, widget=None):
-      """ Connect to a user-specified Telnet server, with the host and login details specified in the Gtk.Entry boxes in the TelnetConnectionDialog. """
-      dialog = TelnetConnectionDialog(self.parent)
-      response = dialog.run()
-      if(response == Gtk.ResponseType.OK):
-         connection_info = dialog.get_connection_info()
-         host = connection_info["HOST"].get_text()
-         port = connection_info["PORT"].get_text()
-         username = connection_info["USERNAME"].get_text()
-         password = connection_info["PASSWORD"].get_text()
-         dialog.destroy()
-      else:
-         dialog.destroy()
-         return
-
-      if(host == ""):
-         logging.error("No Telnet server specified.")
-         return
-      if(port == ""):
-         port = 23 # The default Telnet port
-      else:
-         port = int(port)
-
-      try:
-         self.connection = telnetlib.Telnet(host, port)
-
-         if(username):
-            self.connection.read_until("login: ")
-            self.connection.write(username + "\n")
-         if(password):
-            self.connection.read_until("password: ")
-            self.connection.write(password + "\n")
-      except:
-         logging.exception("Could not create a connection to the Telnet server")
-         self.connection = None
-         return
-
-      self.set_connect_button_sensitive(False)
-
-      self.check_io_event = GObject.timeout_add(1000, self._on_telnet_io)
-
-      return
-
-   def telnet_disconnect(self, widget=None):
-      """ Disconnect from a Telnet server and remove the I/O timer. """
-      if(self.connection):
-         self.connection.close()
-      self.buffer.set_text("")
-      self.connection = None
-      self.set_connect_button_sensitive(True)
-      GObject.source_remove(self.check_io_event)
-      return
-
-   def telnet_send_command(self, widget=None):
-      """ Send the user-specified command in the Gtk.Entry box to the Telnet server (if PyQSO is connected to one). """
-      if(self.connection):
-         self.connection.write(self.command.get_text() + "\n")
-         self.command.set_text("")
-      return
-
-   def _on_telnet_io(self):
-      """ Retrieve any new data from the Telnet server and print it out in the Gtk.TextView widget. Always returns True to satisfy the GObject timer. """
-      if(self.connection):
-         text = self.connection.read_very_eager()
-         try:
-            text = text.replace(u"\u0007", "") # Remove the BEL Unicode character from the end of the line
-         except UnicodeDecodeError as e:
+
+    """ A tool for connecting to a DX cluster (specifically Telnet-based DX clusters). """
+
+    def __init__(self, parent):
+        """ Set up the DX cluster's Gtk.VBox, and set up a timer so that PyQSO can retrieve new data from the Telnet server every few seconds.
+
+        :arg parent: The parent Gtk window.
+        """
+        logging.debug("Setting up the DX cluster...")
+        Gtk.VBox.__init__(self, spacing=2)
+
+        self.connection = None
+        self.parent = parent
+
+        # Set up the menubar
+        self.menubar = Gtk.MenuBar()
+
+        self.items = {}
+
+        # CONNECTION ######
+        mitem_connection = Gtk.MenuItem(label="Connection")
+        self.menubar.append(mitem_connection)
+        subm_connection = Gtk.Menu()
+        mitem_connection.set_submenu(subm_connection)
+
+        # Connect
+        mitem_connect = Gtk.ImageMenuItem(label="Connect to Telnet Server")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_CONNECT, Gtk.IconSize.MENU)
+        mitem_connect.set_image(icon)
+        subm_connection.append(mitem_connect)
+        self.items["CONNECT"] = mitem_connect
+
+        subm_connect = Gtk.Menu()
+
+        # New
+        mitem_new = Gtk.MenuItem(label="New...")
+        mitem_new.connect("activate", self.new_server)
+        subm_connect.append(mitem_new)
+
+        # From Bookmark
+        mitem_bookmark = Gtk.MenuItem(label="From Bookmark")
+        self.subm_bookmarks = Gtk.Menu()
+        mitem_bookmark.set_submenu(self.subm_bookmarks)
+        self._populate_bookmarks()
+        subm_connect.append(mitem_bookmark)
+
+        mitem_connect.set_submenu(subm_connect)
+
+        # Disconnect
+        mitem_disconnect = Gtk.ImageMenuItem(label="Disconnect from Telnet Server")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_DISCONNECT, Gtk.IconSize.MENU)
+        mitem_disconnect.set_image(icon)
+        mitem_disconnect.connect("activate", self.telnet_disconnect)
+        subm_connection.append(mitem_disconnect)
+        self.items["DISCONNECT"] = mitem_disconnect
+
+        self.pack_start(self.menubar, False, False, 0)
+
+        # A TextView object to display the output from the Telnet server.
+        self.renderer = Gtk.TextView()
+        self.renderer.set_editable(False)
+        self.renderer.set_cursor_visible(False)
+        sw = Gtk.ScrolledWindow()
+        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
+        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+        sw.add(self.renderer)
+        self.buffer = self.renderer.get_buffer()
+        self.pack_start(sw, True, True, 0)
+
+        # Set up the command box.
+        self.commandbox = Gtk.HBox(spacing=2)
+        self.command = Gtk.Entry()
+        self.commandbox.pack_start(self.command, True, True, 0)
+        self.send = Gtk.Button(label="Send Command")
+        self.send.connect("clicked", self.telnet_send_command)
+        self.commandbox.pack_start(self.send, False, False, 0)
+        self.pack_start(self.commandbox, False, False, 0)
+
+        self.set_items_sensitive(True)
+
+        self.show_all()
+
+        logging.debug("DX cluster ready!")
+
+        return
+
+    def new_server(self, widget=None):
+        """ Get Telnet server host and login details specified in the Gtk.Entry boxes in the TelnetConnectionDialog and attempt a connection. """
+        dialog = TelnetConnectionDialog(self.parent)
+        response = dialog.run()
+
+        if(response == Gtk.ResponseType.OK):
+            connection_info = dialog.get_connection_info()
+            host = connection_info["HOST"].get_text()
+            port = connection_info["PORT"].get_text()
+            username = connection_info["USERNAME"].get_text()
+            password = connection_info["PASSWORD"].get_text()
+
+            # Save the server details in a new bookmark, if desired.
+            if(connection_info["BOOKMARK"].get_active()):
+                try:
+                    config = configparser.ConfigParser()
+                    config.read(BOOKMARKS_FILE)
+
+                    # Use the host name as the bookmark's identifier.
+                    try:
+                        config.add_section(host)
+                    except configparser.DuplicateSectionError:
+                        # If the hostname already exists, assume the user wants to update the port number, username and/or password.
+                        logging.warning("A server with hostname '%s' already exists. Over-writing existing details..." % (host))
+                    config.set(host, "host", host)
+                    config.set(host, "port", port)
+                    config.set(host, "username", username)
+                    config.set(host, "password", password)
+
+                    # Write the bookmarks to file.
+                    if not os.path.exists(os.path.expanduser('~/.config/pyqso')):
+                        os.makedirs(os.path.expanduser('~/.config/pyqso'))
+                    with open(BOOKMARKS_FILE, 'w') as f:
+                        config.write(f)
+
+                    self._populate_bookmarks()
+
+                except IOError:
+                    # Maybe the bookmarks file could not be written to?
+                    logging.error("Bookmark could not be saved. Check bookmarks file permissions? Going ahead with the server connection anyway...")
+
+            dialog.destroy()
+
+            try:
+                # Convert port (currently of type str) into an int.
+                port = int(port)
+                # Attempt a connection with the server.
+                self.telnet_connect(host, port, username, password)
+            except ValueError as e:
+                logging.error("Could not convert the server's port information to an integer.")
+                logging.exception(e)
+
+        else:
+            dialog.destroy()
+        return
+
+    def _populate_bookmarks(self):
+        """ Populate the list of bookmarked Telnet servers in the menu. """
+        config = configparser.ConfigParser()
+        have_config = (config.read(BOOKMARKS_FILE) != [])
+
+        if(have_config):
+            try:
+                # Clear the menu of all current bookmarks.
+                for i in self.subm_bookmarks.get_children():
+                    self.subm_bookmarks.remove(i)
+
+                # Add all bookmarks in the config file.
+                for bookmark in config.sections():
+                    mitem = Gtk.MenuItem(label=bookmark)
+                    mitem.connect("activate", self.bookmarked_server, bookmark)
+                    self.subm_bookmarks.append(mitem)
+
+            except Exception as e:
+                logging.error("An error occurred whilst populating the DX cluster bookmarks menu.")
+                logging.exception(e)
+
+            self.show_all()  # Need to do this to update the bookmarks list in the menu.
+
+        return
+
+    def bookmarked_server(self, widget, name):
+        """ Get Telnet server host and login details from an existing bookmark and attempt a connection.
+
+        :arg str name: The name of the bookmark. This is the same as the server's hostname.
+        """
+
+        config = configparser.ConfigParser()
+        have_config = (config.read(BOOKMARKS_FILE) != [])
+        try:
+            if(not have_config):
+                raise IOError("The bookmark's details could not be loaded.")
+
+            host = config.get(name, "host")
+            port = int(config.get(name, "port"))
+            username = config.get(name, "username")
+            password = config.get(name, "password")
+            self.telnet_connect(host, port, username, password)
+
+        except ValueError as e:
+            # This exception may occur when casting the port (which is a str) to an int.
+            logging.exception(e)
+        except IOError as e:
+            logging.exception(e)
+        except Exception as e:
+            logging.error("Could not connect to Telnet server '%s'" % name)
+            logging.exception(e)
+
+        return
+
+    def telnet_connect(self, host, port=23, username=None, password=None):
+        """ Connect to a user-specified Telnet server.
+
+        :arg str host: The Telnet server's hostname.
+        :arg int port: The Telnet server's port number. If no port is specified, the default Telnet server port of 23 will be used.
+        :arg str username: The user's username. This is an optional argument.
+        :arg str password: The user's password. This is an optional argument.
+        """
+
+        if(host == "" or host is None):
+            logging.error("No Telnet server specified.")
+            return
+        if(port == "" or port is None):
+            port = 23  # Use the default Telnet port
+
+        try:
+            self.connection = telnetlib.Telnet(host, port)
+
+            if(username):
+                self.connection.read_until("login: ".encode())
+                self.connection.write((username + "\n").encode())
+            if(password):
+                self.connection.read_until("password: ".encode())
+                self.connection.write((password + "\n").encode())
+        except Exception as e:
+            logging.error("Could not create a connection to the Telnet server")
+            logging.exception(e)
+            self.connection = None
+            return
+
+        self.set_items_sensitive(False)
+
+        self.check_io_event = GObject.timeout_add(1000, self._on_telnet_io)
+
+        return
+
+    def telnet_disconnect(self, widget=None):
+        """ Disconnect from a Telnet server and remove the I/O timer. """
+        if(self.connection):
+            self.connection.close()
+        self.buffer.set_text("")
+        self.connection = None
+        self.set_items_sensitive(True)
+
+        # Stop checking for server output once disconnected.
+        try:
+            GObject.source_remove(self.check_io_event)
+        except AttributeError:
+            # This may happen if a connection hasn't yet been established.
             pass
-            
-         # Allow auto-scrolling to the new text entry if the focus is already at
-         # the very end of the Gtk.TextView. Otherwise, don't auto-scroll
-         # in case the user is reading something further up.
-         # Note: This is based on the code from http://forums.gentoo.org/viewtopic-t-445598-view-next.html
-         end_iter = self.buffer.get_end_iter()
-         end_mark = self.buffer.create_mark(None, end_iter)
-         self.renderer.move_mark_onscreen(end_mark)
-         at_end = self.buffer.get_iter_at_mark(end_mark).equal(end_iter)
-         self.buffer.insert(end_iter, text)
-         if(at_end):
+
+        return
+
+    def telnet_send_command(self, widget=None):
+        """ Send the user-specified command in the Gtk.Entry box to the Telnet server (if PyQSO is connected to one). """
+        if(self.connection):
+            self.connection.write((self.command.get_text() + "\n").encode())
+            self.command.set_text("")
+        return
+
+    def _on_telnet_io(self):
+        """ Retrieve any new data from the Telnet server and print it out in the Gtk.TextView widget.
+
+        :returns: Always returns True to satisfy the GObject timer.
+        :rtype: bool
+        """
+        if(self.connection):
+            text = self.connection.read_very_eager().decode()
+            try:
+                text = text.replace("\u0007", "")  # Remove the BEL Unicode character from the end of the line
+            except UnicodeDecodeError:
+                pass
+
+            # Allow auto-scrolling to the new text entry if the focus is already at
+            # the very end of the Gtk.TextView. Otherwise, don't auto-scroll
+            # in case the user is reading something further up.
+            # Note: This is based on the code from http://forums.gentoo.org/viewtopic-t-445598-view-next.html
+            end_iter = self.buffer.get_end_iter()
             end_mark = self.buffer.create_mark(None, end_iter)
-            self.renderer.scroll_mark_onscreen(end_mark) 
+            self.renderer.move_mark_onscreen(end_mark)
+            at_end = self.buffer.get_iter_at_mark(end_mark).equal(end_iter)
+            self.buffer.insert(end_iter, text)
+            if(at_end):
+                end_mark = self.buffer.create_mark(None, end_iter)
+                self.renderer.scroll_mark_onscreen(end_mark)
+
+        return True
+
+    def set_items_sensitive(self, sensitive):
+        """ Enable/disable the relevant buttons for connecting/disconnecting from a DX cluster, so that users cannot click the connect button if PyQSO is already connected.
+
+        :arg bool sensitive: If True, enable the Connect button and disable the Disconnect button. If False, vice versa.
+        """
+        self.items["CONNECT"].set_sensitive(sensitive)
+        self.items["DISCONNECT"].set_sensitive(not sensitive)
+        self.send.set_sensitive(not sensitive)
+        return
+
+
+class TestDXCluster(unittest.TestCase):
+
+    """ The unit tests for the DXCluster class. """
+
+    def setUp(self):
+        """ Set up the objects needed for the unit tests. """
+        self.dxcluster = DXCluster(parent=None)
+
+    def tearDown(self):
+        """ Destroy any unit test resources. """
+        pass
 
-      return True
+    def test_on_telnet_io(self):
+        """ Check that the response from the Telnet server can be correctly decoded. """
 
-   def set_connect_button_sensitive(self, sensitive):
-      """ Enable/disable the relevant buttons for connecting/disconnecting from a DX cluster, so that users cannot click the connect button if PyQSO is already connected. """
-      self.buttons["CONNECT"].set_sensitive(sensitive)
-      self.buttons["DISCONNECT"].set_sensitive(not sensitive)
-      self.send.set_sensitive(not sensitive)
-      return
+        telnetlib.Telnet = unittest.mock.Mock(spec=telnetlib.Telnet)
+        connection = telnetlib.Telnet("hello", "world")
+        connection.read_very_eager.return_value = b"Test message from the Telnet server."
+        self.dxcluster.connection = connection
+        result = self.dxcluster._on_telnet_io()
+        assert(result)
 
+if(__name__ == '__main__'):
+    unittest.main()
diff --git a/pyqso/grey_line.py b/pyqso/grey_line.py
index 1d550d2..42edf28 100644
--- a/pyqso/grey_line.py
+++ b/pyqso/grey_line.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -21,64 +21,75 @@ from gi.repository import Gtk, GObject
 import logging
 from datetime import datetime
 try:
-   import numpy
-   import matplotlib
-   logging.debug("Using version %s of matplotlib." % (matplotlib.__version__))
-   matplotlib.use('Agg')
-   matplotlib.rcParams['font.size'] = 10.0
-   from mpl_toolkits.basemap import Basemap
-   from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
-   have_necessary_modules = True
-except ImportError:
-   logging.error("Could not import a non-standard Python module needed by the GreyLine class, or the version of the non-standard module is too old. Check that all the PyQSO dependencies are satisfied.")
-   have_necessary_modules = False
+    import numpy
+    logging.info("Using version %s of numpy." % (numpy.__version__))
+    import matplotlib
+    logging.info("Using version %s of matplotlib." % (matplotlib.__version__))
+    matplotlib.use('Agg')
+    matplotlib.rcParams['font.size'] = 10.0
+    import mpl_toolkits.basemap
+    logging.info("Using version %s of mpl_toolkits.basemap." % (mpl_toolkits.basemap.__version__))
+    from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
+    have_necessary_modules = True
+except ImportError as e:
+    logging.warning(e)
+    logging.warning("Could not import a non-standard Python module needed by the GreyLine class, or the version of the non-standard module is too old. Check that all the PyQSO dependencies are satisfied.")
+    have_necessary_modules = False
+
 
 class GreyLine(Gtk.VBox):
-   """ A tool for visualising the grey line. """
-   
-   def __init__(self, parent):
-      """ Set up the drawing canvas and the timer which will re-plot the grey line every 30 minutes. """
-      logging.debug("Setting up the grey line...") 
-      Gtk.VBox.__init__(self, spacing=2)
-      self.parent = parent
-
-      if(have_necessary_modules):
-         self.fig = matplotlib.figure.Figure()
-         self.canvas = FigureCanvas(self.fig) # For embedding in the Gtk application
-         self.pack_start(self.canvas, True, True, 0)
-         self.refresh_event = GObject.timeout_add(1800000, self.draw) # Re-draw the grey line automatically after 30 minutes (if the grey line tool is visible).
-
-      self.show_all()
-
-      logging.debug("Grey line ready!") 
-
-      return
-
-   def draw(self):
-      """ Draw the world map and the grey line on top of it. This method always returns True to satisfy the GObject timer. """
-
-      if(have_necessary_modules):
-         if(self.parent.toolbox.tools.get_current_page() != 1 or not self.parent.toolbox.get_visible()):
-            # Don't re-draw if the grey line is not visible.
-            return True # We need to return True in case this is method was called by a timer event.
-         else:
-            logging.debug("Drawing the grey line...") 
-            # Re-draw the grey line
-            self.fig.clf()
-            sub = self.fig.add_subplot(111)
-
-            # Draw the map of the world. This is based on the example from:
-            # http://matplotlib.org/basemap/users/examples.html
-            m = Basemap(projection='mill', lon_0=0, ax=sub, resolution='c', fix_aspect=False)
-            m.drawcountries(linewidth=0.5)
-            m.drawcoastlines(linewidth=0.5)
-            m.drawparallels(numpy.arange(-90, 90, 30), labels=[1, 0, 0, 0])
-            m.drawmeridians(numpy.arange(m.lonmin, m.lonmax+30, 60), labels=[0, 0, 0, 1])
-            m.drawmapboundary(fill_color='lightblue')
-            m.fillcontinents(color='darkgreen', lake_color='lightblue')
-            m.nightshade(datetime.utcnow()) # Add in the grey line using UTC time. Note that this requires NetCDF.
-            logging.debug("Grey line drawn.") 
-            return True
-      else:
-         return False # Don't try to re-draw the canvas if the necessary modules to do so could not be imported.
 
+    """ A tool for visualising the grey line. """
+
+    def __init__(self, parent):
+        """ Set up the drawing canvas and the timer which will re-plot the grey line every 30 minutes.
+
+        :arg parent: The parent Gtk window.
+        """
+        logging.debug("Setting up the grey line...")
+        Gtk.VBox.__init__(self, spacing=2)
+        self.parent = parent
+
+        if(have_necessary_modules):
+            self.fig = matplotlib.figure.Figure()
+            self.canvas = FigureCanvas(self.fig)  # For embedding in the Gtk application
+            self.pack_start(self.canvas, True, True, 0)
+            self.refresh_event = GObject.timeout_add(1800000, self.draw)  # Re-draw the grey line automatically after 30 minutes (if the grey line tool is visible).
+
+        self.show_all()
+
+        logging.debug("Grey line ready!")
+
+        return
+
+    def draw(self):
+        """ Draw the world map and the grey line on top of it.
+
+        :returns: Always returns True to satisfy the GObject timer, unless the necessary GreyLine dependencies are not satisfied (in which case, the method returns False so as to not re-draw the canvas).
+        :rtype: bool
+        """
+
+        if(have_necessary_modules):
+            if(self.parent.toolbox.tools.get_current_page() != 1 or not self.parent.toolbox.get_visible()):
+                # Don't re-draw if the grey line is not visible.
+                return True  # We need to return True in case this is method was called by a timer event.
+            else:
+                logging.debug("Drawing the grey line...")
+                # Re-draw the grey line
+                self.fig.clf()
+                sub = self.fig.add_subplot(111)
+
+                # Draw the map of the world. This is based on the example from:
+                # http://matplotlib.org/basemap/users/examples.html
+                m = mpl_toolkits.basemap.Basemap(projection='mill', lon_0=0, ax=sub, resolution='c', fix_aspect=False)
+                m.drawcountries(linewidth=0.5)
+                m.drawcoastlines(linewidth=0.5)
+                m.drawparallels(numpy.arange(-90, 90, 30), labels=[1, 0, 0, 0])
+                m.drawmeridians(numpy.arange(m.lonmin, m.lonmax+30, 60), labels=[0, 0, 0, 1])
+                m.drawmapboundary(fill_color='lightblue')
+                m.fillcontinents(color='darkgreen', lake_color='lightblue')
+                m.nightshade(datetime.utcnow())  # Add in the grey line using UTC time. Note that this requires NetCDF.
+                logging.debug("Grey line drawn.")
+                return True
+        else:
+            return False  # Don't try to re-draw the canvas if the necessary modules to do so could not be imported.
diff --git a/pyqso/log.py b/pyqso/log.py
index 5788fe8..7125e47 100644
--- a/pyqso/log.py
+++ b/pyqso/log.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python 
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,372 +17,442 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject
-from os.path import basename
+from gi.repository import Gtk
 import logging
 import sqlite3 as sqlite
 import unittest
 
-from adif import AVAILABLE_FIELD_NAMES_TYPES, AVAILABLE_FIELD_NAMES_ORDERED
-from record_dialog import *
+from pyqso.adif import AVAILABLE_FIELD_NAMES_ORDERED
+from pyqso.record_dialog import *
+
 
 class Log(Gtk.ListStore):
-   """ A Log object can store multiple Record objects. """
-   
-   def __init__(self, connection, name):
-
-      # The ListStore constructor needs to know the data types of the columns.
-      # The index is always an integer. We will assume the fields are strings.
-      data_types = [int] + [str]*len(AVAILABLE_FIELD_NAMES_ORDERED)    
-      # Call the constructor of the super class (Gtk.ListStore)
-      Gtk.ListStore.__init__(self, *data_types)
-
-      self.connection = connection
-      self.name = name
-      
-      logging.debug("New Log instance created!")
-      return
-
-   def populate(self):
-      """ Remove everything in the Gtk.ListStore that is rendered already (via the TreeView), and start afresh. """
-
-      logging.debug("Populating '%s'..." % self.name)
-      self.add_missing_db_columns()
-      self.clear()
-      records = self.get_all_records()
-      if(records is not None):
-         for r in records:
-            liststore_entry = [r["id"]]
-            for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
-               # Note: r may contain column names that are not in AVAILABLE_FIELD_NAMES_ORDERED, 
-               # so we need to loop over and only select those that are, since the ListStore will
-               # expect a specific number of columns.
-               liststore_entry.append(r[field_name])
-            self.append(liststore_entry)
-         logging.debug("Finished populating '%s'." % self.name)
-      else:
-         logging.error("Could not populate '%s' because of a database error." % self.name)
-      return
-
-   def add_missing_db_columns(self):
-      """ Check whether each field name in AVAILABLE_FIELD_NAMES_ORDERED is in the database table. If not, add it
-      (with all entries being set to an empty string initially). """
-      logging.debug("Adding any missing database columns...")
-
-      # Get all the column names in the current database table.
-      column_names = []
-      try:
-         with self.connection:
-            c = self.connection.cursor()
-            c.execute("PRAGMA table_info(%s)" % self.name) 
-            result = c.fetchall()
-         for t in result:
-            column_names.append(t[1].upper())
-      except (sqlite.Error, IndexError) as e:
-         logging.exception(e)
-         logging.error("Could not obtain the database column names.")
-         return
-
-      for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
-         if(not(field_name in column_names)):
-            try:
-               with self.connection:
-                  c.execute("ALTER TABLE %s ADD COLUMN %s TEXT DEFAULT \"\"" % (self.name, field_name.lower()))
-            except sqlite.Error as e:
-               logging.exception(e)
-               logging.error("Could not add the missing database column '%s'." % field_name)
-               pass
-      logging.debug("Finished adding any missing database columns.")
-      return
-
-   def add_record(self, fields_and_data):
-      """ Add a record comprising data given in the 'fields_and_data' argument to the log. """
-      logging.debug("Adding record to log...")
-      liststore_entry = []
-      field_names = AVAILABLE_FIELD_NAMES_ORDERED
-      for i in range(0, len(field_names)):
-         if(field_names[i] in fields_and_data.keys()):
-            liststore_entry.append(fields_and_data[field_names[i]])
-         else:
-            liststore_entry.append("")
-
-      try:
-         with self.connection:
+
+    """ A single log inside of the whole logbook. A Log object can store multiple Record objects. """
+
+    def __init__(self, connection, name):
+        """ Set up a new Log object.
+
+        :arg connection: An sqlite database connection.
+        :arg str name: The name of the log (i.e. the sqlite table name).
+        """
+
+        # The ListStore constructor needs to know the data types of the columns.
+        # The index is always an integer. We will assume the fields are strings.
+        data_types = [int] + [str]*len(AVAILABLE_FIELD_NAMES_ORDERED)
+        # Call the constructor of the super class (Gtk.ListStore)
+        Gtk.ListStore.__init__(self, *data_types)
+
+        self.connection = connection
+        self.name = name
+
+        logging.debug("New Log instance created!")
+        return
+
+    def populate(self):
+        """ Remove everything in the Gtk.ListStore that is rendered already (via the TreeView), and start afresh. """
+
+        logging.debug("Populating '%s'..." % self.name)
+        self.add_missing_db_columns()
+        self.clear()
+        records = self.get_all_records()
+        if(records is not None):
+            for r in records:
+                liststore_entry = [r["id"]]
+                for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
+                    # Note: r may contain column names that are not in AVAILABLE_FIELD_NAMES_ORDERED,
+                    # so we need to loop over and only select those that are, since the ListStore will
+                    # expect a specific number of columns.
+                    liststore_entry.append(r[field_name])
+                self.append(liststore_entry)
+            logging.debug("Finished populating '%s'." % self.name)
+        else:
+            logging.error("Could not populate '%s' because of a database error." % self.name)
+        return
+
+    def add_missing_db_columns(self):
+        """ Check whether each field name in AVAILABLE_FIELD_NAMES_ORDERED is in the database table. If not, add it
+        (with all entries being set to an empty string initially).
+
+        :raises sqlite.Error, IndexError: if the existing database column names could not be obtained, or missing column names could not be added.
+        """
+        logging.debug("Adding any missing database columns...")
+
+        # Get all the column names in the current database table.
+        column_names = []
+        try:
+            with self.connection:
+                c = self.connection.cursor()
+                c.execute("PRAGMA table_info(%s)" % self.name)
+                result = c.fetchall()
+            for t in result:
+                column_names.append(t[1].upper())
+        except (sqlite.Error, IndexError) as e:
+            logging.exception(e)
+            logging.error("Could not obtain the database column names.")
+            return
+
+        for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
+            if(not(field_name in column_names)):
+                try:
+                    with self.connection:
+                        c.execute("ALTER TABLE %s ADD COLUMN %s TEXT DEFAULT \"\"" % (self.name, field_name.lower()))
+                except sqlite.Error as e:
+                    logging.exception(e)
+                    logging.error("Could not add the missing database column '%s'." % field_name)
+                    pass
+        logging.debug("Finished adding any missing database columns.")
+        return
+
+    def add_record(self, fields_and_data):
+        """ Add a record (or multiple records) to the log.
+
+        :arg fields_and_data: A list of dictionaries (or possibly just a single dictionary), with each dictionary representing a single QSO, to be added to the log.
+        """
+        logging.debug("Adding record(s) to log...")
+
+        # If a dictionary is given, assume that we only have one record to add.
+        if isinstance(fields_and_data, dict):
+            fields_and_data = [fields_and_data]
+
+        with self.connection:
+            # Get all the column names in the current database table.
             c = self.connection.cursor()
+            c.execute("PRAGMA table_info(%s)" % self.name)
+            column_names = c.fetchall()
+            # Get the index of the last inserted record in the database.
+            c.execute('SELECT max(id) FROM %s' % self.name)
+            last_index = c.fetchone()[0]
+            if last_index is None:
+                # Assume no records are currently present.
+                last_index = 0
+
+        # A list of all the database entries, to be inserted in one go into the database.
+        database_entries = []
+
+        # Construct the SQL query.
+        query = "INSERT INTO %s VALUES (NULL" % self.name
+        for i in range(len(column_names)-1):  # -1 here because we don't want to count the database's 'id' field.
+            query = query + ",?"
+        query = query + ")"
+
+        # Gather all the records (making sure that the entries of each record are in the correct order).
+        for r in range(len(fields_and_data)):
             # What if the database columns are not necessarily in the same order as (or even exist in) AVAILABLE_FIELD_NAMES_ORDERED?
             # PyQSO handles this here, but needs a separate list (called database_entry) to successfully perform the SQL query.
             database_entry = []
-            c.execute("PRAGMA table_info(%s)" % self.name) # Get all the column names in the current database table.
-            column_names = c.fetchall()
-            query = "INSERT INTO %s VALUES (NULL" % self.name
             for t in column_names:
-               # 't' here is a tuple
-               column_name = str(t[1])
-               if( (column_name.upper() in AVAILABLE_FIELD_NAMES_ORDERED) and (column_name.upper() in fields_and_data.keys()) ):
-                  database_entry.append(fields_and_data[column_name.upper()])
-                  query = query + ",?"
-               else:
-                  if(column_name != "id"): # Ignore the row index field. This is a special case since it's not in AVAILABLE_FIELD_NAMES_ORDERED.
-                     query = query + ",\"\""
-            query = query + ")"
-            c.execute(query, database_entry)
-            index = c.lastrowid
-
-         liststore_entry.insert(0, index) # Add the record's index.
-         self.append(liststore_entry)
-         logging.debug("Successfully added the record to the log.")
-      except (sqlite.Error, IndexError) as e:
-         logging.exception(e)
-         logging.error("Could not add the record to the log.")
-      return
-
-   def delete_record(self, index, iter=None):
-      """ Delete a record with a specific index in the SQL database. The corresponding record is also deleted from the Gtk.ListStore data structure. Note that iter should always be given. It is given a default value of None for unit testing purposes only. """
-      logging.debug("Deleting record from log...")
-      # Get the selected row in the logbook
-      try:
-         with self.connection:
-            c = self.connection.cursor()
-            query = "DELETE FROM %s" % self.name
-            c.execute(query+" WHERE id=?", [index])
-         if(iter is not None):
-            self.remove(iter)
-         logging.debug("Successfully deleted the record from the log.")
-      except (sqlite.Error, IndexError) as e:
-         logging.exception(e)
-         logging.error("Could not delete the record from the log.")
-      return
-
-   def edit_record(self, index, field_name, data, iter=None, column_index=None):
-      """ Edit a specified record by replacing the data in the field 'field_name' with the data given in the argument called 'data'. Note that both iter and column_index should always be given. These are given default values of None for unit testing purposes only. """
-      logging.debug("Editing field '%s' in record %d..." % (field_name, index))
-      try:
-         with self.connection:
-            c = self.connection.cursor()
-            query = "UPDATE %s SET %s" % (self.name, field_name)
-            query = query + "=? WHERE id=?"
-            c.execute(query, [data, index]) # First update the SQL database...
-         if(iter is not None and column_index is not None):
-            self.set(iter, column_index, data) # ...and then the ListStore.
-         logging.debug("Successfully edited field '%s' in record %d in the log." % (field_name, index))
-      except (sqlite.Error, IndexError) as e:
-         logging.exception(e)
-         logging.error("Could not edit field %s in record %d in the log." % (field_name, index))
-      return
-
-   def remove_duplicates(self):
-      """ Remove any duplicate records from the log. Return the total number of duplicates, and the number of duplicates that were successfully removed. Hopefully these will be the same. """
-      duplicates = self.get_duplicates()
-      if(len(duplicates) == 0):
-         return (0, 0) # Nothing to do here.
-
-      removed = 0 # Count the number of records that are removed. Hopefully this will be the same as len(duplicates).
-      while removed != len(duplicates): # Unfortunately, in certain cases, extra passes may be necessary to ensure that all duplicates are removed.
-         path = Gtk.TreePath(0) # Start with the first row in the log.
-         iter = self.get_iter(path)
-         while iter is not None:
-            row_index = self.get_value(iter, 0) # Get the index.
-            if(row_index in duplicates): # Is this a duplicate row? If so, delete it.
-               self.delete_record(row_index, iter)
-               removed += 1
-            iter = self.iter_next(iter) # Move on to the next row, until iter_next returns None.
-
-      assert(removed == len(duplicates))
-      return (len(duplicates), removed)
-
-   def get_duplicates(self):
-      """ Find the duplicates in the log, based on the CALL, QSO_DATE, TIME_ON, FREQ and MODE fields, and return a list of their row IDs. """
-      duplicates = []
-      try:
-         with self.connection:
-            c = self.connection.cursor()
-            c.execute(
-   """SELECT rowid FROM %s WHERE rowid NOT IN
+                column_name = str(t[1])  # 't' here is a tuple
+                if((column_name.upper() in AVAILABLE_FIELD_NAMES_ORDERED) and (column_name.upper() in list(fields_and_data[r].keys()))):
+                    database_entry.append(fields_and_data[r][column_name.upper()])
+                else:
+                    if(column_name != "id"):  # Ignore the row index field. This is a special case since it's not in AVAILABLE_FIELD_NAMES_ORDERED.
+                        database_entry.append("")
+            database_entries.append(database_entry)
+
+            # Add the data to the ListStore as well.
+            liststore_entry = []
+            field_names = AVAILABLE_FIELD_NAMES_ORDERED
+            for i in range(0, len(field_names)):
+                if(field_names[i] in list(fields_and_data[r].keys())):
+                    liststore_entry.append(fields_and_data[r][field_names[i]])
+                else:
+                    liststore_entry.append("")
+
+            # Add the record's index.
+            index = last_index + (r+1)  # +1 here because r begins at zero, and we don't want to count the already-present record with index last_index.
+            liststore_entry.insert(0, index)
+            self.append(liststore_entry)
+
+        # Execute the query.
+        with self.connection:
+            c.executemany(query, database_entries)
+
+        logging.debug("Successfully added the record(s) to the log.")
+        return
+
+    def delete_record(self, index, iter=None):
+        """ Delete a specified record from the log. The corresponding record is also deleted from the Gtk.ListStore data structure.
+
+        :arg int index: The index of the record in the SQL database.
+        :arg iter: iter should always be given. It is given a default value of None for unit testing purposes only.
+        :raises sqlite.Error, IndexError: if the record could not be deleted.
+        """
+        logging.debug("Deleting record from log...")
+        # Get the selected row in the logbook
+        try:
+            with self.connection:
+                c = self.connection.cursor()
+                query = "DELETE FROM %s" % self.name
+                c.execute(query+" WHERE id=?", [index])
+            if(iter is not None):
+                self.remove(iter)
+            logging.debug("Successfully deleted the record from the log.")
+        except (sqlite.Error, IndexError) as e:
+            logging.exception(e)
+            logging.error("Could not delete the record from the log.")
+        return
+
+    def edit_record(self, index, field_name, data, iter=None, column_index=None):
+        """ Edit a specified record by replacing the current data in a specified field with the data provided.
+
+        :arg int index: The index of the record in the SQL database.
+        :arg str field_name: The name of the field whose data should be modified.
+        :arg str data: The data that should replace the current data in the field.
+        :arg iter: Should always be given. A default value of None is used for unit testing purposes only.
+        :arg column_index: Should always be given. A default value of None is used for unit testing purposes only.
+        :raises sqlite.Error, IndexError: if the record could not be edited.
+        """
+        logging.debug("Editing field '%s' in record %d..." % (field_name, index))
+        try:
+            with self.connection:
+                c = self.connection.cursor()
+                query = "UPDATE %s SET %s" % (self.name, field_name)
+                query = query + "=? WHERE id=?"
+                c.execute(query, [data, index])  # First update the SQL database...
+            if(iter is not None and column_index is not None):
+                self.set(iter, column_index, data)  # ...and then the ListStore.
+            logging.debug("Successfully edited field '%s' in record %d in the log." % (field_name, index))
+        except (sqlite.Error, IndexError) as e:
+            logging.exception(e)
+            logging.error("Could not edit field %s in record %d in the log." % (field_name, index))
+        return
+
+    def remove_duplicates(self):
+        """ Remove any duplicate records from the log.
+
+        :returns: The total number of duplicates, and the number of duplicates that were successfully removed. Hopefully these will be the same.
+        :rtype: tuple
+        """
+        duplicates = self.get_duplicates()
+        if(len(duplicates) == 0):
+            return (0, 0)  # Nothing to do here.
+
+        removed = 0  # Count the number of records that are removed. Hopefully this will be the same as len(duplicates).
+        while removed != len(duplicates):  # Unfortunately, in certain cases, extra passes may be necessary to ensure that all duplicates are removed.
+            path = Gtk.TreePath(0)  # Start with the first row in the log.
+            iter = self.get_iter(path)
+            while iter is not None:
+                row_index = self.get_value(iter, 0)  # Get the index.
+                if(row_index in duplicates):  # Is this a duplicate row? If so, delete it.
+                    self.delete_record(row_index, iter)
+                    removed += 1
+                iter = self.iter_next(iter)  # Move on to the next row, until iter_next returns None.
+
+        assert(removed == len(duplicates))
+        return (len(duplicates), removed)
+
+    def get_duplicates(self):
+        """ Find the duplicates in the log, based on the CALL, QSO_DATE, TIME_ON, FREQ and MODE fields.
+
+        :returns: A list of row IDs corresponding to the duplicate records.
+        :rtype: list
+        """
+        duplicates = []
+        try:
+            with self.connection:
+                c = self.connection.cursor()
+                c.execute(
+                    """SELECT rowid FROM %s WHERE rowid NOT IN
    (
    SELECT MIN(rowid) FROM %s GROUP BY call, qso_date, time_on, freq, mode
    )""" % (self.name, self.name))
-            result = c.fetchall()
-         for rowid in result:
-            duplicates.append(rowid[0]) # Get the integer from inside the tuple.
-      except (sqlite.Error, IndexError) as e:
-         logging.exception(e)
-      return duplicates
-         
-   def get_record_by_index(self, index):
-      """ Return a record with a given index in the log. The record is represented by a dictionary of field-value pairs. """
-      try:
-         with self.connection:
-            c = self.connection.cursor()
-            query = "SELECT * FROM %s WHERE id=?" % self.name
-            c.execute(query, [index])
-            return c.fetchone()
-      except sqlite.Error as e:
-         logging.exception(e)
-         return None
-
-   def get_all_records(self):
-      """ Return a list of all the records in the log. Each record is represented by a dictionary. """
-      try:
-         with self.connection:
-            c = self.connection.cursor()
-            c.execute("SELECT * FROM %s" % self.name)
-            return c.fetchall()
-      except sqlite.Error as e:
-         logging.exception(e)
-         return None
-
-   def get_number_of_records(self):
-      """ Return the total number of records in the log. """
-      try:
-         with self.connection:
-            c = self.connection.cursor()
-            c.execute("SELECT Count(*) FROM %s" % self.name)
-            return c.fetchone()[0]
-      except (sqlite.Error, IndexError) as e:
-         logging.exception(e)
-         return None
+                result = c.fetchall()
+            for rowid in result:
+                duplicates.append(rowid[0])  # Get the integer from inside the tuple.
+        except (sqlite.Error, IndexError) as e:
+            logging.exception(e)
+        return duplicates
+
+    def get_record_by_index(self, index):
+        """ Return a record with a given index in the log.
+
+        :arg int index: The index of the record in the SQL database.
+        :returns: The desired record, represented by a dictionary of field-value pairs.
+        :rtype: dict
+        """
+        try:
+            with self.connection:
+                c = self.connection.cursor()
+                query = "SELECT * FROM %s WHERE id=?" % self.name
+                c.execute(query, [index])
+                return c.fetchone()
+        except sqlite.Error as e:
+            logging.exception(e)
+            return None
+
+    def get_all_records(self):
+        """ Return a list of all the records in the log.
+
+        :returns: A list of all the records in the log. Each record is represented by a dictionary.
+        :rtype: dict
+        """
+        try:
+            with self.connection:
+                c = self.connection.cursor()
+                c.execute("SELECT * FROM %s" % self.name)
+                return c.fetchall()
+        except sqlite.Error as e:
+            logging.exception(e)
+            return None
+
+    def get_number_of_records(self):
+        """ Return the total number of records in the log.
+
+        :returns: The total number of records in the log.
+        :rtype: int
+        """
+        try:
+            with self.connection:
+                c = self.connection.cursor()
+                c.execute("SELECT Count(*) FROM %s" % self.name)
+                return c.fetchone()[0]
+        except (sqlite.Error, IndexError) as e:
+            logging.exception(e)
+            return None
+
 
 class TestLog(unittest.TestCase):
 
-   def setUp(self):
-      self.connection = sqlite.connect(":memory:")
-      self.connection.row_factory = sqlite.Row
-
-      self.field_names = ["CALL", "QSO_DATE", "TIME_ON", "FREQ", "BAND", "MODE", "RST_SENT", "RST_RCVD"]
-      self.fields_and_data = {"CALL":"TEST123", "QSO_DATE":"20130312", "TIME_ON":"1234", "FREQ":"145.500", "BAND":"2m", "MODE":"FM", "RST_SENT":"59", "RST_RCVD":"59"}
-
-      c = self.connection.cursor()
-      query = "CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT"
-      for field_name in self.field_names:
-         s = ", %s TEXT" % field_name.lower()
-         query = query + s
-      query = query + ")"
-      c.execute(query)
-
-      self.log = Log(self.connection, "test")
-
-   def tearDown(self):
-      self.connection.close()
-
-   def test_log_add_missing_db_columns(self):
-
-      column_names_before = []
-      column_names_after = []
-
-      c = self.connection.cursor()
-      c.execute("PRAGMA table_info(test)") 
-      result = c.fetchall()
-      for t in result:
-         column_names_before.append(t[1].upper())
-
-      self.log.add_missing_db_columns()
-
-      c.execute("PRAGMA table_info(test)") 
-      result = c.fetchall()
-      for t in result:
-         column_names_after.append(t[1].upper())
-
-      print "Column names before: ", column_names_before
-      print "Column names after: ", column_names_after
-
-      assert(len(column_names_before) == len(self.field_names) + 1) # Added 1 here because of the "ID" column in all database tables.
-      assert(len(column_names_after) == len(AVAILABLE_FIELD_NAMES_ORDERED) + 1)
-      for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
-         assert(field_name in column_names_after)
-
-   def test_log_add_record(self):
-      self.log.add_record(self.fields_and_data)
-      c = self.connection.cursor()
-      c.execute("SELECT * FROM test")
-      records = c.fetchall()
-      
-      assert len(records) == 1
-      
-      for field_name in self.field_names:
-         print self.fields_and_data[field_name], records[0][field_name]
-         assert self.fields_and_data[field_name] == records[0][field_name]
-
-   def test_log_delete_record(self):
-      query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
-      c = self.connection.cursor()
-      c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
-
-      c.execute("SELECT * FROM test")
-      records_before = c.fetchall()
-
-      self.log.delete_record(1)
-
-      c.execute("SELECT * FROM test")
-      records_after = c.fetchall()
-
-      assert(len(records_before) == 1)
-      assert(len(records_after) == 0)
-      
-   def test_log_edit_record(self):
-      query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
-      c = self.connection.cursor()
-      c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
-
-      c.execute("SELECT * FROM test")
-      record_before = c.fetchall()[0]
-
-      self.log.edit_record(1, "CALL", "TEST456")
-      self.log.edit_record(1, "FREQ", "145.450")
-
-      c.execute("SELECT * FROM test")
-      record_after = c.fetchall()[0]
-
-      assert(record_before["CALL"] == "TEST123")
-      assert(record_after["CALL"] == "TEST456")
-      assert(record_before["FREQ"] == "145.500")
-      assert(record_after["FREQ"] == "145.450")
-
-   def test_log_get_record_by_index(self):
-      query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
-      c = self.connection.cursor()
-      c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
-
-      record = self.log.get_record_by_index(1)
-      print "Contents of retrieved record: ", record
-      for field_name in record.keys():
-         if(field_name.upper() == "ID"):
-            continue
-         else:
-            assert(record[field_name.upper()] == self.fields_and_data[field_name.upper()])
-      assert(len(record) == len(self.fields_and_data) + 1)
-
-   def test_log_get_all_records(self):
-      query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
-      c = self.connection.cursor()
-      # Add the same record twice
-      c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
-      c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
-
-      records = self.log.get_all_records()
-      print "Contents of all retrieved records: ", records
-      assert(len(records) == 2) # There should be 2 records
-      for field_name in self.field_names:
-         assert(records[0][field_name] == self.fields_and_data[field_name])
-         assert(records[1][field_name] == self.fields_and_data[field_name])
-
-   def test_log_get_number_of_records(self):
-      query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
-      c = self.connection.cursor()
-      # Add the same record twice
-      c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
-      c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
-
-      number_of_records = self.log.get_number_of_records()
-      print "Number of records in the log: ", number_of_records
-      assert(number_of_records == 2) # There should be 2 records
-
-   def test_log_get_duplicates(self):
-      query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
-      c = self.connection.cursor()
-      n = 5 # The total number of records to insert.
-      for i in range(0, n):
-         c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
-      assert len(self.log.get_duplicates()) == n-1 # Expecting n-1 duplicates.
+    def setUp(self):
+        self.connection = sqlite.connect(":memory:")
+        self.connection.row_factory = sqlite.Row
+
+        self.field_names = ["CALL", "QSO_DATE", "TIME_ON", "FREQ", "BAND", "MODE", "RST_SENT", "RST_RCVD"]
+        self.fields_and_data = {"CALL": "TEST123", "QSO_DATE": "20130312", "TIME_ON": "1234", "FREQ": "145.500", "BAND": "2m", "MODE": "FM", "RST_SENT": "59", "RST_RCVD": "59"}
+
+        c = self.connection.cursor()
+        query = "CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT"
+        for field_name in self.field_names:
+            s = ", %s TEXT" % field_name.lower()
+            query = query + s
+        query = query + ")"
+        c.execute(query)
+
+        self.log = Log(self.connection, "test")
+
+    def tearDown(self):
+        self.connection.close()
+
+    def test_log_add_missing_db_columns(self):
+
+        column_names_before = []
+        column_names_after = []
+
+        c = self.connection.cursor()
+        c.execute("PRAGMA table_info(test)")
+        result = c.fetchall()
+        for t in result:
+            column_names_before.append(t[1].upper())
+
+        self.log.add_missing_db_columns()
+
+        c.execute("PRAGMA table_info(test)")
+        result = c.fetchall()
+        for t in result:
+            column_names_after.append(t[1].upper())
+
+        print("Column names before: ", column_names_before)
+        print("Column names after: ", column_names_after)
+
+        assert(len(column_names_before) == len(self.field_names) + 1)  # Added 1 here because of the "ID" column in all database tables.
+        assert(len(column_names_after) == len(AVAILABLE_FIELD_NAMES_ORDERED) + 1)
+        for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
+            assert(field_name in column_names_after)
+
+    def test_log_add_record(self):
+        self.log.add_record(self.fields_and_data)
+        c = self.connection.cursor()
+        c.execute("SELECT * FROM test")
+        records = c.fetchall()
+
+        assert len(records) == 1
+
+        for field_name in self.field_names:
+            print(self.fields_and_data[field_name], records[0][field_name])
+            assert self.fields_and_data[field_name] == records[0][field_name]
+
+    def test_log_delete_record(self):
+        query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
+        c = self.connection.cursor()
+        c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
+
+        c.execute("SELECT * FROM test")
+        records_before = c.fetchall()
+
+        self.log.delete_record(1)
+
+        c.execute("SELECT * FROM test")
+        records_after = c.fetchall()
+
+        assert(len(records_before) == 1)
+        assert(len(records_after) == 0)
+
+    def test_log_edit_record(self):
+        query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
+        c = self.connection.cursor()
+        c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
+
+        c.execute("SELECT * FROM test")
+        record_before = c.fetchall()[0]
+
+        self.log.edit_record(1, "CALL", "TEST456")
+        self.log.edit_record(1, "FREQ", "145.450")
+
+        c.execute("SELECT * FROM test")
+        record_after = c.fetchall()[0]
+
+        assert(record_before["CALL"] == "TEST123")
+        assert(record_after["CALL"] == "TEST456")
+        assert(record_before["FREQ"] == "145.500")
+        assert(record_after["FREQ"] == "145.450")
+
+    def test_log_get_record_by_index(self):
+        query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
+        c = self.connection.cursor()
+        c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
+
+        record = self.log.get_record_by_index(1)
+        print("Contents of retrieved record: ", record)
+        for field_name in list(record.keys()):
+            if(field_name.upper() == "ID"):
+                continue
+            else:
+                assert(record[field_name.upper()] == self.fields_and_data[field_name.upper()])
+        assert(len(record) == len(self.fields_and_data) + 1)
+
+    def test_log_get_all_records(self):
+        query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
+        c = self.connection.cursor()
+        # Add the same record twice
+        c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
+        c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
+
+        records = self.log.get_all_records()
+        print("Contents of all retrieved records: ", records)
+        assert(len(records) == 2)  # There should be 2 records
+        for field_name in self.field_names:
+            assert(records[0][field_name] == self.fields_and_data[field_name])
+            assert(records[1][field_name] == self.fields_and_data[field_name])
+
+    def test_log_get_number_of_records(self):
+        query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
+        c = self.connection.cursor()
+        # Add the same record twice
+        c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
+        c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
+
+        number_of_records = self.log.get_number_of_records()
+        print("Number of records in the log: ", number_of_records)
+        assert(number_of_records == 2)  # There should be 2 records
+
+    def test_log_get_duplicates(self):
+        query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
+        c = self.connection.cursor()
+        n = 5  # The total number of records to insert.
+        for i in range(0, n):
+            c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
+        assert len(self.log.get_duplicates()) == n-1  # Expecting n-1 duplicates.
 
 if(__name__ == '__main__'):
-   unittest.main()
+    unittest.main()
diff --git a/pyqso/log_name_dialog.py b/pyqso/log_name_dialog.py
index 1cdf306..df46436 100644
--- a/pyqso/log_name_dialog.py
+++ b/pyqso/log_name_dialog.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,39 +17,50 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject
+from gi.repository import Gtk
 import logging
-import re
-import calendar
+
 
 class LogNameDialog(Gtk.Dialog):
-   
-   def __init__(self, parent, title=None, name=None):
-      
-      if(title is None):
-         title = "New Log"
-      else:
-         title = title
-      Gtk.Dialog.__init__(self, title=title, parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
 
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label("Log Name:")
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 6)
-      self.entry = Gtk.Entry()
-      if(name is not None):
-         self.entry.set_text(name)
-      hbox_temp.pack_start(self.entry, False, False, 6)
-      self.vbox.pack_start(hbox_temp, False, False, 6)
+    """ A Gtk.Dialog where a user can specify the name of a Log object. """
+
+    def __init__(self, parent, title=None, name=None):
+        """ Create and show the log name dialog to the user.
+
+        :arg parent: The parent Gtk window.
+        :arg title: The title of the dialog. If this is None, it is assumed that a new log is going to be created.
+        :arg name: The existing name of the Log object. Defaults to None if not specified (because the Log does not yet exist).
+        """
+
+        if(title is None):
+            title = "New Log"
+        else:
+            title = title
+        Gtk.Dialog.__init__(self, title=title, parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
+
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label("Log Name:")
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 6)
+        self.entry = Gtk.Entry()
+        if(name is not None):
+            self.entry.set_text(name)
+        hbox_temp.pack_start(self.entry, False, False, 6)
+        self.vbox.pack_start(hbox_temp, False, False, 6)
 
-      self.show_all()
+        self.show_all()
 
-      logging.debug("New LogNameDialog instance created!")
+        logging.debug("New LogNameDialog instance created!")
 
-      return
+        return
 
-   def get_log_name(self):
-      logging.debug("Retrieving the log name from the LogNameDialog...")
-      return self.entry.get_text()
+    def get_log_name(self):
+        """ Return the log name specified in the Gtk.Entry box by the user.
 
+        :returns: The log's name.
+        :rtype: str
+        """
 
+        logging.debug("Retrieving the log name from the LogNameDialog...")
+        return self.entry.get_text()
diff --git a/pyqso/logbook.py b/pyqso/logbook.py
index 35c3d06..d3f22dd 100644
--- a/pyqso/logbook.py
+++ b/pyqso/logbook.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2012 Christian T. Jacobs.
+#    Copyright (C) 2012-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,999 +17,1268 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject, Pango, PangoCairo
+from gi.repository import Gtk, Pango, PangoCairo
 import logging
 import sqlite3 as sqlite
-from os.path import basename, getctime, getmtime, expanduser, exists
-import datetime
-import ConfigParser
+from os.path import basename, getmtime, expanduser
+from datetime import datetime, date
+import configparser
+
+try:
+    from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
+    from matplotlib.figure import Figure
+    from matplotlib.dates import DateFormatter, MonthLocator
+    have_matplotlib = True
+except ImportError as e:
+    logging.warning(e)
+    logging.warning("Could not import matplotlib, so you will not be able to plot annual logbook statistics. Check that all the PyQSO dependencies are satisfied.")
+    have_matplotlib = False
+
+from pyqso.adif import *
+from pyqso.log import *
+from pyqso.log_name_dialog import *
+from pyqso.auxiliary_dialogs import *
 
-from adif import *
-from log import *
-from log_name_dialog import *
-from auxiliary_dialogs import *
 
 class Logbook(Gtk.Notebook):
-   """ A Logbook object can store multiple Log objects. """
-   
-   def __init__(self, parent):
-
-      Gtk.Notebook.__init__(self)
-
-      self.parent = parent
-      self.connection = None
-      self.summary = {}
-      self.logs = []
-      logging.debug("New Logbook instance created!")
-      return
-   
-   def new(self, widget=None):
-      """ Create a new logbook, and open it. """
-      
-      # Get the new file's path from a dialog.
-      dialog = Gtk.FileChooserDialog("Create a New SQLite Database File",
-                                 None,
-                                 Gtk.FileChooserAction.SAVE,
-                                 (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
-                                 Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
-      dialog.set_do_overwrite_confirmation(True)
-
-      response = dialog.run()
-      if(response == Gtk.ResponseType.OK):
-         path = dialog.get_filename()
-      else:
-         path = None
-      dialog.destroy()
-
-      if(path is None): # If the Cancel button has been clicked, path will still be None
-         logging.debug("No file path specified.")
-         return
-      else:
-         # Clear the contents of the file, in case the file exists already.
-         open(path, 'w').close()
-         # Open the new logbook, ready for use.
-         self.open(path=path)
-   
-   def open(self, widget=None, path=None):
-      """ Open a logbook, and render all the logs within it. 
-      An optional 'path' argument can be specified if the database file location is known.
-      Otherwise, a file selection dialog will appear. """
-
-      if(path is None):
-         # If no path has been provided, get one from a "File Open" dialog.
-         dialog = Gtk.FileChooserDialog("Open SQLite Database File",
-                                    None,
-                                    Gtk.FileChooserAction.OPEN,
-                                    (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
-                                    Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
-         
-         response = dialog.run()
-         if(response == Gtk.ResponseType.OK):
+
+    """ A Logbook object can store multiple Log objects. """
+
+    def __init__(self, parent):
+        """ Create a new Logbook object and initialise the list of Logs.
+
+        :arg parent: The parent Gtk window.
+        """
+
+        Gtk.Notebook.__init__(self)
+
+        self.parent = parent
+        self.connection = None
+        self.summary = {}
+        self.logs = []
+        logging.debug("New Logbook instance created!")
+        return
+
+    def new(self, widget=None):
+        """ Create a new logbook, and open it. """
+
+        # Get the new file's path from a dialog.
+        dialog = Gtk.FileChooserDialog("Create a New SQLite Database File",
+                                       self.parent,
+                                       Gtk.FileChooserAction.SAVE,
+                                      (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                                       Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
+        dialog.set_do_overwrite_confirmation(True)
+
+        response = dialog.run()
+        if(response == Gtk.ResponseType.OK):
             path = dialog.get_filename()
-         dialog.destroy()
-         
-         if(path is None): # If the Cancel button has been clicked, path will still be None
+        else:
+            path = None
+        dialog.destroy()
+
+        if(path is None):  # If the Cancel button has been clicked, path will still be None
             logging.debug("No file path specified.")
             return
-         
-      connected = self.db_connect(path=path)
-      if(connected):
-         # If the connection setup was successful, then open all the logs in the database
-         
-         self.path = path
-         
-         logging.debug("Trying to retrieve all the logs in the logbook...")
-         self.logs = [] # A fresh stack of Log objects
-         try:
-            with self.connection:
-               c = self.connection.cursor()
-               c.execute("SELECT name FROM sqlite_master WHERE type='table'")
-               names = c.fetchall()
-               for name in names:
-                  if(name[0][0:7] == "sqlite_"):
-                     continue # Skip SQLite internal tables
-                  l = Log(self.connection, name[0])
-                  l.populate()
-                  self.logs.append(l)
-         except (sqlite.Error, IndexError) as e:
-            logging.exception(e)
-            error(parent = self.parent, message = "Oops! Something went wrong when trying to retrieve the logs from the logbook. Perhaps the logbook file is encrypted, corrupted, or in the wrong format?")
-            return
+        else:
+            # Clear the contents of the file, in case the file exists already.
+            open(path, 'w').close()
+            # Open the new logbook, ready for use.
+            self.open(path=path)
+        return
+
+    def open(self, widget=None, path=None):
+        """ Open a logbook, and render all the logs within it.
+
+        :arg str path: An optional argument containing the database file location, if already known. If this is None, a file selection dialog will appear.
+        """
+
+        if(path is None):
+            # If no path has been provided, get one from a "File Open" dialog.
+            dialog = Gtk.FileChooserDialog("Open SQLite Database File",
+                                           self.parent,
+                                           Gtk.FileChooserAction.OPEN,
+                                          (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                                           Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
+
+            response = dialog.run()
+            if(response == Gtk.ResponseType.OK):
+                path = dialog.get_filename()
+            dialog.destroy()
 
-         logging.debug("All logs retrieved successfully. Now attempting to render them all in the Gtk.Notebook...")
-         # For rendering the logs. One treeview and one treeselection per Log.
-         self.treeview = []
-         self.treeselection = []
-         self.sorter = []
-         self.filter = []
-         self._create_summary_page()
-         self._create_dummy_page()
-
-         # FIXME: This is an unfortunate work-around. If the area around the "+/New Log" button
-         # is clicked, PyQSO will change to an empty page. This signal is used to stop this from happening. 
-         self.connect("switch-page", self._on_switch_page)
-
-         for i in range(len(self.logs)):
-            self._render_log(i) 
-         logging.debug("All logs rendered successfully.")
-
-         self.update_summary()  
-         self.parent.toolbox.awards.count()  
-
-         context_id = self.parent.statusbar.get_context_id("Status")
-         self.parent.statusbar.push(context_id, "Logbook: %s" % self.path)
-         self.parent.toolbar.set_logbook_button_sensitive(False)
-         self.parent.menu.set_logbook_item_sensitive(False)
-         self.parent.menu.set_log_items_sensitive(True)
-         self.parent.toolbar.filter_source.set_sensitive(True)
-
-         self.show_all()
-
-      else:
-         logging.debug("Not connected to a logbook. No logs were opened.")
-
-      return
-
-   def close(self, widget=None):
-      """ Close the logbook that is currently open. """
-
-      disconnected = self.db_disconnect()
-      if(disconnected):
-         logging.debug("Closing all logs in the logbook...")
-         while(self.get_n_pages() > 0):
-            # Once a page is removed, the other pages get re-numbered,
-            # so a 'for' loop isn't the best option here.
-            self.remove_page(0)
-         logging.debug("All logs now closed.")
-
-         context_id = self.parent.statusbar.get_context_id("Status")
-         self.parent.statusbar.push(context_id, "No logbook is currently open.")
-         self.parent.toolbar.set_logbook_button_sensitive(True)
-         self.parent.menu.set_logbook_item_sensitive(True)
-         self.parent.menu.set_log_items_sensitive(False)
-         self.parent.toolbar.filter_source.set_sensitive(False)
-      else:
-         logging.debug("Unable to disconnect from the database. No logs were closed.")
-      return
-
-   def db_connect(self, path=None):
-      """ Create an SQL database connection to the Logbook's data source """
-
-      logging.debug("Attempting to connect to the logbook database...")
-      # Try setting up the SQL database connection
-      try:
-         self.db_disconnect() # Destroy any existing connections first.
-         self.connection = sqlite.connect(path)
-         self.connection.row_factory = sqlite.Row
-      except sqlite.Error as e:
-         # PyQSO can't connect to the database.
-         logging.exception(e)
-         error(parent=self.parent, message="PyQSO cannot connect to the database. Check file permissions?")
-         return False
-
-      logging.debug("Database connection created successfully!")
-      return True
-         
-   def db_disconnect(self):
-      """ Destroy the connection to the Logbook's data source. """
-      logging.debug("Cleaning up any existing database connections...")
-      if(self.connection):
-         try:
-            self.connection.close()
-         except sqlite.Error as e:
+            if(path is None):  # If the Cancel button has been clicked, path will still be None
+                logging.debug("No file path specified.")
+                return
+
+        connected = self.db_connect(path)
+        if(connected):
+            # If the connection setup was successful, then open all the logs in the database
+
+            self.path = path
+
+            logging.debug("Trying to retrieve all the logs in the logbook...")
+            self.logs = []  # A fresh stack of Log objects
+            try:
+                with self.connection:
+                    c = self.connection.cursor()
+                    c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT GLOB 'sqlite_*'")
+                    for name in c:
+                        l = Log(self.connection, name[0])
+                        l.populate()
+                        self.logs.append(l)
+            except (sqlite.Error, IndexError) as e:
+                logging.exception(e)
+                error(parent=self.parent, message="Oops! Something went wrong when trying to retrieve the logs from the logbook. Perhaps the logbook file is encrypted, corrupted, or in the wrong format?")
+                return
+
+            logging.debug("All logs retrieved successfully. Now attempting to render them all in the Gtk.Notebook...")
+            # For rendering the logs. One treeview and one treeselection per Log.
+            self.treeview = []
+            self.treeselection = []
+            self.sorter = []
+            self.filter = []
+            self._create_summary_page()
+            self._create_dummy_page()
+
+            # FIXME: This is an unfortunate work-around. If the area around the "+/New Log" button
+            # is clicked, PyQSO will change to an empty page. This signal is used to stop this from happening.
+            self.connect("switch-page", self._on_switch_page)
+
+            for i in range(len(self.logs)):
+                self._render_log(i)
+            logging.debug("All logs rendered successfully.")
+
+            self.update_summary()
+            self.parent.toolbox.awards.count()
+
+            context_id = self.parent.statusbar.get_context_id("Status")
+            self.parent.statusbar.push(context_id, "Logbook: %s" % self.path)
+            self.parent.toolbar.set_logbook_button_sensitive(False)
+            self.parent.menu.set_logbook_item_sensitive(False)
+            self.parent.menu.set_log_items_sensitive(True)
+            self.parent.toolbar.filter_source.set_sensitive(True)
+
+            self.show_all()
+
+        else:
+            logging.debug("Not connected to a logbook. No logs were opened.")
+
+        return
+
+    def close(self, widget=None):
+        """ Close the logbook that is currently open. """
+
+        disconnected = self.db_disconnect()
+        if(disconnected):
+            logging.debug("Closing all logs in the logbook...")
+            while(self.get_n_pages() > 0):
+                # Once a page is removed, the other pages get re-numbered,
+                # so a 'for' loop isn't the best option here.
+                self.remove_page(0)
+            logging.debug("All logs now closed.")
+
+            context_id = self.parent.statusbar.get_context_id("Status")
+            self.parent.statusbar.push(context_id, "No logbook is currently open.")
+            self.parent.toolbar.set_logbook_button_sensitive(True)
+            self.parent.menu.set_logbook_item_sensitive(True)
+            self.parent.menu.set_log_items_sensitive(False)
+            self.parent.toolbar.filter_source.set_sensitive(False)
+        else:
+            logging.debug("Unable to disconnect from the database. No logs were closed.")
+        return
+
+    def db_connect(self, path):
+        """ Create an SQL database connection to the Logbook's data source.
+
+        :arg str path: The path of the database file.
+        """
+
+        logging.debug("Attempting to connect to the logbook database...")
+        # Try setting up the SQL database connection
+        try:
+            self.db_disconnect()  # Destroy any existing connections first.
+            self.connection = sqlite.connect(path)
+            self.connection.row_factory = sqlite.Row
+        except sqlite.Error as e:
+            # PyQSO can't connect to the database.
             logging.exception(e)
+            error(parent=self.parent, message="PyQSO cannot connect to the database. Check file permissions?")
             return False
-      else:
-         logging.debug("Already disconnected. Nothing to do here.")
-      return True
-
-   def _create_dummy_page(self):
-      """ Create a blank page in the Gtk.Notebook for the "+" (New Log) tab. """
-      blank_treeview = Gtk.TreeView()
-      # Allow the Log to be scrolled up/down
-      sw = Gtk.ScrolledWindow()
-      sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
-      sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
-      sw.add(blank_treeview)
-      vbox = Gtk.VBox()
-      vbox.pack_start(sw, True, True, 0)
-
-      # Add a "+" button to the tab
-      hbox = Gtk.HBox(False, 0)
-      icon = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU)
-      button = Gtk.Button()
-      button.set_relief(Gtk.ReliefStyle.NONE)
-      button.set_focus_on_click(False)
-      button.connect("clicked", self.new_log)
-      button.add(icon)
-      button.set_tooltip_text('New Log')
-      hbox.pack_start(button, False, False, 0)
-      hbox.show_all()
-      vbox.show_all()
-
-      self.insert_page(vbox, hbox, 1)
-      self.show_all()
-      self.set_current_page(0)
-      return
-
-   def _create_summary_page(self):
-      """ Create a summary page containing the number of logs in the logbook, and the logbook's modification date. """
-      vbox = Gtk.VBox()
-
-      # Database name in large font at the top of the summary page
-      hbox = Gtk.HBox()
-      label = Gtk.Label(halign=Gtk.Align.START)
-      label.set_markup("<span size=\"x-large\">%s</span>" % basename(self.path))
-      hbox.pack_start(label, False, False, 6)
-      vbox.pack_start(hbox, False, False, 2)
-
-      hbox = Gtk.HBox()
-      label = Gtk.Label("Number of logs: ", halign=Gtk.Align.START)
-      hbox.pack_start(label, False, False, 6)
-      self.summary["NUMBER_OF_LOGS"] = Gtk.Label("0")
-      hbox.pack_start(self.summary["NUMBER_OF_LOGS"], False, False, 2)
-      vbox.pack_start(hbox, False, False, 2)
-
-      hbox = Gtk.HBox()
-      label = Gtk.Label("Date modified: ", halign=Gtk.Align.START)
-      hbox.pack_start(label, False, False, 6)
-      self.summary["DATE_MODIFIED"] = Gtk.Label("0")
-      hbox.pack_start(self.summary["DATE_MODIFIED"], False, False, 2)
-      vbox.pack_start(hbox, False, False, 2)
-
-      hbox = Gtk.HBox(False, 0)
-      label = Gtk.Label("Summary  ")
-      icon = Gtk.Image.new_from_stock(Gtk.STOCK_INDEX, Gtk.IconSize.MENU)
-      hbox.pack_start(label, False, False, 0)
-      hbox.pack_start(icon, False, False, 0)
-      hbox.show_all()
-
-      self.insert_page(vbox, hbox, 0) # Append the new log as a new tab
-      self.show_all()
-
-      return
-
-   def update_summary(self):
-      """ Update the information presented on the summary page. """
-      self.summary["NUMBER_OF_LOGS"].set_label(str(self.get_number_of_logs()))
-      try:
-         t = datetime.fromtimestamp(getmtime(self.path)).strftime("%d %B %Y @ %H:%M")
-         self.summary["DATE_MODIFIED"].set_label(str(t))
-      except (IOError, OSError) as e:
-         logging.exception(e)
-      return
-
-   def _on_switch_page(self, widget, label, new_page):
-      if(new_page == self.get_n_pages()-1): # The last (right-most) tab is the "New Log" tab.
-         self.stop_emission("switch-page")
-         
-      # Disable the record buttons if a log page is not selected.
-      if(new_page == 0):
-         self.parent.toolbar.set_record_buttons_sensitive(False)
-         self.parent.menu.set_record_items_sensitive(False)
-      else:
-         self.parent.toolbar.set_record_buttons_sensitive(True)
-         self.parent.menu.set_record_items_sensitive(True)
-      return
-
-   def new_log(self, widget=None):
-      """ Create a new log in the logbook. """
-      if(self.connection is None):
-         return
-      exists = True
-      dialog = LogNameDialog(self.parent)
-      while(exists):
-         response = dialog.run()
-         if(response == Gtk.ResponseType.OK):
-            log_name = dialog.get_log_name()
+
+        logging.debug("Database connection created successfully!")
+        return True
+
+    def db_disconnect(self):
+        """ Destroy the connection to the Logbook's data source.
+
+        :returns: True if the connection was successfully destroyed, and False otherwise.
+        :rtype: bool
+        """
+
+        logging.debug("Cleaning up any existing database connections...")
+        if(self.connection):
             try:
-               with self.connection:
-                  c = self.connection.cursor()
-                  query = "CREATE TABLE %s (id INTEGER PRIMARY KEY AUTOINCREMENT" % log_name
-                  for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
-                     s = ", %s TEXT" % field_name.lower()
-                     query = query + s
-                  query = query + ")"
-                  c.execute(query)
-                  exists = False
+                self.connection.close()
             except sqlite.Error as e:
-               logging.exception(e)
-               # Data is not valid - inform the user.
-               error(parent=self.parent, message="Database error. Try another log name.")
-               exists = True
-         else:
-            dialog.destroy()
+                logging.exception(e)
+                return False
+        else:
+            logging.debug("Already disconnected. Nothing to do here.")
+        return True
+
+    def _create_dummy_page(self):
+        """ Create a blank page in the Gtk.Notebook for the "+" (New Log) tab. """
+
+        blank_treeview = Gtk.TreeView()
+        # Allow the Log to be scrolled up/down
+        sw = Gtk.ScrolledWindow()
+        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
+        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+        sw.add(blank_treeview)
+        vbox = Gtk.VBox()
+        vbox.pack_start(sw, True, True, 0)
+
+        # Add a "+" button to the tab
+        hbox = Gtk.HBox(False, 0)
+        icon = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU)
+        button = Gtk.Button()
+        button.set_relief(Gtk.ReliefStyle.NONE)
+        button.set_focus_on_click(False)
+        button.connect("clicked", self.new_log)
+        button.add(icon)
+        button.set_tooltip_text('New Log')
+        hbox.pack_start(button, False, False, 0)
+        hbox.show_all()
+        vbox.show_all()
+
+        self.insert_page(vbox, hbox, 1)
+        self.show_all()
+        self.set_current_page(0)
+        return
+
+    def _create_summary_page(self):
+        """ Create a summary page containing the number of logs in the logbook, and the logbook's modification date. """
+
+        vbox = Gtk.VBox()
+
+        # Database name in large font at the top of the summary page
+        hbox = Gtk.HBox()
+        label = Gtk.Label(halign=Gtk.Align.START)
+        label.set_markup("<span size=\"x-large\">%s</span>" % basename(self.path))
+        hbox.pack_start(label, False, False, 6)
+        vbox.pack_start(hbox, False, False, 4)
+
+        hbox = Gtk.HBox()
+        label = Gtk.Label("Number of logs: ", halign=Gtk.Align.START)
+        hbox.pack_start(label, False, False, 6)
+        self.summary["LOG_COUNT"] = Gtk.Label("0")
+        hbox.pack_start(self.summary["LOG_COUNT"], False, False, 4)
+        vbox.pack_start(hbox, False, False, 4)
+
+        hbox = Gtk.HBox()
+        label = Gtk.Label("Total number of QSOs: ", halign=Gtk.Align.START)
+        hbox.pack_start(label, False, False, 6)
+        self.summary["QSO_COUNT"] = Gtk.Label("0")
+        hbox.pack_start(self.summary["QSO_COUNT"], False, False, 4)
+        vbox.pack_start(hbox, False, False, 4)
+
+        hbox = Gtk.HBox()
+        label = Gtk.Label("Date modified: ", halign=Gtk.Align.START)
+        hbox.pack_start(label, False, False, 6)
+        self.summary["DATE_MODIFIED"] = Gtk.Label("0")
+        hbox.pack_start(self.summary["DATE_MODIFIED"], False, False, 4)
+        vbox.pack_start(hbox, False, False, 4)
+
+        hseparator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
+        vbox.pack_start(hseparator, False, False, 4)
+
+        # Yearly statistics
+        config = configparser.ConfigParser()
+        have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != [])
+        (section, option) = ("general", "show_yearly_statistics")
+        if(have_config and config.has_option(section, option)):
+            if(config.get("general", "show_yearly_statistics") == "True" and have_matplotlib):
+                hbox = Gtk.HBox()
+                label = Gtk.Label("Display statistics for year: ", halign=Gtk.Align.START)
+                hbox.pack_start(label, False, False, 6)
+                self.summary["YEAR_SELECT"] = Gtk.ComboBoxText()
+                min_year, max_year = self._find_year_bounds()
+                if min_year and max_year:
+                    for year in range(max_year, min_year-1, -1):
+                        self.summary["YEAR_SELECT"].append_text(str(year))
+                self.summary["YEAR_SELECT"].append_text("")
+                self.summary["YEAR_SELECT"].connect("changed", self._on_year_changed)
+                hbox.pack_start(self.summary["YEAR_SELECT"], False, False, 6)
+                vbox.pack_start(hbox, False, False, 4)
+
+                self.summary["YEARLY_STATISTICS"] = Figure()
+                canvas = FigureCanvas(self.summary["YEARLY_STATISTICS"])
+                canvas.set_size_request(800, 250)
+                canvas.show()
+                vbox.pack_start(canvas, True, True, 4)
+
+        # Summary tab label and icon.
+        hbox = Gtk.HBox(False, 0)
+        label = Gtk.Label("Summary  ")
+        icon = Gtk.Image.new_from_stock(Gtk.STOCK_INDEX, Gtk.IconSize.MENU)
+        hbox.pack_start(label, False, False, 0)
+        hbox.pack_start(icon, False, False, 0)
+        hbox.show_all()
+
+        self.insert_page(vbox, hbox, 0)  # Append as a new tab
+        self.show_all()
+
+        return
+
+    def _on_year_changed(self, combo):
+        """ Re-plot the statistics for the year selected by the user. """
+
+        # Clear figure
+        self.summary["YEARLY_STATISTICS"].clf()
+        self.summary["YEARLY_STATISTICS"].canvas.draw()
+
+        # Get year to show statistics for.
+        year = combo.get_active_text()
+        try:
+            year = int(year)
+        except ValueError:
+            # Empty year string.
             return
 
-      dialog.destroy()
+        # Number of contacts made each month
+        contact_count_plot = self.summary["YEARLY_STATISTICS"].add_subplot(121)
+        contact_count = self._get_annual_contact_count(year)
+
+        # x-axis formatting based on the date
+        contact_count_plot.bar(list(contact_count.keys()), list(contact_count.values()), color="k", width=15, align="center")
+        formatter = DateFormatter("%b")
+        contact_count_plot.xaxis.set_major_formatter(formatter)
+        month_locator = MonthLocator()
+        contact_count_plot.xaxis.set_major_locator(month_locator)
+        contact_count_plot.set_ylabel("Number of QSOs")
+
+        # Set x-axis upper limit based on the current month.
+        contact_count_plot.xaxis_date()
+        contact_count_plot.set_xlim([date(year-1, 12, 16), date(year, 12, 15)])  # Make a bit of space either side of January and December of the selected year.
+
+        # Pie chart of all the modes used.
+        mode_count_plot = self.summary["YEARLY_STATISTICS"].add_subplot(122)
+        mode_count = self._get_annual_mode_count(year)
+        (patches, texts, autotexts) = mode_count_plot.pie(list(mode_count.values()), labels=mode_count.keys(), autopct='%1.1f%%', shadow=False)
+        for p in patches:
+            # Make the patches partially transparent.
+            p.set_alpha(0.75)
+        mode_count_plot.set_title("Modes used")
+
+        self.summary["YEARLY_STATISTICS"].canvas.draw()
+
+        return
+
+    def _find_year_bounds(self):
+        """ Find the years of the oldest and newest QSOs across all logs in the logbook. """
+
+        c = self.connection.cursor()
+        max_years = []
+        min_years = []
+        for log in self.logs:
+            query = "SELECT min(QSO_DATE), max(QSO_DATE) FROM %s" % (log.name)
+            c.execute(query)
+            years = c.fetchone()
+            if years[0] and years[1]:
+                min_years.append(int(years[0][:4]))
+                max_years.append(int(years[1][:4]))
+
+        if len(min_years) == 0 or max_years == 0:
+            return None, None
+        else:
+            # Return the min and max across all logs.
+            return min(min_years), max(max_years)
+
+    def _get_annual_contact_count(self, year):
+        """ Find the total number of contacts made in each month in the specified year. """
+
+        contact_count = {}
+        c = self.connection.cursor()
+
+        for log in self.logs:
+            query = "SELECT QSO_DATE, count(QSO_DATE) FROM %s WHERE QSO_DATE >= %d0101 AND QSO_DATE < %d0101 GROUP by QSO_DATE" % (log.name, year, year+1)
+            c.execute(query)
+            xy = c.fetchall()
+
+            for i in range(len(xy)):
+                date_str = xy[i][0]
+                y = int(date_str[0:4])
+                m = int(date_str[4:6])
+                date = datetime(y, m, 1)  # Collect all contacts together by month.
+                if date in contact_count.keys():
+                    contact_count[date] += xy[i][1]
+                else:
+                    contact_count[date] = xy[i][1]
+
+        return contact_count
+
+    def _get_annual_mode_count(self, year):
+        """ Find the total number of contacts made with each mode in a specified year. """
+
+        mode_count = {}
+
+        for log in self.logs:
+            query = "SELECT MODE, count(MODE) FROM %s WHERE QSO_DATE >= %d0101 AND QSO_DATE < %d0101 GROUP by MODE" % (log.name, year, year+1)
+            c = self.connection.cursor()
+            c.execute(query)
+            xy = c.fetchall()
+
+            for i in range(len(xy)):
+                mode = xy[i][0]
+                if mode == "":
+                    mode = "Unspecified"
+
+                # Add to running total
+                if mode in mode_count.keys():
+                    mode_count[mode] += xy[i][1]
+                else:
+                    mode_count[mode] = xy[i][1]
+
+        return mode_count
+
+    def update_summary(self):
+        """ Update the information presented on the summary page. """
+
+        self.summary["LOG_COUNT"].set_label(str(self.get_number_of_logs()))
+        self.summary["QSO_COUNT"].set_label(str(self.get_number_of_qsos()))
+        try:
+            t = datetime.fromtimestamp(getmtime(self.path)).strftime("%d %B %Y @ %H:%M")
+            self.summary["DATE_MODIFIED"].set_label(str(t))
+        except (IOError, OSError) as e:
+            logging.exception(e)
+        return
 
-      l = Log(self.connection, log_name) # Empty log
-      l.populate()
+    def _on_switch_page(self, widget, label, new_page):
+        """ Handle a tab/page change, and enable/disable the relevant Record-related buttons. """
 
-      self.logs.append(l)
-      self._render_log(self.get_number_of_logs()-1)
-      self.update_summary()
+        if(new_page == self.get_n_pages()-1):  # The last (right-most) tab is the "New Log" tab.
+            self.stop_emission("switch-page")
 
-      self.set_current_page(self.get_number_of_logs())
-      return
+        # Disable the record buttons if a log page is not selected.
+        if(new_page == 0):
+            self.parent.toolbar.set_record_buttons_sensitive(False)
+            self.parent.menu.set_record_items_sensitive(False)
+        else:
+            self.parent.toolbar.set_record_buttons_sensitive(True)
+            self.parent.menu.set_record_items_sensitive(True)
+        return
 
-   def delete_log(self, widget, page=None):
-      """ Delete the log that is currently selected in the logbook. """
-      if(self.connection is None):
-         return
-         
-      if(page is None):
-         page_index = self.get_current_page() # Gets the index of the selected tab in the logbook
-         if(page_index == 0): # If we are on the Summary page...
-            logging.debug("No log currently selected!")
+    def new_log(self, widget=None):
+        """ Create a new log in the logbook. """
+
+        if(self.connection is None):
             return
-         else:
-            page = self.get_nth_page(page_index) # Gets the Gtk.VBox of the selected tab in the logbook
-
-      log_index = self._get_log_index(name=page.get_name())
-      log = self.logs[log_index]
-      
-      # We also need the page's index in order to remove it using remove_page below.   
-      # This may not be the same as what self.get_current_page() returns.  
-      page_index = self.page_num(page)
-            
-      if(page_index == 0 or page_index == self.get_n_pages()-1): # Only the "New Log" tab is present (i.e. no actual logs in the logbook)
-         logging.debug("No logs to delete!")
-         return
-
-      response = question(parent=self.parent, message="Are you sure you want to delete log %s?" % log.name)
-      if(response == Gtk.ResponseType.YES):
-         try:
-            with self.connection:
-               c = self.connection.cursor()
-               c.execute("DROP TABLE %s" % log.name)
-         except sqlite.Error as e:
-            logging.exception(e)
-            error(parent=self.parent, message="Database error. Could not delete the log.")
+        exists = True
+        dialog = LogNameDialog(self.parent)
+        while(exists):
+            response = dialog.run()
+            if(response == Gtk.ResponseType.OK):
+                log_name = dialog.get_log_name()
+                try:
+                    with self.connection:
+                        c = self.connection.cursor()
+                        query = "CREATE TABLE %s (id INTEGER PRIMARY KEY AUTOINCREMENT" % log_name
+                        for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
+                            s = ", %s TEXT" % field_name.lower()
+                            query = query + s
+                        query = query + ")"
+                        c.execute(query)
+                        exists = False
+                except sqlite.Error as e:
+                    logging.exception(e)
+                    # Data is not valid - inform the user.
+                    error(parent=self.parent, message="Database error. Try another log name.")
+                    exists = True
+            else:
+                dialog.destroy()
+                return
+
+        dialog.destroy()
+
+        l = Log(self.connection, log_name)  # Empty log
+        l.populate()
+
+        self.logs.append(l)
+        self._render_log(self.get_number_of_logs()-1)
+        self.update_summary()
+
+        self.set_current_page(self.get_number_of_logs())
+        return
+
+    def delete_log(self, widget, page=None):
+        """ Delete the log that is currently selected in the logbook.
+
+        :arg Gtk.Widget page: An optional argument corresponding to the currently-selected page/tab.
+        """
+        if(self.connection is None):
+            return
+
+        if(page is None):
+            page_index = self.get_current_page()  # Gets the index of the selected tab in the logbook
+            if(page_index == 0):  # If we are on the Summary page...
+                logging.debug("No log currently selected!")
+                return
+            else:
+                page = self.get_nth_page(page_index)  # Gets the Gtk.VBox of the selected tab in the logbook
+
+        log_index = self._get_log_index(name=page.get_name())
+        log = self.logs[log_index]
+
+        # We also need the page's index in order to remove it using remove_page below.
+        # This may not be the same as what self.get_current_page() returns.
+        page_index = self.page_num(page)
+
+        if(page_index == 0 or page_index == self.get_n_pages()-1):  # Only the "New Log" tab is present (i.e. no actual logs in the logbook)
+            logging.debug("No logs to delete!")
             return
 
-         self.logs.pop(log_index)
-         # Remove the log from the renderers too
-         self.treeview.pop(log_index)
-         self.treeselection.pop(log_index)
-         self.sorter.pop(log_index)
-         self.filter.pop(log_index)
-         # And finally remove the tab in the Logbook
-         self.remove_page(page_index)
-
-      self.update_summary()
-      self.parent.toolbox.awards.count()
-      return
-
-   def filter_logs(self, widget):
-      """ Re-filter all the logs when the user-defined expression is changed. """
-      for i in range(0, len(self.filter)):
-         self.filter[i].refilter()
-      return
-
-   def _filter_by_callsign(self, model, iter, data):
-      """ Filter all the logs in the logbook by the callsign field, based on a user-defined expression. """
-      value = model.get_value(iter, 1)
-      callsign = self.parent.toolbar.filter_source.get_text()
-      
-      if(callsign is None or callsign == ""):
-         # If there is nothing to filter with, then show all the records!
-         return True
-      else:
-         # This should be case insensitive. 
-         # Also, we could use value[:][0:len(callsign))] if we wanted to match from the very start of each callsign.
-         return callsign.upper() in value or callsign.lower() in value
-
-   def _render_log(self, index):
-      """ Render the Log (identified by 'index') in the Gtk.Notebook. """
-      self.filter.append(self.logs[index].filter_new(root=None))
-      # Set the callsign column as the column we want to filter by
-      self.filter[index].set_visible_func(self._filter_by_callsign, data=None)
-      self.sorter.append(Gtk.TreeModelSort(model=self.filter[index]))
-      self.sorter[index].set_sort_column_id(0, Gtk.SortType.ASCENDING)
-
-      self.treeview.append(Gtk.TreeView(self.sorter[index]))
-      self.treeview[index].set_grid_lines(Gtk.TreeViewGridLines.BOTH)
-      self.treeview[index].connect("row-activated", self.edit_record_callback)
-      self.treeselection.append(self.treeview[index].get_selection())
-      self.treeselection[index].set_mode(Gtk.SelectionMode.SINGLE)
-      # Allow the Log to be scrolled up/down
-      sw = Gtk.ScrolledWindow()
-      sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
-      sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
-      sw.add(self.treeview[index])
-      vbox = Gtk.VBox()
-      vbox.set_name(self.logs[index].name) # Set a name for the tab itself so we can match it up with the associated Log object later.
-      vbox.pack_start(sw, True, True, 0)
-
-      # Add a close button to the tab
-      hbox = Gtk.HBox(False, 0)
-      label = Gtk.Label(self.logs[index].name)
-      hbox.pack_start(label, False, False, 0)
-      hbox.show_all()
-
-      self.insert_page(vbox, hbox, index+1) # Append the new log as a new tab
-
-      # The first column of the logbook will always be the unique record index.
-      # Let's append this separately to the field names.
-      renderer = Gtk.CellRendererText()
-      column = Gtk.TreeViewColumn("Index", renderer, text=0)
-      column.set_resizable(True)
-      column.set_min_width(50)
-      column.set_clickable(True)
-      column.set_sort_order(Gtk.SortType.ASCENDING)
-      column.set_sort_indicator(True)
-      column.connect("clicked", self.sort_log, 0)
-      self.treeview[index].append_column(column)
-         
-      # Set up column names for each selected field
-      field_names = AVAILABLE_FIELD_NAMES_ORDERED
-      for i in range(0, len(field_names)):
-         renderer = Gtk.CellRendererText()
-         column = Gtk.TreeViewColumn(AVAILABLE_FIELD_NAMES_FRIENDLY[field_names[i]], renderer, text=i+1)
-         column.set_resizable(True)
-         column.set_min_width(50)
-         column.set_clickable(True)
-
-         # Special cases
-         if(field_names[i] == "NOTES"):
-            # Give the 'Notes' column some extra space, since this is likely to contain some long sentences...
-            column.set_min_width(300)
-            # ... but don't let it automatically re-size itself.
-            column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
-
-         column.connect("clicked", self.sort_log, i+1)
-
-         config = ConfigParser.ConfigParser()
-         have_config = (config.read(expanduser('~/.pyqso.ini')) != [])
-         (section, option) = ("view", AVAILABLE_FIELD_NAMES_ORDERED[i].lower())
-         if(have_config and config.has_option(section, option)):
-            column.set_visible(config.get(section, option) == "True")
-         self.treeview[index].append_column(column)
-
-      self.show_all()
-      return
-
-   def _compare_date_and_time(self, model, row1, row2, user_data):
-      """ Compares two rows in a Gtk.ListStore, and sorts by both date and time. """
-      date1 = model.get_value(row1, user_data[0])
-      date2 = model.get_value(row2, user_data[0])
-      time1 = model.get_value(row1, user_data[1])
-      time2 = model.get_value(row2, user_data[1])
-      if(date1 < date2):
-         return 1
-      elif(date1 == date2):
-         # If the dates are the same, then let's also sort by time.
-         if(time1 > time2):
+        response = question(parent=self.parent, message="Are you sure you want to delete log %s?" % log.name)
+        if(response == Gtk.ResponseType.YES):
+            try:
+                with self.connection:
+                    c = self.connection.cursor()
+                    c.execute("DROP TABLE %s" % log.name)
+            except sqlite.Error as e:
+                logging.exception(e)
+                error(parent=self.parent, message="Database error. Could not delete the log.")
+                return
+
+            self.logs.pop(log_index)
+            # Remove the log from the renderers too
+            self.treeview.pop(log_index)
+            self.treeselection.pop(log_index)
+            self.sorter.pop(log_index)
+            self.filter.pop(log_index)
+            # And finally remove the tab in the Logbook
+            self.remove_page(page_index)
+
+        self.update_summary()
+        self.parent.toolbox.awards.count()
+        return
+
+    def filter_logs(self, widget=None):
+        """ Re-filter all the logs when the user-defined expression is changed. """
+        for i in range(0, len(self.filter)):
+            self.filter[i].refilter()
+        return
+
+    def _filter_by_callsign(self, model, iter, data):
+        """ Filter all the logs in the logbook by the callsign field, based on a user-defined expression.
+
+        :arg Gtk.TreeModel model: The model used to filter the log data.
+        :arg Gtk.TreeIter iter: A pointer to a particular row in the model.
+        :arg data: The user-defined expression to filter by.
+        :returns: True if a record matches the expression, or if there is nothing to filter. Otherwise, returns False.
+        :rtype: bool
+        """
+        value = model.get_value(iter, 1)
+        callsign = self.parent.toolbar.filter_source.get_text()
+
+        if(callsign is None or callsign == ""):
+            # If there is nothing to filter with, then show all the records!
+            return True
+        else:
+            # This should be case insensitive.
+            # Also, we could use value[:][0:len(callsign))] if we wanted to match from the very start of each callsign.
+            return callsign.upper() in value or callsign.lower() in value
+
+    def _render_log(self, index):
+        """ Render a Log in the Gtk.Notebook.
+
+        :arg int index: The index of the Log (in the list of Logs) to render.
+        """
+        self.filter.append(self.logs[index].filter_new(root=None))
+        # Set the callsign column as the column we want to filter by
+        self.filter[index].set_visible_func(self._filter_by_callsign, data=None)
+        self.sorter.append(Gtk.TreeModelSort(model=self.filter[index]))
+        self.sorter[index].set_sort_column_id(0, Gtk.SortType.ASCENDING)
+
+        self.treeview.append(Gtk.TreeView(self.sorter[index]))
+        self.treeview[index].set_grid_lines(Gtk.TreeViewGridLines.BOTH)
+        self.treeview[index].connect("row-activated", self.edit_record_callback)
+        self.treeselection.append(self.treeview[index].get_selection())
+        self.treeselection[index].set_mode(Gtk.SelectionMode.SINGLE)
+        # Allow the Log to be scrolled up/down
+        sw = Gtk.ScrolledWindow()
+        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
+        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+        sw.add(self.treeview[index])
+        vbox = Gtk.VBox()
+        vbox.set_name(self.logs[index].name)  # Set a name for the tab itself so we can match it up with the associated Log object later.
+        vbox.pack_start(sw, True, True, 0)
+
+        # Add a close button to the tab
+        hbox = Gtk.HBox(False, 0)
+        label = Gtk.Label(self.logs[index].name)
+        hbox.pack_start(label, False, False, 0)
+        hbox.show_all()
+
+        self.insert_page(vbox, hbox, index+1)  # Append the new log as a new tab
+
+        # The first column of the logbook will always be the unique record index.
+        # Let's append this separately to the field names.
+        renderer = Gtk.CellRendererText()
+        column = Gtk.TreeViewColumn("Index", renderer, text=0)
+        column.set_resizable(True)
+        column.set_min_width(50)
+        column.set_clickable(True)
+        column.set_sort_order(Gtk.SortType.ASCENDING)
+        column.set_sort_indicator(True)
+        column.connect("clicked", self.sort_log, 0)
+        self.treeview[index].append_column(column)
+
+        # Set up column names for each selected field
+        field_names = AVAILABLE_FIELD_NAMES_ORDERED
+        for i in range(0, len(field_names)):
+            renderer = Gtk.CellRendererText()
+            column = Gtk.TreeViewColumn(AVAILABLE_FIELD_NAMES_FRIENDLY[field_names[i]], renderer, text=i+1)
+            column.set_resizable(True)
+            column.set_min_width(50)
+            column.set_clickable(True)
+
+            # Special cases
+            if(field_names[i] == "NOTES"):
+                # Give the 'Notes' column some extra space, since this is likely to contain some long sentences...
+                column.set_min_width(300)
+                # ... but don't let it automatically re-size itself.
+                column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
+
+            column.connect("clicked", self.sort_log, i+1)
+
+            config = configparser.ConfigParser()
+            have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != [])
+            (section, option) = ("view", AVAILABLE_FIELD_NAMES_ORDERED[i].lower())
+            if(have_config and config.has_option(section, option)):
+                column.set_visible(config.get(section, option) == "True")
+            self.treeview[index].append_column(column)
+
+        self.show_all()
+        return
+
+    def _compare_date_and_time(self, model, row1, row2, user_data):
+        """ Compare two rows (let's call them A and B) in a Gtk.ListStore, and sort by both date and time.
+
+        :arg Gtk.TreeModel model: The model used to sort the log data.
+        :arg Gtk.TreeIter row1: The pointer to row A.
+        :arg Gtk.TreeIter row2: The pointer to row B.
+        :arg user_data: The specific column from which to retrieve data for rows A and B.
+        :returns: 1 if Row B's date/time is more recent than Row A's; 0 if both dates and times are the same; -1 if Row A's date/time is more recent than Row B's.
+        :rtype: int
+        """
+        date1 = model.get_value(row1, user_data[0])
+        date2 = model.get_value(row2, user_data[0])
+        time1 = model.get_value(row1, user_data[1])
+        time2 = model.get_value(row2, user_data[1])
+        if(date1 < date2):
+            return 1
+        elif(date1 == date2):
+            # If the dates are the same, then let's also sort by time.
+            if(time1 > time2):
+                return -1
+            elif(time1 == time2):
+                return 0
+            else:
+                return 1
+        else:
             return -1
-         elif(time1 == time2):
-            return 0
-         else:
+
+    def _compare_default(self, model, row1, row2, user_data):
+        """ The default sorting function for all Gtk.ListStore objects.
+
+        :arg Gtk.TreeModel model: The model used to sort the log data.
+        :arg Gtk.TreeIter row1: The pointer to row A.
+        :arg Gtk.TreeIter row2: The pointer to row B.
+        :arg user_data: The specific column from which to retrieve data for rows A and B.
+        :returns: 1 if the value of Row A's column value is less than Row B's column value; 0 if both values are the same; -1 if Row A's column value is greater than Row B's column value.
+        :rtype: int
+        """
+        value1 = model.get_value(row1, user_data)
+        value2 = model.get_value(row2, user_data)
+        if(value1 < value2):
             return 1
-      else:
-         return -1
-
-   def _compare_default(self, model, row1, row2, user_data):
-      """ The default sorting function for all Gtk.ListStore objects. """
-      value1 = model.get_value(row1, user_data)
-      value2 = model.get_value(row2, user_data)
-      if(value1 < value2):
-         return 1
-      elif(value1 == value2):
-         return 0
-      else:
-         return -1
-
-   def sort_log(self, widget, column_index):
-      """ Sort the log (that is currently selected) based on the column identified by column_index. """
-      log_index = self._get_log_index()
-      column = self.treeview[log_index].get_column(column_index)
-
-      if(AVAILABLE_FIELD_NAMES_ORDERED[column_index-1] == "QSO_DATE"):
-         # If the field being sorted is the QSO_DATE, then also sort by the TIME_ON field so we get the
-         # correct chronological order.
-         # Note: This assumes that the TIME_ON field is always immediately to the right of the QSO_DATE field.
-         self.sorter[log_index].set_sort_func(column_index, self._compare_date_and_time, user_data=[column_index, column_index+1])
-      else:
-         self.sorter[log_index].set_sort_func(column_index, self._compare_default, user_data=column_index)
-
-      # If we are operating on the currently-sorted column...
-      if(self.sorter[log_index].get_sort_column_id()[0] == column_index):
-         order = column.get_sort_order()
-         # ...then check if we need to reverse the order of searching.
-         if(order == Gtk.SortType.ASCENDING):
-            self.sorter[log_index].set_sort_column_id(column_index, Gtk.SortType.DESCENDING)
-            column.set_sort_order(Gtk.SortType.DESCENDING)
-         else:
+        elif(value1 == value2):
+            return 0
+        else:
+            return -1
+
+    def sort_log(self, widget, column_index):
+        """ Sort the log (that is currently selected) with respect to a given field.
+
+        :arg int column_index: The index of the column to sort by.
+        """
+
+        log_index = self._get_log_index()
+        column = self.treeview[log_index].get_column(column_index)
+
+        if(AVAILABLE_FIELD_NAMES_ORDERED[column_index-1] == "QSO_DATE"):
+            # If the field being sorted is the QSO_DATE, then also sort by the TIME_ON field so we get the
+            # correct chronological order.
+            # Note: This assumes that the TIME_ON field is always immediately to the right of the QSO_DATE field.
+            self.sorter[log_index].set_sort_func(column_index, self._compare_date_and_time, user_data=[column_index, column_index+1])
+        else:
+            self.sorter[log_index].set_sort_func(column_index, self._compare_default, user_data=column_index)
+
+        # If we are operating on the currently-sorted column...
+        if(self.sorter[log_index].get_sort_column_id()[0] == column_index):
+            order = column.get_sort_order()
+            # ...then check if we need to reverse the order of searching.
+            if(order == Gtk.SortType.ASCENDING):
+                self.sorter[log_index].set_sort_column_id(column_index, Gtk.SortType.DESCENDING)
+                column.set_sort_order(Gtk.SortType.DESCENDING)
+            else:
+                self.sorter[log_index].set_sort_column_id(column_index, Gtk.SortType.ASCENDING)
+                column.set_sort_order(Gtk.SortType.ASCENDING)
+        else:
+            # Otherwise, change to the new sorted column. Default to ASCENDING order.
             self.sorter[log_index].set_sort_column_id(column_index, Gtk.SortType.ASCENDING)
             column.set_sort_order(Gtk.SortType.ASCENDING)
-      else:
-         # Otherwise, change to the new sorted column. Default to ASCENDING order.
-         self.sorter[log_index].set_sort_column_id(column_index, Gtk.SortType.ASCENDING)
-         column.set_sort_order(Gtk.SortType.ASCENDING)
-
-         # Show an arrow pointing in the direction of the sorting.
-         # (First we need to remove the arrow from the previously-sorted column.
-         # Since we don't know which one that was, just remove the arrow from all columns
-         # and start again. This only loops over a few dozen columns at most, so
-         # hopefully it won't take too much time.)
-         for i in range(0, len(AVAILABLE_FIELD_NAMES_ORDERED)):
-            column = self.treeview[log_index].get_column(i)
-            column.set_sort_indicator(False)
-         column = self.treeview[log_index].get_column(column_index)
-         column.set_sort_indicator(True)
-      return
-      
-   def rename_log(self, widget=None):
-      """ Rename the log that is currently selected. """
-      if(self.connection is None):
-         return
-      page_index = self.get_current_page()
-      if(page_index == 0): # If we are on the Summary page...
-         logging.debug("No log currently selected!")
-         return
-      page = self.get_nth_page(page_index) # Gets the Gtk.VBox of the selected tab in the logbook
-      old_log_name = page.get_name()
-      
-      log_index = self._get_log_index(name=old_log_name)
-      
-      exists = True
-      dialog = LogNameDialog(self.parent, title="Rename Log", name=old_log_name)
-      while(exists):
-         response = dialog.run()
-         if(response == Gtk.ResponseType.OK):
-            new_log_name = dialog.get_log_name()
-            try:
-               with self.connection:
-                  c = self.connection.cursor()
-                  query = "ALTER TABLE %s RENAME TO %s" % (old_log_name, new_log_name)
-                  c.execute(query)
-                  exists = False
-            except sqlite.Error as e:
-               logging.exception(e)
-               # Data is not valid - inform the user.
-               error(parent=self.parent, message="Database error. Try another log name.")
-               exists = True
-         else:
-            dialog.destroy()
+
+            # Show an arrow pointing in the direction of the sorting.
+            # (First we need to remove the arrow from the previously-sorted column.
+            # Since we don't know which one that was, just remove the arrow from all columns
+            # and start again. This only loops over a few dozen columns at most, so
+            # hopefully it won't take too much time.)
+            for i in range(0, len(AVAILABLE_FIELD_NAMES_ORDERED)):
+                column = self.treeview[log_index].get_column(i)
+                column.set_sort_indicator(False)
+            column = self.treeview[log_index].get_column(column_index)
+            column.set_sort_indicator(True)
+        return
+
+    def rename_log(self, widget=None):
+        """ Rename the log that is currently selected. """
+        if(self.connection is None):
+            return
+        page_index = self.get_current_page()
+        if(page_index == 0):  # If we are on the Summary page...
+            logging.debug("No log currently selected!")
+            return
+        page = self.get_nth_page(page_index)  # Gets the Gtk.VBox of the selected tab in the logbook
+        old_log_name = page.get_name()
+
+        log_index = self._get_log_index(name=old_log_name)
+
+        exists = True
+        dialog = LogNameDialog(self.parent, title="Rename Log", name=old_log_name)
+        while(exists):
+            response = dialog.run()
+            if(response == Gtk.ResponseType.OK):
+                new_log_name = dialog.get_log_name()
+                try:
+                    with self.connection:
+                        c = self.connection.cursor()
+                        query = "ALTER TABLE %s RENAME TO %s" % (old_log_name, new_log_name)
+                        c.execute(query)
+                        exists = False
+                except sqlite.Error as e:
+                    logging.exception(e)
+                    # Data is not valid - inform the user.
+                    error(parent=self.parent, message="Database error. Try another log name.")
+                    exists = True
+            else:
+                dialog.destroy()
+                return
+
+        dialog.destroy()
+
+        # Remember to change the Log object's name...
+        self.logs[log_index].name = new_log_name
+
+        # ...and the page's name
+        page.set_name(self.logs[log_index].name)
+
+        # ...and update the tab's label
+        hbox = Gtk.HBox(False, 0)
+        label = Gtk.Label(new_log_name)
+        hbox.pack_start(label, False, False, 0)
+        hbox.show_all()
+        self.set_tab_label(page, hbox)
+
+        # The number of logs will obviously stay the same, but
+        # we want to update the logbook's modification date.
+        self.update_summary()
+
+        return
+
+    def import_log(self, widget=None):
+        """ Import a log from an ADIF file. """
+        dialog = Gtk.FileChooserDialog("Import ADIF Log File",
+                                       self.parent,
+                                       Gtk.FileChooserAction.OPEN,
+                                      (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                                       Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
+        filter = Gtk.FileFilter()
+        filter.set_name("All ADIF files (*.adi, *.ADI)")
+        filter.add_pattern("*.adi")
+        filter.add_pattern("*.ADI")
+        dialog.add_filter(filter)
+
+        filter = Gtk.FileFilter()
+        filter.set_name("All files")
+        filter.add_pattern("*")
+        dialog.add_filter(filter)
+
+        response = dialog.run()
+        if(response == Gtk.ResponseType.OK):
+            path = dialog.get_filename()
+        else:
+            path = None
+        dialog.destroy()
+
+        if(path is None):
+            logging.debug("No file path specified.")
             return
 
-      dialog.destroy()
-      
-      # Remember to change the Log object's name...
-      self.logs[log_index].name = new_log_name
-      
-      # ...and the page's name
-      page.set_name(self.logs[log_index].name)
-
-      # ...and update the tab's label
-      hbox = Gtk.HBox(False, 0)
-      label = Gtk.Label(new_log_name)
-      hbox.pack_start(label, False, False, 0)
-      hbox.show_all()
-      self.set_tab_label(page, hbox)
-      
-      # The number of logs will obviously stay the same, but
-      # we want to update the logbook's modification date.
-      self.update_summary()
-      
-      return
-
-   def import_log(self, widget=None):
-      """ Import a log from an ADIF file. """
-      dialog = Gtk.FileChooserDialog("Import ADIF Log File",
-                                    None,
-                                    Gtk.FileChooserAction.OPEN,
-                                    (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
-                                    Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
-      filter = Gtk.FileFilter()
-      filter.set_name("All ADIF files (*.adi)")
-      filter.add_pattern("*.adi")
-      dialog.add_filter(filter)
-
-      filter = Gtk.FileFilter()
-      filter.set_name("All files")
-      filter.add_pattern("*")
-      dialog.add_filter(filter)
-      
-      response = dialog.run()
-      if(response == Gtk.ResponseType.OK):
-         path = dialog.get_filename()
-      else:
-         path = None
-      dialog.destroy()
-      
-      if(path is None):
-         logging.debug("No file path specified.")
-         return
-
-      dialog = LogNameDialog(self.parent, title="Import Log")
-      while(True):
-         response = dialog.run()
-         if(response == Gtk.ResponseType.OK):
-            log_name = dialog.get_log_name()
-            if(self.log_name_exists(log_name)):
-               # Import into existing log
-               exists = True
-               l = self.logs[self._get_log_index(name=log_name)]
-               response = question(parent=self.parent, message="Are you sure you want to import into an existing log?")
-               if(response == Gtk.ResponseType.YES):
-                  break
-            elif(self.log_name_exists(log_name) is None):
-               # Could not determine if the log name exists. It's safer to stop here than to try to add a new log.
-               error(parent=self.parent, message="Database error. Could not check if the log name exists.")
-               dialog.destroy()
-               return
+        dialog = LogNameDialog(self.parent, title="Import Log")
+        while(True):
+            response = dialog.run()
+            if(response == Gtk.ResponseType.OK):
+                log_name = dialog.get_log_name()
+                if(self.log_name_exists(log_name)):
+                    # Import into existing log
+                    exists = True
+                    l = self.logs[self._get_log_index(name=log_name)]
+                    response = question(parent=self.parent, message="Are you sure you want to import into an existing log?")
+                    if(response == Gtk.ResponseType.YES):
+                        break
+                elif(self.log_name_exists(log_name) is None):
+                    # Could not determine if the log name exists. It's safer to stop here than to try to add a new log.
+                    error(parent=self.parent, message="Database error. Could not check if the log name exists.")
+                    dialog.destroy()
+                    return
+                else:
+                    # Create a new log with the name the user supplies
+                    exists = False
+                    try:
+                        with self.connection:
+                            c = self.connection.cursor()
+                            query = "CREATE TABLE %s (id INTEGER PRIMARY KEY AUTOINCREMENT" % log_name
+                            for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
+                                s = ", %s TEXT" % field_name.lower()
+                                query = query + s
+                            query = query + ")"
+                            c.execute(query)
+                            l = Log(self.connection, log_name)
+                            break
+                    except sqlite.Error as e:
+                        logging.exception(e)
+                        # Data is not valid - inform the user.
+                        error(parent=self.parent, message="Database error. Try another log name.")
             else:
-               # Create a new log with the name the user supplies
-               exists = False
-               try:
-                  with self.connection:
-                     c = self.connection.cursor()
-                     query = "CREATE TABLE %s (id INTEGER PRIMARY KEY AUTOINCREMENT" % log_name
-                     for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
-                        s = ", %s TEXT" % field_name.lower()
-                        query = query + s
-                     query = query + ")"
-                     c.execute(query)
-                     l = Log(self.connection, log_name)
-                     break
-               except sqlite.Error as e:
-                  logging.exception(e)
-                  # Data is not valid - inform the user.
-                  error(parent=self.parent, message="Database error. Try another log name.")
-         else:
-            dialog.destroy()
+                dialog.destroy()
+                return
+
+        dialog.destroy()
+
+        adif = ADIF()
+        logging.debug("Importing records from the ADIF file with path: %s" % path)
+        records = adif.read(path)
+        l.add_record(records)
+        l.populate()
+
+        if(not exists):
+            self.logs.append(l)
+            self._render_log(self.get_number_of_logs()-1)
+        self.update_summary()
+        self.parent.toolbox.awards.count()
+
+        return
+
+    def export_log(self, widget=None):
+        """ Export the log (that is currently selected) to an ADIF file. """
+        page_index = self.get_current_page()  # Gets the index of the selected tab in the logbook
+        if(page_index == 0):  # If we are on the Summary page...
+            logging.debug("No log currently selected!")
             return
-      
-      dialog.destroy()
-
-      adif = ADIF()
-      records = adif.read(path)      
-      logging.debug("Importing records from the ADIF file with path: %s" % path)
-      for record in records:
-         l.add_record(record)
-      l.populate()
-
-      if(not exists):
-         self.logs.append(l)
-         self._render_log(self.get_number_of_logs()-1)
-      self.update_summary()
-      self.parent.toolbox.awards.count()
-      
-      return
-      
-   def export_log(self, widget=None):
-      """ Export the log (that is currently selected) to an ADIF file. """
-      page_index = self.get_current_page() # Gets the index of the selected tab in the logbook
-      if(page_index == 0): # If we are on the Summary page...
-         logging.debug("No log currently selected!")
-         return
-
-      log_index = self._get_log_index()
-      log = self.logs[log_index]
-
-      dialog = Gtk.FileChooserDialog("Export Log to File",
-                              None,
-                              Gtk.FileChooserAction.SAVE,
-                              (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
-                              Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
-      dialog.set_do_overwrite_confirmation(True)
-
-      filter = Gtk.FileFilter()
-      filter.set_name("All ADIF files (*.adi)")
-      filter.add_pattern("*.adi")
-      dialog.add_filter(filter)
-
-      filter = Gtk.FileFilter()
-      filter.set_name("All files")
-      filter.add_pattern("*")
-      dialog.add_filter(filter)
-
-      response = dialog.run()
-      if(response == Gtk.ResponseType.OK):
-         path = dialog.get_filename()
-      else:
-         path = None
-      dialog.destroy()
-         
-      if(path is None):
-         logging.debug("No file path specified.")
-      else:
-         adif = ADIF()
-         records = log.get_all_records()
-         if(records is not None):
-            adif.write(records, path)
-         else:
-            error(self.parent, "Could not retrieve the records from the SQL database. No records have been exported.")
-      return
-
-   def print_log(self, widget=None):
-      """ Print all the records in the log (that is currently selected). 
-      Note that only a few important fields are printed because of the restricted width of the page. """
-      page_index = self.get_current_page() # Gets the index of the selected tab in the logbook
-      if(page_index == 0): # If we are on the Summary page...
-         logging.debug("No log currently selected!")
-         return
-      log_index = self._get_log_index()
-      log = self.logs[log_index]
-
-      self.text_to_print = "Callsign\t---\tDate\t---\tTime\t---\tFrequency\t---\tMode\n"
-      records = log.get_all_records()
-      if(records is not None):
-         for r in records:
-            self.text_to_print += str(r["CALL"]) + "\t---\t" + str(r["QSO_DATE"]) + "\t---\t" + str(r["TIME_ON"]) + "\t---\t" + str(r["FREQ"]) + "\t---\t" + str(r["MODE"]) + "\n"
-
-         action = Gtk.PrintOperationAction.PRINT_DIALOG
-         operation = Gtk.PrintOperation()
-         operation.set_default_page_setup(Gtk.PageSetup())
-         operation.set_unit(Gtk.Unit.MM)
-
-         operation.connect("begin_print", self._begin_print)
-         operation.connect("draw_page", self._draw_page)
-         result = operation.run(action, parent=self.parent)
-      else:
-         error(self.parent, "Could not retrieve the records from the SQL database. No records have been printed.")
-      return
-    
-   def _begin_print(self, operation, context):
-      width = context.get_width()
-      height = context.get_height()
-      layout = context.create_pango_layout()
-      layout.set_font_description(Pango.FontDescription("normal 10"))
-      layout.set_width(int(width*Pango.SCALE))
-      layout.set_text(self.text_to_print, -1)
-
-      number_of_pages = 0
-      page_height = 0
-      for line in range(0, layout.get_line_count()):
-         layout_line = layout.get_line(line)
-         ink_rectangle, logical_rectangle = layout_line.get_extents()
-         x_bearing, y_bearing, logical_rectangle_width, logical_rectangle_height = logical_rectangle.x, logical_rectangle.y, logical_rectangle.width, logical_rectangle.height
-         self.line_height = logical_rectangle.height/1024.0 + 3
-         page_height += self.line_height
-         if(page_height + self.line_height > height):
-            number_of_pages += 1
-            page_height = self.line_height
-      operation.set_n_pages(number_of_pages + 1)
-      self.text_to_print = self.text_to_print.split("\n")
-      return
-
-   def _draw_page(self, operation, context, page_number):
-      cr = context.get_cairo_context()
-      cr.set_source_rgb(0, 0, 0)
-      layout = context.create_pango_layout()
- 
-      current_line_number = 0
-      for line in self.text_to_print:
-         layout.set_text(line, -1)  
-         cr.move_to(5, current_line_number*self.line_height)
-         PangoCairo.update_layout(cr, layout)
-         PangoCairo.show_layout(cr, layout)
-         current_line_number = current_line_number + 1
-         if(current_line_number*self.line_height > context.get_height()):
-            for j in range(0, current_line_number):
-               self.text_to_print.pop(0) # Remove what has been printed already before draw_page is called again
-            break
-      return
-
-   def add_record_callback(self, widget):
-      # Get the log index
-      try:
-         log_index = self._get_log_index()
-         if(log_index is None):
-            raise ValueError("The log index could not be determined. Perhaps you tried adding a record when the Summary page was selected?")
-      except ValueError as e:
-         error(self.parent, e)
-         return
-      log = self.logs[log_index]
-      
-      dialog = RecordDialog(parent=self.parent, log=log, index=None)
-      all_valid = False # Are all the field entries valid?
-
-      adif = ADIF()
-      while(not all_valid): 
-         # This while loop gives the user infinite attempts at giving valid data.
-         # The add/edit record window will stay open until the user gives valid data,
-         # or until the Cancel button is clicked.
-         all_valid = True
-         response = dialog.run()
-         if(response == Gtk.ResponseType.OK):
-            fields_and_data = {}
-            field_names = AVAILABLE_FIELD_NAMES_ORDERED
-            for i in range(0, len(field_names)):
-               # Validate user input.
-               fields_and_data[field_names[i]] = dialog.get_data(field_names[i])
-               if(not(adif.is_valid(field_names[i], fields_and_data[field_names[i]], AVAILABLE_FIELD_NAMES_TYPES[field_names[i]]))):
-                  # Data is not valid - inform the user.
-                  error(parent=self.parent, message="The data in field \"%s\" is not valid!" % field_names[i])
-                  all_valid = False
-                  break # Don't check the other data until the user has fixed the current one.
-
-            if(all_valid):
-               # All data has been validated, so we can go ahead and add the new record.
-               log.add_record(fields_and_data)
-               self.update_summary()
-               self.parent.toolbox.awards.count()
-               # Select the new Record's row in the treeview.
-               number_of_records = log.get_number_of_records()
-               if(number_of_records is not None):
-                  self.treeselection[log_index].select_path(number_of_records)
-
-      dialog.destroy()
-      return
-      
-   def delete_record_callback(self, widget):
-      # Get the log index
-      try:
-         log_index = self._get_log_index()
-         if(log_index is None):
-            raise ValueError("The log index could not be determined. Perhaps you tried deleting a record when the Summary page was selected?")
-      except ValueError as e:
-         error(self.parent, e)
-         return
-      log = self.logs[log_index]
-      
-      (sort_model, path) = self.treeselection[log_index].get_selected_rows() # Get the selected row in the log
-      try:
-         sort_iter = sort_model.get_iter(path[0])
-         # Remember that the filter model is a child of the sort model...
-         filter_model = sort_model.get_model()
-         filter_iter = self.sorter[log_index].convert_iter_to_child_iter(sort_iter)
-         # ...and the ListStore model (i.e. the log) is a child of the filter model.
-         child_iter = self.filter[log_index].convert_iter_to_child_iter(filter_iter)
-         row_index = log.get_value(child_iter,0)
-      except IndexError:
-         logging.debug("Trying to delete a record, but there are no records in the log!")
-         return
-
-      response = question(parent=self.parent, message = "Are you sure you want to delete record %d?" % row_index)
-      if(response == Gtk.ResponseType.YES):
-         # Deletes the record with index 'row_index' from the Records list.
-         # 'iter' is needed to remove the record from the ListStore itself.
-         log.delete_record(row_index, iter=child_iter)
-         self.update_summary()
-         self.parent.toolbox.awards.count()
-      return
-
-   def edit_record_callback(self, widget, path, view_column):
-      # Note: the path and view_column arguments need to be passed in
-      # since they associated with the row-activated signal.
-
-      # Get the log index
-      try:
-         log_index = self._get_log_index()
-         if(log_index is None):
-            raise ValueError("The log index could not be determined. Perhaps you tried editing a record when the Summary page was selected?")
-      except ValueError as e:
-         error(self.parent, e)
-         return
-      log = self.logs[log_index]
-
-      (sort_model, path) = self.treeselection[log_index].get_selected_rows() # Get the selected row in the log
-      try:
-         sort_iter = sort_model.get_iter(path[0])
-         # Remember that the filter model is a child of the sort model...
-         filter_model = sort_model.get_model()
-         filter_iter = self.sorter[log_index].convert_iter_to_child_iter(sort_iter)
-         # ...and the ListStore model (i.e. the log) is a child of the filter model.
-         child_iter = self.filter[log_index].convert_iter_to_child_iter(filter_iter)
-         row_index = log.get_value(child_iter,0)
-      except IndexError:
-         logging.debug("Could not find the selected row's index!")
-         return
-
-      dialog = RecordDialog(parent=self.parent, log=self.logs[log_index], index=row_index)
-      all_valid = False # Are all the field entries valid?
-
-      adif = ADIF()
-      while(not all_valid): 
-         # This while loop gives the user infinite attempts at giving valid data.
-         # The add/edit record window will stay open until the user gives valid data,
-         # or until the Cancel button is clicked.
-         all_valid = True
-         response = dialog.run()
-         if(response == Gtk.ResponseType.OK):
-            fields_and_data = {}
-            field_names = AVAILABLE_FIELD_NAMES_ORDERED
-            for i in range(0, len(field_names)):
-               # Validate user input.
-               fields_and_data[field_names[i]] = dialog.get_data(field_names[i])
-               if(not(adif.is_valid(field_names[i], fields_and_data[field_names[i]], AVAILABLE_FIELD_NAMES_TYPES[field_names[i]]))):
-                  # Data is not valid - inform the user.
-                  error(parent=self.parent, message="The data in field \"%s\" is not valid!" % field_names[i])
-                  all_valid = False
-                  break # Don't check the other fields until the user has fixed the current field's data.
-
-            if(all_valid):
-               # All data has been validated, so we can go ahead and update the record.
-               record = log.get_record_by_index(row_index)
-               if(record is None):
-                  message = "Could not retrieve record with row_index %d from the SQL database. The record has not been edited." % row_index
-                  logging.error(message)
-                  error(parent=self.parent, message=message)
-               else:
-                  for i in range(0, len(field_names)):
-                     # Check whether the data has actually changed. Database updates can be expensive.
-                     if(record[field_names[i].lower()] != fields_and_data[field_names[i]]):
-                        # Update the record in the database and then in the ListStore.
-                        # We add 1 onto the column_index here because we don't want to consider the index column.
-                        log.edit_record(row_index, field_names[i], fields_and_data[field_names[i]], iter=child_iter, column_index=i+1)
-                  self.update_summary()
-                  self.parent.toolbox.awards.count()
-
-      dialog.destroy()
-      return
-
-   def remove_duplicates_callback(self, widget=None):
-      """ Remove duplicate records in a log. 
-      Detecting duplicate records is done based on the CALL, QSO_DATE, TIME_ON, FREQ, and MODE fields. """
-      logging.debug("Removing duplicate records...")
-
-      log_index = self._get_log_index()
-      log = self.logs[log_index]
-
-      (number_of_duplicates, number_of_duplicates_removed) = log.remove_duplicates()
-      info(self.parent, "Found %d duplicate(s). Successfully removed %d duplicate(s)." % (number_of_duplicates, number_of_duplicates_removed))
-      return
-
-   def get_number_of_logs(self):
-      """ Return the total number of logs in the logbook. """
-      return len(self.logs)
-
-   def log_name_exists(self, table_name):
-      """ Return True if the log name already exists in the logbook, and False if it does not already exist. Return None if there is a database error. """
-      try:
-         with self.connection:
-            c = self.connection.cursor()
-            c.execute("SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE name=?)", [table_name])
-            exists = c.fetchone()
-         if(exists[0] == 1):
-            return True
-         else:
-            return False
-      except (sqlite.Error, IndexError) as e:
-         logging.exception(e) # Database error. PyQSO could not check if the log name exists.
-         return None
-
-   def _get_log_index(self, name=None):
-      """ Given the name of a log, return its index in the self.log list. """
-      if(name is None):
-         # If no page name is supplied, then just use the currently selected page
-         page_index = self.get_current_page() # Gets the index of the selected tab in the logbook
-         if(page_index == 0 or page_index == self.get_n_pages()-1):
-            # We either have the Summary page, or the "+" (add log) dummy page.
+
+        log_index = self._get_log_index()
+        log = self.logs[log_index]
+
+        dialog = Gtk.FileChooserDialog("Export Log to File",
+                                       self.parent,
+                                       Gtk.FileChooserAction.SAVE,
+                                      (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                                       Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
+        dialog.set_do_overwrite_confirmation(True)
+
+        filter = Gtk.FileFilter()
+        filter.set_name("All ADIF files (*.adi, *.ADI)")
+        filter.add_pattern("*.adi")
+        filter.add_pattern("*.ADI")
+        dialog.add_filter(filter)
+
+        filter = Gtk.FileFilter()
+        filter.set_name("All files")
+        filter.add_pattern("*")
+        dialog.add_filter(filter)
+
+        response = dialog.run()
+        if(response == Gtk.ResponseType.OK):
+            path = dialog.get_filename()
+        else:
+            path = None
+        dialog.destroy()
+
+        if(path is None):
+            logging.debug("No file path specified.")
+        else:
+            adif = ADIF()
+            records = log.get_all_records()
+            if(records is not None):
+                adif.write(records, path)
+            else:
+                error(self.parent, "Could not retrieve the records from the SQL database. No records have been exported.")
+        return
+
+    def print_log(self, widget=None):
+        """ Print all the records in the log (that is currently selected).
+        Note that only a few important fields are printed because of the restricted width of the page. """
+        page_index = self.get_current_page()  # Gets the index of the selected tab in the logbook
+        if(page_index == 0):  # If we are on the Summary page...
             logging.debug("No log currently selected!")
+            return
+        log_index = self._get_log_index()
+        log = self.logs[log_index]
+
+        self.text_to_print = "Callsign\t---\tDate\t---\tTime\t---\tFrequency\t---\tMode\n"
+        records = log.get_all_records()
+        if(records is not None):
+            for r in records:
+                self.text_to_print += str(r["CALL"]) + "\t---\t" + str(r["QSO_DATE"]) + "\t---\t" + str(r["TIME_ON"]) + "\t---\t" + str(r["FREQ"]) + "\t---\t" + str(r["MODE"]) + "\n"
+
+            action = Gtk.PrintOperationAction.PRINT_DIALOG
+            operation = Gtk.PrintOperation()
+            operation.set_default_page_setup(Gtk.PageSetup())
+            operation.set_unit(Gtk.Unit.MM)
+
+            operation.connect("begin_print", self._begin_print)
+            operation.connect("draw_page", self._draw_page)
+            operation.run(action, parent=self.parent)
+        else:
+            error(self.parent, "Could not retrieve the records from the SQL database. No records have been printed.")
+        return
+
+    def _begin_print(self, operation, context):
+        """ Specify the layout/position/font of the text on the pages to be printed.
+
+        :arg Gtk.PrintOperation operation: The printing API.
+        :arg Gtk.PrintContext context: Used to draw/render the pages to print.
+        """
+        width = context.get_width()
+        height = context.get_height()
+        layout = context.create_pango_layout()
+        layout.set_font_description(Pango.FontDescription("normal 10"))
+        layout.set_width(int(width*Pango.SCALE))
+        layout.set_text(self.text_to_print, -1)
+
+        number_of_pages = 0
+        page_height = 0
+        for line in range(0, layout.get_line_count()):
+            layout_line = layout.get_line(line)
+            ink_rectangle, logical_rectangle = layout_line.get_extents()
+            self.line_height = logical_rectangle.height/1024.0 + 3
+            page_height += self.line_height
+            if(page_height + self.line_height > height):
+                number_of_pages += 1
+                page_height = self.line_height
+        operation.set_n_pages(number_of_pages + 1)
+        self.text_to_print = self.text_to_print.split("\n")
+        return
+
+    def _draw_page(self, operation, context, page_number):
+        """ Render the QSO details on the page.
+
+        :arg Gtk.PrintOperation operation: The printing API.
+        :arg Gtk.PrintContext context: Used to draw/render the pages to print.
+        :arg int page_number: The current page number.
+        """
+        cr = context.get_cairo_context()
+        cr.set_source_rgb(0, 0, 0)
+        layout = context.create_pango_layout()
+
+        current_line_number = 0
+        for line in self.text_to_print:
+            layout.set_text(line, -1)
+            cr.move_to(5, current_line_number*self.line_height)
+            PangoCairo.update_layout(cr, layout)
+            PangoCairo.show_layout(cr, layout)
+            current_line_number += 1
+            if(current_line_number*self.line_height > context.get_height()):
+                for j in range(0, current_line_number):
+                    self.text_to_print.pop(0)  # Remove what has been printed already before draw_page is called again
+                break
+        return
+
+    def add_record_callback(self, widget):
+        """ A callback function used to add a particular record/QSO. """
+        # Get the log index
+        try:
+            log_index = self._get_log_index()
+            if(log_index is None):
+                raise ValueError("The log index could not be determined. Perhaps you tried adding a record when the Summary page was selected?")
+        except ValueError as e:
+            error(self.parent, e)
+            return
+        log = self.logs[log_index]
+
+        # Keep the dialog open after adding a record?
+        config = configparser.ConfigParser()
+        have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != [])
+        (section, option) = ("general", "keep_open")
+        if(have_config and config.has_option(section, option)):
+            keep_open = config.get("general", "keep_open") == "True"
+        else:
+            keep_open = False
+        adif = ADIF()
+
+        exit = False
+        while not exit:
+            dialog = RecordDialog(parent=self.parent, log=log, index=None)
+
+            all_valid = False  # Are all the field entries valid?
+
+            # Shall we exit the while loop (and therefore close the Add Record dialog)?
+            if keep_open:
+                exit = False
+            else:
+                exit = True
+
+            while not all_valid:
+                # This while loop gives the user infinite attempts at giving valid data.
+                # The add/edit record window will stay open until the user gives valid data,
+                # or until the Cancel button is clicked.
+                all_valid = True
+                response = dialog.run()
+                if(response == Gtk.ResponseType.OK):
+                    fields_and_data = {}
+                    field_names = AVAILABLE_FIELD_NAMES_ORDERED
+                    for i in range(0, len(field_names)):
+                        # Validate user input.
+                        fields_and_data[field_names[i]] = dialog.get_data(field_names[i])
+                        if(not(adif.is_valid(field_names[i], fields_and_data[field_names[i]], AVAILABLE_FIELD_NAMES_TYPES[field_names[i]]))):
+                            # Data is not valid - inform the user.
+                            error(parent=self.parent, message="The data in field \"%s\" is not valid!" % field_names[i])
+                            all_valid = False
+                            break  # Don't check the other data until the user has fixed the current one.
+
+                    if(all_valid):
+                        # All data has been validated, so we can go ahead and add the new record.
+                        log.add_record(fields_and_data)
+                        self.update_summary()
+                        self.parent.toolbox.awards.count()
+                        # Select the new Record's row in the treeview.
+                        number_of_records = log.get_number_of_records()
+                        if(number_of_records is not None):
+                            self.treeselection[log_index].select_path(number_of_records)
+                else:
+                    exit = True
+                    break
+            dialog.destroy()
+        return
+
+    def delete_record_callback(self, widget):
+        """ A callback function used to delete a particular record/QSO. """
+
+        # Get the log index
+        try:
+            log_index = self._get_log_index()
+            if(log_index is None):
+                raise ValueError("The log index could not be determined. Perhaps you tried deleting a record when the Summary page was selected?")
+        except ValueError as e:
+            error(self.parent, e)
+            return
+        log = self.logs[log_index]
+
+        (sort_model, path) = self.treeselection[log_index].get_selected_rows()  # Get the selected row in the log
+        try:
+            sort_iter = sort_model.get_iter(path[0])
+            filter_iter = self.sorter[log_index].convert_iter_to_child_iter(sort_iter)
+            # ...and the ListStore model (i.e. the log) is a child of the filter model.
+            child_iter = self.filter[log_index].convert_iter_to_child_iter(filter_iter)
+            row_index = log.get_value(child_iter, 0)
+        except IndexError:
+            logging.debug("Trying to delete a record, but there are no records in the log!")
+            return
+
+        response = question(parent=self.parent, message="Are you sure you want to delete record %d?" % row_index)
+        if(response == Gtk.ResponseType.YES):
+            # Deletes the record with index 'row_index' from the Records list.
+            # 'iter' is needed to remove the record from the ListStore itself.
+            log.delete_record(row_index, iter=child_iter)
+            self.update_summary()
+            self.parent.toolbox.awards.count()
+        return
+
+    def edit_record_callback(self, widget, path, view_column):
+        """ A callback function used to edit a particular record/QSO.
+        Note that the widget, path and view_column arguments are not used,
+        but need to be passed in since they associated with the row-activated signal
+        which is generated when the user double-clicks on a record. """
+
+        # Get the log index
+        try:
+            log_index = self._get_log_index()
+            if(log_index is None):
+                raise ValueError("The log index could not be determined. Perhaps you tried editing a record when the Summary page was selected?")
+        except ValueError as e:
+            error(self.parent, e)
+            return
+        log = self.logs[log_index]
+
+        (sort_model, path) = self.treeselection[log_index].get_selected_rows()  # Get the selected row in the log
+        try:
+            sort_iter = sort_model.get_iter(path[0])
+            filter_iter = self.sorter[log_index].convert_iter_to_child_iter(sort_iter)
+            # ...and the ListStore model (i.e. the log) is a child of the filter model.
+            child_iter = self.filter[log_index].convert_iter_to_child_iter(filter_iter)
+            row_index = log.get_value(child_iter, 0)
+        except IndexError:
+            logging.debug("Could not find the selected row's index!")
+            return
+
+        dialog = RecordDialog(parent=self.parent, log=self.logs[log_index], index=row_index)
+        all_valid = False  # Are all the field entries valid?
+
+        adif = ADIF()
+        while(not all_valid):
+            # This while loop gives the user infinite attempts at giving valid data.
+            # The add/edit record window will stay open until the user gives valid data,
+            # or until the Cancel button is clicked.
+            all_valid = True
+            response = dialog.run()
+            if(response == Gtk.ResponseType.OK):
+                fields_and_data = {}
+                field_names = AVAILABLE_FIELD_NAMES_ORDERED
+                for i in range(0, len(field_names)):
+                    # Validate user input.
+                    fields_and_data[field_names[i]] = dialog.get_data(field_names[i])
+                    if(not(adif.is_valid(field_names[i], fields_and_data[field_names[i]], AVAILABLE_FIELD_NAMES_TYPES[field_names[i]]))):
+                        # Data is not valid - inform the user.
+                        error(parent=self.parent, message="The data in field \"%s\" is not valid!" % field_names[i])
+                        all_valid = False
+                        break  # Don't check the other fields until the user has fixed the current field's data.
+
+                if(all_valid):
+                    # All data has been validated, so we can go ahead and update the record.
+                    record = log.get_record_by_index(row_index)
+                    if(record is None):
+                        message = "Could not retrieve record with row_index %d from the SQL database. The record has not been edited." % row_index
+                        logging.error(message)
+                        error(parent=self.parent, message=message)
+                    else:
+                        for i in range(0, len(field_names)):
+                            # Check whether the data has actually changed. Database updates can be expensive.
+                            if(record[field_names[i].lower()] != fields_and_data[field_names[i]]):
+                                # Update the record in the database and then in the ListStore.
+                                # We add 1 onto the column_index here because we don't want to consider the index column.
+                                log.edit_record(row_index, field_names[i], fields_and_data[field_names[i]], iter=child_iter, column_index=i+1)
+                        self.update_summary()
+                        self.parent.toolbox.awards.count()
+
+        dialog.destroy()
+        return
+
+    def remove_duplicates_callback(self, widget=None):
+        """ Remove duplicate records in a log.
+        Detecting duplicate records is done based on the CALL, QSO_DATE, TIME_ON, FREQ, and MODE fields. """
+        logging.debug("Removing duplicate records...")
+
+        log_index = self._get_log_index()
+        log = self.logs[log_index]
+
+        (number_of_duplicates, number_of_duplicates_removed) = log.remove_duplicates()
+        info(self.parent, "Found %d duplicate(s). Successfully removed %d duplicate(s)." % (number_of_duplicates, number_of_duplicates_removed))
+        return
+
+    def get_number_of_logs(self):
+        """ Return the total number of logs in the logbook.
+
+        :returns: The total number of logs in the logbook.
+        :rtype: int
+        """
+        return len(self.logs)
+
+    def get_number_of_qsos(self):
+        """ Return the total number of QSOs/records in the whole logbook.
+
+        :returns: The total number of QSOs/records in the whole logbook.
+        :rtype: int
+        """
+        total = 0
+        for log in self.logs:
+            total += log.get_number_of_records()
+        return total
+
+    def log_name_exists(self, table_name):
+        """ Determine whether a Log object with a given name exists in the SQL database.
+
+        :arg str table_name: The name of the log (i.e. the name of the table in the SQL database).
+        :returns: True if the log name already exists in the logbook; False if it does not already exist; None if there is a database error.
+        :rtype: bool or None
+        """
+        try:
+            with self.connection:
+                c = self.connection.cursor()
+                c.execute("SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE name=?)", [table_name])
+                exists = c.fetchone()
+            if(exists[0] == 1):
+                return True
+            else:
+                return False
+        except (sqlite.Error, IndexError) as e:
+            logging.exception(e)  # Database error. PyQSO could not check if the log name exists.
             return None
-         name = self.get_nth_page(page_index).get_name()
-      # If a page of the logbook (and therefore a Log object) gets deleted, 
-      # then the page_index may not correspond to the index of the log in the self.logs list.
-      # Therefore, we have to search for the tab with the same name as the log.
-      for i in range(0, len(self.logs)):
-         if(self.logs[i].name == name):
-            log_index = i
-            break
-      return log_index
+
+    def _get_log_index(self, name=None):
+        """ Given the name of a log, return its index in the list of Log objects.
+
+        :arg str name: The name of the log. If None, use the name of the currently-selected log.
+        :returns: The index of the named log in the list of Log objects.
+        :rtype: int
+        """
+        if(name is None):
+            # If no page name is supplied, then just use the currently selected page
+            page_index = self.get_current_page()  # Gets the index of the selected tab in the logbook
+            if(page_index == 0 or page_index == self.get_n_pages()-1):
+                # We either have the Summary page, or the "+" (add log) dummy page.
+                logging.debug("No log currently selected!")
+                return None
+            name = self.get_nth_page(page_index).get_name()
+        # If a page of the logbook (and therefore a Log object) gets deleted,
+        # then the page_index may not correspond to the index of the log in the self.logs list.
+        # Therefore, we have to search for the tab with the same name as the log.
+        for i in range(0, len(self.logs)):
+            if(self.logs[i].name == name):
+                log_index = i
+                break
+        return log_index
+
 
 class TestLogbook(unittest.TestCase):
-   """ The unit tests for the Logbook class. """
-
-   def setUp(self):
-      """ Set up the Logbook object and connection to the test database needed for the unit tests. """
-      import os
-      self.logbook = Logbook(parent=None)
-      success = self.logbook.db_connect(os.path.dirname(os.path.realpath(__file__))+"/unittest_resources/test.db")
-      assert success
-      
-   def tearDown(self):
-      """ Disconnect from the test database. """
-      success = self.logbook.db_disconnect()
-      assert success
-
-   def test_log_name_exists(self):
-      """ Check that only the log called 'test' exists. """
-      assert self.logbook.log_name_exists("test") # Log 'test' exists.
-      assert not self.logbook.log_name_exists("hello") # Log 'hello' should not exist.
-      
+
+    """ The unit tests for the Logbook class. """
+
+    def setUp(self):
+        """ Set up the Logbook object and connection to the test database needed for the unit tests. """
+        import os
+        self.logbook = Logbook(parent=None)
+        success = self.logbook.db_connect(os.path.dirname(os.path.realpath(__file__))+"/unittest_resources/test.db")
+        assert success
+
+    def tearDown(self):
+        """ Disconnect from the test database. """
+        success = self.logbook.db_disconnect()
+        assert success
+
+    def test_log_name_exists(self):
+        """ Check that only the log called 'test' exists. """
+        assert self.logbook.log_name_exists("test")  # Log 'test' exists.
+        assert not self.logbook.log_name_exists("hello")  # Log 'hello' should not exist.
+
 if(__name__ == '__main__'):
-   unittest.main()
+    unittest.main()
diff --git a/pyqso/menu.py b/pyqso/menu.py
index 5ba6c51..4c8b078 100644
--- a/pyqso/menu.py
+++ b/pyqso/menu.py
@@ -1,8 +1,8 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2012 Christian T. Jacobs.
+#    Copyright (C) 2012-2016 Christian T. Jacobs.
 
-#    This logbook is part of PyQSO.
+#    This file is part of PyQSO.
 
 #    PyQSO is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
@@ -17,253 +17,269 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject
+from gi.repository import Gtk
 import logging
-import ConfigParser
+import configparser
 import os.path
 
+
 class Menu(Gtk.MenuBar):
-   
-   def __init__(self, parent):
-      logging.debug("New Menu instance created!")
-      
-      # First let's call the constructor of the super class (Gtk.MenuBar)
-      Gtk.MenuBar.__init__(self)
-
-      logging.debug("Setting up the menu bar...")      
-      agrp = Gtk.AccelGroup()
-      parent.add_accel_group(agrp)
-
-      self.items = {}
-      
-      ###### LOGBOOK ######
-      mitem_logbook = Gtk.MenuItem("Logbook")
-      self.append(mitem_logbook)  
-      subm_logbook = Gtk.Menu()
-      mitem_logbook.set_submenu(subm_logbook)
-
-      # Create logbook
-      mitem_connect = Gtk.ImageMenuItem("Create a New Logbook...")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_NEW, Gtk.IconSize.MENU)
-      mitem_connect.set_image(icon)
-      mitem_connect.connect("activate", parent.logbook.new)
-      subm_logbook.append(mitem_connect)
-      self.items["NEW_LOGBOOK"] = mitem_connect
-          
-      # Open logbook
-      mitem_connect = Gtk.ImageMenuItem("Open an Existing Logbook...")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.MENU)
-      mitem_connect.set_image(icon)
-      mitem_connect.connect("activate", parent.logbook.open)
-      key, mod = Gtk.accelerator_parse("<Control>O")
-      mitem_connect.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
-      subm_logbook.append(mitem_connect)
-      self.items["OPEN_LOGBOOK"] = mitem_connect
-
-      # Close logbook
-      mitem_disconnect = Gtk.ImageMenuItem("Close Logbook")
-      mitem_disconnect.connect("activate", parent.logbook.close)
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU)
-      mitem_disconnect.set_image(icon)
-      key, mod = Gtk.accelerator_parse("<Control>W")
-      mitem_disconnect.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
-      subm_logbook.append(mitem_disconnect)
-      self.items["CLOSE_LOGBOOK"] = mitem_disconnect
-
-      subm_logbook.append(Gtk.SeparatorMenuItem())
-
-      # New log
-      mitem_new = Gtk.ImageMenuItem("New Log")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU)
-      mitem_new.set_image(icon)
-      mitem_new.connect("activate", parent.logbook.new_log)
-      key, mod = Gtk.accelerator_parse("<Control>N")
-      mitem_new.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
-      subm_logbook.append(mitem_new)
-      self.items["NEW_LOG"] = mitem_new
-
-      # Delete the current log
-      mitem_delete = Gtk.ImageMenuItem("Delete Selected Log")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.MENU)
-      mitem_delete.set_image(icon)
-      mitem_delete.connect("activate", parent.logbook.delete_log)
-      subm_logbook.append(mitem_delete)
-      self.items["DELETE_LOG"] = mitem_delete
-      
-      # Rename the current log
-      mitem_rename = Gtk.ImageMenuItem("Rename Selected Log")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.MENU)
-      mitem_rename.set_image(icon)
-      mitem_rename.connect("activate", parent.logbook.rename_log)
-      subm_logbook.append(mitem_rename)
-      self.items["RENAME_LOG"] = mitem_rename
-
-      subm_logbook.append(Gtk.SeparatorMenuItem())
-
-      # Import log
-      mitem_import = Gtk.ImageMenuItem("Import Log")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_GO_FORWARD, Gtk.IconSize.MENU)
-      mitem_import.set_image(icon)
-      mitem_import.connect("activate", parent.logbook.import_log)
-      subm_logbook.append(mitem_import)
-      self.items["IMPORT_LOG"] = mitem_import
-
-      # Export the current log
-      mitem_export = Gtk.ImageMenuItem("Export Log")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_GO_BACK, Gtk.IconSize.MENU)
-      mitem_export.set_image(icon)
-      mitem_export.connect("activate", parent.logbook.export_log)
-      subm_logbook.append(mitem_export)
-      self.items["EXPORT_LOG"] = mitem_export
- 
-      subm_logbook.append(Gtk.SeparatorMenuItem())
-
-      # Print log
-      mitem_print = Gtk.ImageMenuItem("Print Log")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_PRINT, Gtk.IconSize.MENU)
-      mitem_print.set_image(icon)
-      mitem_print.connect("activate", parent.logbook.print_log)
-      key, mod = Gtk.accelerator_parse("<Control>P")
-      mitem_print.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
-      subm_logbook.append(mitem_print)
-      self.items["PRINT_LOG"] = mitem_print
-
-      subm_logbook.append(Gtk.SeparatorMenuItem())
-
-      # Quit
-      mitem_quit = Gtk.ImageMenuItem("Quit")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_QUIT, Gtk.IconSize.MENU)
-      mitem_quit.set_image(icon)
-      mitem_quit.connect("activate", Gtk.main_quit)
-      key, mod = Gtk.accelerator_parse("<Control>Q")
-      mitem_quit.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
-      subm_logbook.append(mitem_quit)
-      self.items["QUIT"] = mitem_quit
-      
-      
-      ###### RECORDS ######
-      mitem_records = Gtk.MenuItem("Records")
-      self.append(mitem_records)  
-      subm_records = Gtk.Menu()
-      mitem_records.set_submenu(subm_records)
-      
-      mitem_addrecord = Gtk.ImageMenuItem("Add Record...")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU)
-      mitem_addrecord.set_image(icon)
-      mitem_addrecord.connect("activate", parent.logbook.add_record_callback)
-      key, mod = Gtk.accelerator_parse("<Control>R")
-      mitem_addrecord.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
-      subm_records.append(mitem_addrecord)
-      self.items["ADD_RECORD"] = mitem_addrecord
-      
-      mitem_editrecord = Gtk.ImageMenuItem("Edit Selected Record...")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.MENU)
-      mitem_editrecord.set_image(icon)
-      mitem_editrecord.connect("activate", parent.logbook.edit_record_callback, None, None)
-      key, mod = Gtk.accelerator_parse("<Control>E")
-      mitem_editrecord.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
-      subm_records.append(mitem_editrecord)
-      self.items["EDIT_RECORD"] = mitem_editrecord
-
-      mitem_deleterecord = Gtk.ImageMenuItem("Delete Selected Record...")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.MENU)
-      mitem_deleterecord.set_image(icon)
-      mitem_deleterecord.connect("activate", parent.logbook.delete_record_callback)
-      key, mod = Gtk.accelerator_parse("Delete")
-      mitem_deleterecord.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
-      subm_records.append(mitem_deleterecord)
-      self.items["DELETE_RECORD"] = mitem_deleterecord
-
-      mitem_removeduplicates = Gtk.ImageMenuItem("Remove Duplicate Records")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_FIND_AND_REPLACE, Gtk.IconSize.MENU)
-      mitem_removeduplicates.set_image(icon)
-      mitem_removeduplicates.connect("activate", parent.logbook.remove_duplicates_callback)
-      subm_records.append(mitem_removeduplicates)
-      self.items["REMOVE_DUPLICATES"] = mitem_removeduplicates
-      
-      
-      ###### VIEW ######
-      mitem_view = Gtk.MenuItem("View")
-      self.append(mitem_view)  
-      subm_view = Gtk.Menu()
-      mitem_view.set_submenu(subm_view)
-
-      mitem_toolbox = Gtk.CheckMenuItem("Toolbox")
-      config = ConfigParser.ConfigParser()
-      have_config = (config.read(os.path.expanduser('~/.pyqso.ini')) != [])
-      (section, option) = ("general", "show_toolbox")
-      if(have_config and config.has_option(section, option)):
-         mitem_toolbox.set_active(config.get(section, option) == "True")
-      else:
-         mitem_toolbox.set_active(False) # Don't show the toolbox by default
-      mitem_toolbox.connect("activate", parent.toolbox.toggle_visible_callback)
-      subm_view.append(mitem_toolbox)
-      self.items["TOOLBOX"] = mitem_toolbox
-
-      mitem_preferences = Gtk.ImageMenuItem("Preferences...")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_PREFERENCES, Gtk.IconSize.MENU)
-      mitem_preferences.set_image(icon)
-      mitem_preferences.connect("activate", parent.show_preferences)
-      subm_view.append(mitem_preferences)
-      self.items["PREFERENCES"] = mitem_preferences
-            
-
-      ###### HELP ######
-      mitem_help = Gtk.MenuItem("Help")
-      self.append(mitem_help)  
-      subm_help = Gtk.Menu()
-      mitem_help.set_submenu(subm_help)
-      
-      # About
-      mitem_about = Gtk.ImageMenuItem("About PyQSO")
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_ABOUT, Gtk.IconSize.MENU)
-      mitem_about.set_image(icon)
-      mitem_about.connect("activate", parent.show_about)
-      subm_help.append(mitem_about)
-
-      self.set_logbook_item_sensitive(True)
-      self.set_log_items_sensitive(False)
-      self.set_record_items_sensitive(False)
-      
-      logging.debug("Menu bar ready!") 
-
-      return
-      
-   def set_logbook_item_sensitive(self, sensitive):
-      logging.debug("Setting the 'Create/Open Logbook' menu item's sensitivity to: %s..." % sensitive) 
-      self.items["NEW_LOGBOOK"].set_sensitive(sensitive)
-      self.items["OPEN_LOGBOOK"].set_sensitive(sensitive)
-      self.items["CLOSE_LOGBOOK"].set_sensitive(not sensitive)
-      logging.debug("Set the 'Create/Open Logbook' menu item's sensitivity to: %s." % sensitive) 
-      return
-
-   def set_log_items_sensitive(self, sensitive):
-      logging.debug("Setting log-related menu item sensitivity to: %s..." % sensitive) 
-      for item_name in ["NEW_LOG", "DELETE_LOG", "RENAME_LOG", "IMPORT_LOG", "EXPORT_LOG", "PRINT_LOG"]:
-         self.items[item_name].set_sensitive(sensitive)
-      logging.debug("Set log-related menu item sensitivity to: %s." % sensitive) 
-      return
-
-   def set_record_items_sensitive(self, sensitive):
-      logging.debug("Setting record-related menu item sensitivity to: %s..." % sensitive) 
-      for item_name in ["ADD_RECORD", "EDIT_RECORD", "DELETE_RECORD", "REMOVE_DUPLICATES"]:
-         self.items[item_name].set_sensitive(sensitive)
-      logging.debug("Set record-related menu item sensitivity to: %s." % sensitive) 
-      return
 
+    """ The PyQSO menu bar along the top of the main window. """
+
+    def __init__(self, parent):
+        """ Set up all menu items and connect to the various functions.
+
+        :arg parent: The parent Gtk window.
+        """
+
+        logging.debug("New Menu instance created!")
+
+        # First let's call the constructor of the super class (Gtk.MenuBar)
+        Gtk.MenuBar.__init__(self)
+
+        logging.debug("Setting up the menu bar...")
+        agrp = Gtk.AccelGroup()
+        parent.add_accel_group(agrp)
+
+        self.items = {}
+
+        # LOGBOOK ######
+        mitem_logbook = Gtk.MenuItem("Logbook")
+        self.append(mitem_logbook)
+        subm_logbook = Gtk.Menu()
+        mitem_logbook.set_submenu(subm_logbook)
+
+        # Create logbook
+        mitem_connect = Gtk.ImageMenuItem("Create a New Logbook...")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_NEW, Gtk.IconSize.MENU)
+        mitem_connect.set_image(icon)
+        mitem_connect.connect("activate", parent.logbook.new)
+        subm_logbook.append(mitem_connect)
+        self.items["NEW_LOGBOOK"] = mitem_connect
+
+        # Open logbook
+        mitem_connect = Gtk.ImageMenuItem("Open an Existing Logbook...")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.MENU)
+        mitem_connect.set_image(icon)
+        mitem_connect.connect("activate", parent.logbook.open)
+        key, mod = Gtk.accelerator_parse("<Control>O")
+        mitem_connect.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
+        subm_logbook.append(mitem_connect)
+        self.items["OPEN_LOGBOOK"] = mitem_connect
+
+        # Close logbook
+        mitem_disconnect = Gtk.ImageMenuItem("Close Logbook")
+        mitem_disconnect.connect("activate", parent.logbook.close)
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU)
+        mitem_disconnect.set_image(icon)
+        key, mod = Gtk.accelerator_parse("<Control>W")
+        mitem_disconnect.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
+        subm_logbook.append(mitem_disconnect)
+        self.items["CLOSE_LOGBOOK"] = mitem_disconnect
+
+        subm_logbook.append(Gtk.SeparatorMenuItem())
+
+        # New log
+        mitem_new = Gtk.ImageMenuItem("New Log")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU)
+        mitem_new.set_image(icon)
+        mitem_new.connect("activate", parent.logbook.new_log)
+        key, mod = Gtk.accelerator_parse("<Control>N")
+        mitem_new.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
+        subm_logbook.append(mitem_new)
+        self.items["NEW_LOG"] = mitem_new
+
+        # Delete the current log
+        mitem_delete = Gtk.ImageMenuItem("Delete Selected Log")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.MENU)
+        mitem_delete.set_image(icon)
+        mitem_delete.connect("activate", parent.logbook.delete_log)
+        subm_logbook.append(mitem_delete)
+        self.items["DELETE_LOG"] = mitem_delete
+
+        # Rename the current log
+        mitem_rename = Gtk.ImageMenuItem("Rename Selected Log")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.MENU)
+        mitem_rename.set_image(icon)
+        mitem_rename.connect("activate", parent.logbook.rename_log)
+        subm_logbook.append(mitem_rename)
+        self.items["RENAME_LOG"] = mitem_rename
+
+        subm_logbook.append(Gtk.SeparatorMenuItem())
+
+        # Import log
+        mitem_import = Gtk.ImageMenuItem("Import Log")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_GO_FORWARD, Gtk.IconSize.MENU)
+        mitem_import.set_image(icon)
+        mitem_import.connect("activate", parent.logbook.import_log)
+        subm_logbook.append(mitem_import)
+        self.items["IMPORT_LOG"] = mitem_import
+
+        # Export the current log
+        mitem_export = Gtk.ImageMenuItem("Export Log")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_GO_BACK, Gtk.IconSize.MENU)
+        mitem_export.set_image(icon)
+        mitem_export.connect("activate", parent.logbook.export_log)
+        subm_logbook.append(mitem_export)
+        self.items["EXPORT_LOG"] = mitem_export
+
+        subm_logbook.append(Gtk.SeparatorMenuItem())
+
+        # Print log
+        mitem_print = Gtk.ImageMenuItem("Print Log")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_PRINT, Gtk.IconSize.MENU)
+        mitem_print.set_image(icon)
+        mitem_print.connect("activate", parent.logbook.print_log)
+        key, mod = Gtk.accelerator_parse("<Control>P")
+        mitem_print.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
+        subm_logbook.append(mitem_print)
+        self.items["PRINT_LOG"] = mitem_print
+
+        subm_logbook.append(Gtk.SeparatorMenuItem())
+
+        # Quit
+        mitem_quit = Gtk.ImageMenuItem("Quit")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_QUIT, Gtk.IconSize.MENU)
+        mitem_quit.set_image(icon)
+        mitem_quit.connect("activate", Gtk.main_quit)
+        key, mod = Gtk.accelerator_parse("<Control>Q")
+        mitem_quit.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
+        subm_logbook.append(mitem_quit)
+        self.items["QUIT"] = mitem_quit
+
+        # RECORDS ######
+        mitem_records = Gtk.MenuItem("Records")
+        self.append(mitem_records)
+        subm_records = Gtk.Menu()
+        mitem_records.set_submenu(subm_records)
+
+        mitem_addrecord = Gtk.ImageMenuItem("Add Record...")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU)
+        mitem_addrecord.set_image(icon)
+        mitem_addrecord.connect("activate", parent.logbook.add_record_callback)
+        key, mod = Gtk.accelerator_parse("<Control>R")
+        mitem_addrecord.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
+        subm_records.append(mitem_addrecord)
+        self.items["ADD_RECORD"] = mitem_addrecord
+
+        mitem_editrecord = Gtk.ImageMenuItem("Edit Selected Record...")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.MENU)
+        mitem_editrecord.set_image(icon)
+        mitem_editrecord.connect("activate", parent.logbook.edit_record_callback, None, None)
+        key, mod = Gtk.accelerator_parse("<Control>E")
+        mitem_editrecord.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
+        subm_records.append(mitem_editrecord)
+        self.items["EDIT_RECORD"] = mitem_editrecord
+
+        mitem_deleterecord = Gtk.ImageMenuItem("Delete Selected Record...")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.MENU)
+        mitem_deleterecord.set_image(icon)
+        mitem_deleterecord.connect("activate", parent.logbook.delete_record_callback)
+        key, mod = Gtk.accelerator_parse("Delete")
+        mitem_deleterecord.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
+        subm_records.append(mitem_deleterecord)
+        self.items["DELETE_RECORD"] = mitem_deleterecord
+
+        mitem_removeduplicates = Gtk.ImageMenuItem("Remove Duplicate Records")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_FIND_AND_REPLACE, Gtk.IconSize.MENU)
+        mitem_removeduplicates.set_image(icon)
+        mitem_removeduplicates.connect("activate", parent.logbook.remove_duplicates_callback)
+        subm_records.append(mitem_removeduplicates)
+        self.items["REMOVE_DUPLICATES"] = mitem_removeduplicates
+
+        # VIEW ######
+        mitem_view = Gtk.MenuItem("View")
+        self.append(mitem_view)
+        subm_view = Gtk.Menu()
+        mitem_view.set_submenu(subm_view)
+
+        mitem_toolbox = Gtk.CheckMenuItem("Toolbox")
+        config = configparser.ConfigParser()
+        have_config = (config.read(os.path.expanduser('~/.config/pyqso/preferences.ini')) != [])
+        (section, option) = ("general", "show_toolbox")
+        if(have_config and config.has_option(section, option)):
+            mitem_toolbox.set_active(config.get(section, option) == "True")
+        else:
+            mitem_toolbox.set_active(False)  # Don't show the toolbox by default
+        mitem_toolbox.connect("activate", parent.toolbox.toggle_visible_callback)
+        subm_view.append(mitem_toolbox)
+        self.items["TOOLBOX"] = mitem_toolbox
+
+        mitem_preferences = Gtk.ImageMenuItem("Preferences...")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_PREFERENCES, Gtk.IconSize.MENU)
+        mitem_preferences.set_image(icon)
+        mitem_preferences.connect("activate", parent.show_preferences)
+        subm_view.append(mitem_preferences)
+        self.items["PREFERENCES"] = mitem_preferences
+
+        # HELP ######
+        mitem_help = Gtk.MenuItem("Help")
+        self.append(mitem_help)
+        subm_help = Gtk.Menu()
+        mitem_help.set_submenu(subm_help)
+
+        # About
+        mitem_about = Gtk.ImageMenuItem("About PyQSO")
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_ABOUT, Gtk.IconSize.MENU)
+        mitem_about.set_image(icon)
+        mitem_about.connect("activate", parent.show_about)
+        subm_help.append(mitem_about)
+
+        self.set_logbook_item_sensitive(True)
+        self.set_log_items_sensitive(False)
+        self.set_record_items_sensitive(False)
+
+        logging.debug("Menu bar ready!")
+
+        return
+
+    def set_logbook_item_sensitive(self, sensitive):
+        """ Enable/disable logbook-related menu items.
+
+        :arg bool sensitive: If True, enable the 'new logbook' and 'open logbook' menu items. If False, disable them.
+        """
+        logging.debug("Setting the 'Create/Open Logbook' menu item's sensitivity to: %s..." % sensitive)
+        self.items["NEW_LOGBOOK"].set_sensitive(sensitive)
+        self.items["OPEN_LOGBOOK"].set_sensitive(sensitive)
+        self.items["CLOSE_LOGBOOK"].set_sensitive(not sensitive)
+        logging.debug("Set the 'Create/Open Logbook' menu item's sensitivity to: %s." % sensitive)
+        return
+
+    def set_log_items_sensitive(self, sensitive):
+        """ Enable/disable log-related menu items.
+
+        :arg bool sensitive: If True, enable all the log-related menu items. If False, disable them all.
+        """
+        logging.debug("Setting log-related menu item sensitivity to: %s..." % sensitive)
+        for item_name in ["NEW_LOG", "DELETE_LOG", "RENAME_LOG", "IMPORT_LOG", "EXPORT_LOG", "PRINT_LOG"]:
+            self.items[item_name].set_sensitive(sensitive)
+        logging.debug("Set log-related menu item sensitivity to: %s." % sensitive)
+        return
+
+    def set_record_items_sensitive(self, sensitive):
+        """ Enable/disable record-related menu items.
+
+        :arg bool sensitive: If True, enable all the record-related menu items. If False, disable them all.
+        """
+        logging.debug("Setting record-related menu item sensitivity to: %s..." % sensitive)
+        for item_name in ["ADD_RECORD", "EDIT_RECORD", "DELETE_RECORD", "REMOVE_DUPLICATES"]:
+            self.items[item_name].set_sensitive(sensitive)
+        logging.debug("Set record-related menu item sensitivity to: %s." % sensitive)
+        return
diff --git a/pyqso/preferences_dialog.py b/pyqso/preferences_dialog.py
index 8a38cdf..60c4b13 100644
--- a/pyqso/preferences_dialog.py
+++ b/pyqso/preferences_dialog.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,398 +17,557 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject
+from gi.repository import Gtk
 import logging
-import ConfigParser
+import configparser
 import os.path
 import base64
-from math import ceil
 try:
-   import Hamlib
-   have_hamlib = True
+    import Hamlib
+    have_hamlib = True
 except ImportError:
-   logging.error("Could not import the Hamlib module!")
-   have_hamlib = False
+    logging.warning("Could not import the Hamlib module!")
+    have_hamlib = False
+
+from pyqso.adif import *
+
+PREFERENCES_FILE = os.path.expanduser("~/.config/pyqso/preferences.ini")
 
-from pyqso.adif import AVAILABLE_FIELD_NAMES_FRIENDLY, AVAILABLE_FIELD_NAMES_ORDERED, MODES
 
 class PreferencesDialog(Gtk.Dialog):
-   
-   def __init__(self, parent):
-      logging.debug("Setting up the preferences dialog...")
 
-      Gtk.Dialog.__init__(self, title="Preferences", parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
+    """ A dialog to specify the PyQSO preferences. """
+
+    def __init__(self, parent):
+        """ Set up the various pages of the preferences dialog. """
+
+        logging.debug("Setting up the preferences dialog...")
+
+        Gtk.Dialog.__init__(self, title="Preferences", parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
+
+        self.preferences = Gtk.Notebook()
+
+        self.general = GeneralPage()
+        self.preferences.insert_page(self.general, Gtk.Label("General"), 0)
+
+        self.view = ViewPage()
+        self.preferences.insert_page(self.view, Gtk.Label("View"), 1)
 
-      self.preferences = Gtk.Notebook()
+        self.hamlib = HamlibPage()
+        self.preferences.insert_page(self.hamlib, Gtk.Label("Hamlib"), 2)
 
-      self.general = GeneralPage()
-      self.preferences.insert_page(self.general, Gtk.Label("General"), 0)
+        self.records = RecordsPage()
+        self.preferences.insert_page(self.records, Gtk.Label("Records"), 2)
 
-      self.view = ViewPage()
-      self.preferences.insert_page(self.view, Gtk.Label("View"), 1)
+        self.adif = ADIFPage()
+        self.preferences.insert_page(self.adif, Gtk.Label("ADIF"), 2)
 
-      self.hamlib = HamlibPage()
-      self.preferences.insert_page(self.hamlib, Gtk.Label("Hamlib"), 2)
+        self.vbox.pack_start(self.preferences, True, True, 2)
+        self.show_all()
 
-      self.records = RecordsPage()
-      self.preferences.insert_page(self.records, Gtk.Label("Records"), 2)
+        logging.debug("Preferences dialog ready!")
 
-      self.vbox.pack_start(self.preferences, True, True, 2)
-      self.show_all()
+        return
 
-      logging.debug("Preferences dialog ready!")
+    def commit(self):
+        """ Commit the user preferences to the configuration file. """
 
-      return
+        logging.debug("Committing the user preferences to the configuration file...")
+        general_data = self.general.get_data()
+        view_data = self.view.get_data()
+        hamlib_data = self.hamlib.get_data()
+        records_data = self.records.get_data()
+        adif_data = self.adif.get_data()
 
-   def commit(self):
-      """ Commit the user preferences to the configuration file. """
-      logging.debug("Committing the user preferences to the configuration file...")
-      general_data = self.general.get_data()
-      view_data = self.view.get_data()
-      hamlib_data = self.hamlib.get_data()
-      records_data = self.records.get_data()
+        config = configparser.ConfigParser()
 
-      config = ConfigParser.ConfigParser()
+        # General
+        config.add_section("general")
+        for key in list(general_data.keys()):
+            config.set("general", key.lower(), str(general_data[key]))
 
-      # General
-      config.add_section("general")
-      for key in general_data.keys():
-         config.set("general", key.lower(), general_data[key])
+        # View
+        config.add_section("view")
+        for key in list(view_data.keys()):
+            config.set("view", key.lower(), str(view_data[key]))
 
-      # View
-      config.add_section("view")
-      for key in view_data.keys():
-         config.set("view", key.lower(), view_data[key])
+        # ADIF
+        config.add_section("adif")
+        for key in list(adif_data.keys()):
+            config.set("adif", key.lower(), str(adif_data[key]))
 
-      # Hamlib
-      config.add_section("hamlib")
-      for key in hamlib_data.keys():
-         config.set("hamlib", key.lower(), hamlib_data[key])
-      
-      # Records
-      config.add_section("records")
-      for key in records_data.keys():
-         config.set("records", key.lower(), records_data[key])
+        # Hamlib
+        config.add_section("hamlib")
+        for key in list(hamlib_data.keys()):
+            config.set("hamlib", key.lower(), str(hamlib_data[key]))
 
-      with open(os.path.expanduser('~/.pyqso.ini'), 'w') as f:
-         config.write(f)
+        # Records
+        config.add_section("records")
+        for key in list(records_data.keys()):
+            config.set("records", key.lower(), str(records_data[key]))
+
+        # Write the preferences to file.
+        with open(os.path.expanduser(PREFERENCES_FILE), 'w') as f:
+            config.write(f)
+
+        return
 
-      return
 
 class GeneralPage(Gtk.VBox):
-   
-   def __init__(self):
-      logging.debug("Setting up the General page of the preferences dialog...")
-
-      Gtk.VBox.__init__(self, spacing=2)
-
-      # Remember that the have_config conditional in the PyQSO class may be out-of-date the next time the user opens up the preferences dialog
-      # because a configuration file may have been created after launching the application. Let's check to see if one exists again...
-      config = ConfigParser.ConfigParser()
-      have_config = (config.read(os.path.expanduser('~/.pyqso.ini')) != [])
-
-      self.sources = {}
-
-      frame = Gtk.Frame()
-      frame.set_label("Startup")
-      hbox = Gtk.HBox()
-      self.sources["SHOW_TOOLBOX"] = Gtk.CheckButton("Show toolbox by default")
-      (section, option) = ("general", "show_toolbox")
-      if(have_config and config.has_option(section, option)):
-         self.sources["SHOW_TOOLBOX"].set_active(config.get(section, option) == "True")
-      else:
-         self.sources["SHOW_TOOLBOX"].set_active(False)
-      hbox.pack_start(self.sources["SHOW_TOOLBOX"], False, False, 2)
-      frame.add(hbox)
-      self.pack_start(frame, False, False, 2)
-
-      logging.debug("General page of the preferences dialog ready!")
-      return
-
-   def get_data(self):
-      logging.debug("Retrieving data from the General page of the preferences dialog...")
-      data = {}
-      data["SHOW_TOOLBOX"] = self.sources["SHOW_TOOLBOX"].get_active()
-      return data
+
+    """ The section of the preferences dialog containing general preferences. """
+
+    def __init__(self):
+        logging.debug("Setting up the General page of the preferences dialog...")
+
+        Gtk.VBox.__init__(self, spacing=2)
+
+        # Remember that the have_config conditional in the PyQSO class may be out-of-date the next time the user opens up the preferences dialog
+        # because a configuration file may have been created after launching the application. Let's check to see if one exists again...
+        config = configparser.ConfigParser()
+        have_config = (config.read(PREFERENCES_FILE) != [])
+
+        self.sources = {}
+
+        # Startup
+        frame = Gtk.Frame()
+        frame.set_label("Startup")
+
+        vbox = Gtk.VBox()
+
+        # Show toolbox
+        hbox = Gtk.HBox()
+        self.sources["SHOW_TOOLBOX"] = Gtk.CheckButton("Show toolbox by default")
+        (section, option) = ("general", "show_toolbox")
+        if(have_config and config.has_option(section, option)):
+            self.sources["SHOW_TOOLBOX"].set_active(config.get(section, option) == "True")
+        else:
+            self.sources["SHOW_TOOLBOX"].set_active(False)
+        hbox.pack_start(self.sources["SHOW_TOOLBOX"], False, False, 2)
+        vbox.pack_start(hbox, False, False, 2)
+
+        # Show statistics
+        hbox = Gtk.HBox()
+        self.sources["SHOW_YEARLY_STATISTICS"] = Gtk.CheckButton("Show yearly logbook statistics on the Summary page")
+        (section, option) = ("general", "show_yearly_statistics")
+        if(have_config and config.has_option(section, option)):
+            self.sources["SHOW_YEARLY_STATISTICS"].set_active(config.get(section, option) == "True")
+        else:
+            self.sources["SHOW_YEARLY_STATISTICS"].set_active(False)
+        hbox.pack_start(self.sources["SHOW_YEARLY_STATISTICS"], False, False, 2)
+        vbox.pack_start(hbox, False, False, 2)
+
+        frame.add(vbox)
+        self.pack_start(frame, False, False, 2)
+
+        # Dialogs
+        frame = Gtk.Frame()
+        frame.set_label("Dialogs")
+
+        vbox = Gtk.VBox()
+
+        # Keep 'Add Record' dialog open
+        hbox = Gtk.HBox()
+        self.sources["KEEP_OPEN"] = Gtk.CheckButton("Keep the Add Record dialog open after a QSO is added")
+        (section, option) = ("general", "keep_open")
+        if(have_config and config.has_option(section, option)):
+            self.sources["KEEP_OPEN"].set_active(config.get(section, option) == "True")
+        else:
+            self.sources["KEEP_OPEN"].set_active(False)
+        hbox.pack_start(self.sources["KEEP_OPEN"], False, False, 2)
+        vbox.pack_start(hbox, False, False, 2)
+
+        frame.add(vbox)
+        self.pack_start(frame, False, False, 2)
+
+        logging.debug("General page of the preferences dialog ready!")
+        return
+
+    def get_data(self):
+        logging.debug("Retrieving data from the General page of the preferences dialog...")
+        data = {}
+        data["SHOW_TOOLBOX"] = self.sources["SHOW_TOOLBOX"].get_active()
+        data["SHOW_YEARLY_STATISTICS"] = self.sources["SHOW_YEARLY_STATISTICS"].get_active()
+        data["KEEP_OPEN"] = self.sources["KEEP_OPEN"].get_active()
+        return data
+
 
 class ViewPage(Gtk.VBox):
-   
-   def __init__(self):
-      logging.debug("Setting up the View page of the preferences dialog...")
-
-      Gtk.VBox.__init__(self, spacing=2)
-
-      config = ConfigParser.ConfigParser()
-      have_config = (config.read(os.path.expanduser('~/.pyqso.ini')) != [])
-
-      self.sources = {}
-
-      # Visible fields frame
-      frame = Gtk.Frame()
-      frame.set_label("Visible fields")
-
-      # Divide the list of available field names up into multiple columns (of maximum length 'max_buttons_per_column')
-      # so we don't make the Preferences dialog too long.      
-      hbox = Gtk.HBox(spacing=2)
-      max_buttons_per_column = 6
-      number_of_columns = int( len(AVAILABLE_FIELD_NAMES_ORDERED)/max_buttons_per_column ) + 1 # Number of check buttons per column
-      for i in range(0, number_of_columns):
-         vbox = Gtk.VBox(spacing=2)
-         for j in range(0, max_buttons_per_column):
-            if(i*max_buttons_per_column + j >= len(AVAILABLE_FIELD_NAMES_ORDERED)):
-               break
-            field_name = AVAILABLE_FIELD_NAMES_ORDERED[i*max_buttons_per_column + j]
-            button = Gtk.CheckButton(AVAILABLE_FIELD_NAMES_FRIENDLY[field_name ])
-            if(have_config and config.has_option("view", field_name.lower())):
-               button.set_active(config.get("view", field_name.lower()) == "True")
-            else:
-               button.set_active(True)
-            self.sources[field_name] = button
-            vbox.pack_start(button, False, False, 2)
-         hbox.pack_start(vbox, False, False, 2)
-      frame.add(hbox)
-      self.pack_start(frame, False, False, 2)
-
-      self.label = Gtk.Label("Note: View-related changes will not take effect\nuntil PyQSO is restarted.")
-      self.pack_start(self.label, False, False, 2)
-
-      logging.debug("View page of the preferences dialog ready!")
-      return
-
-   def get_data(self):
-      logging.debug("Retrieving data from the View page of the preferences dialog...")
-      data = {}
-      for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
-         data[field_name] = self.sources[field_name].get_active()
-      return data
+
+    """ The section of the preferences dialog containing view-related preferences. """
+
+    def __init__(self):
+        logging.debug("Setting up the View page of the preferences dialog...")
+
+        Gtk.VBox.__init__(self, spacing=2)
+
+        config = configparser.ConfigParser()
+        have_config = (config.read(PREFERENCES_FILE) != [])
+
+        self.sources = {}
+
+        # Visible fields frame
+        frame = Gtk.Frame()
+        frame.set_label("Visible fields")
+
+        # Divide the list of available field names up into multiple columns (of maximum length 'max_buttons_per_column')
+        # so we don't make the Preferences dialog too long.
+        hbox = Gtk.HBox(spacing=2)
+        max_buttons_per_column = 6
+        number_of_columns = int(len(AVAILABLE_FIELD_NAMES_ORDERED)/max_buttons_per_column) + 1  # Number of check buttons per column
+        for i in range(0, number_of_columns):
+            vbox = Gtk.VBox(spacing=2)
+            for j in range(0, max_buttons_per_column):
+                if(i*max_buttons_per_column + j >= len(AVAILABLE_FIELD_NAMES_ORDERED)):
+                    break
+                field_name = AVAILABLE_FIELD_NAMES_ORDERED[i*max_buttons_per_column + j]
+                button = Gtk.CheckButton(AVAILABLE_FIELD_NAMES_FRIENDLY[field_name])
+                if(have_config and config.has_option("view", field_name.lower())):
+                    button.set_active(config.get("view", field_name.lower()) == "True")
+                else:
+                    button.set_active(True)
+                self.sources[field_name] = button
+                vbox.pack_start(button, False, False, 2)
+            hbox.pack_start(vbox, False, False, 2)
+        frame.add(hbox)
+        self.pack_start(frame, False, False, 2)
+
+        self.label = Gtk.Label("Note: View-related changes will not take effect\nuntil PyQSO is restarted.")
+        self.pack_start(self.label, False, False, 2)
+
+        logging.debug("View page of the preferences dialog ready!")
+        return
+
+    def get_data(self):
+        logging.debug("Retrieving data from the View page of the preferences dialog...")
+        data = {}
+        for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
+            data[field_name] = self.sources[field_name].get_active()
+        return data
+
 
 class HamlibPage(Gtk.VBox):
-   
-   def __init__(self):
-      logging.debug("Setting up the Hamlib page of the preferences dialog...")
-
-      Gtk.VBox.__init__(self, spacing=2)
-
-      config = ConfigParser.ConfigParser()
-      have_config = (config.read(os.path.expanduser('~/.pyqso.ini')) != [])
-
-      self.sources = {}
-
-      frame = Gtk.Frame()
-      frame.set_label("Hamlib support")
-
-      vbox_inner = Gtk.VBox(spacing=2)
-
-      self.sources["AUTOFILL"] = Gtk.CheckButton("Auto-fill Frequency field")
-      (section, option) = ("hamlib", "autofill")
-      if(have_config and config.has_option(section, option)):
-         self.sources["AUTOFILL"].set_active(config.get(section, option) == "True")
-      else:
-         self.sources["AUTOFILL"].set_active(False)
-      vbox_inner.pack_start(self.sources["AUTOFILL"], False, False, 2)
-
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label("Model: ")
-      label.set_alignment(0, 0.5)
-      label.set_width_chars(17)
-      hbox_temp.pack_start(label, False, False, 2)
-
-      # Get the list of rig models
-      models = ["RIG_MODEL_NONE"]
-      if(have_hamlib):
-         try:
-            for item in dir(Hamlib):
-               if(item.startswith("RIG_MODEL_")):
-                  models.append(item)
-         except:
-            logging.error("Could not obtain rig models list via Hamlib!")
-      else:
-         logging.debug("Hamlib module not present. Could not obtain a list of rig models.")
-
-      self.sources["RIG_MODEL"] = Gtk.ComboBoxText()
-      for model in models:
-         self.sources["RIG_MODEL"].append_text(model)
-      (section, option) = ("hamlib", "rig_model")
-      if(have_config and config.has_option("hamlib", "rig_model")):
-         self.sources["RIG_MODEL"].set_active(models.index(config.get("hamlib", "rig_model")))
-      else:
-         self.sources["RIG_MODEL"].set_active(models.index("RIG_MODEL_NONE")) # Set to RIG_MODEL_NONE as the default option.
-      hbox_temp.pack_start(self.sources["RIG_MODEL"], True, True, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # Path to rig
-      hbox_temp = Gtk.HBox()
-      label = Gtk.Label("Path to radio device: ")
-      label.set_width_chars(17)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["RIG_PATHNAME"] = Gtk.Entry()
-      (section, option) = ("hamlib", "rig_pathname")
-      if(have_config and config.has_option(section, option)):
-         self.sources["RIG_PATHNAME"].set_text(config.get(section, option))
-      hbox_temp.pack_start(self.sources["RIG_PATHNAME"], True, True, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      frame.add(vbox_inner)
-      self.pack_start(frame, True, True, 2)
-
-      logging.debug("Hamlib page of the preferences dialog ready!")
-      return
-
-   def get_data(self):
-      logging.debug("Retrieving data from the Hamlib page of the preferences dialog...")
-      data = {}
-      data["AUTOFILL"] = self.sources["AUTOFILL"].get_active()
-      data["RIG_PATHNAME"] = self.sources["RIG_PATHNAME"].get_text()
-      data["RIG_MODEL"] = self.sources["RIG_MODEL"].get_active_text()
-      return data
+
+    """ The section of the preferences dialog containing Hamlib-related preferences. """
+
+    def __init__(self):
+        logging.debug("Setting up the Hamlib page of the preferences dialog...")
+
+        Gtk.VBox.__init__(self, spacing=2)
+
+        config = configparser.ConfigParser()
+        have_config = (config.read(PREFERENCES_FILE) != [])
+
+        self.sources = {}
+
+        frame = Gtk.Frame()
+        frame.set_label("Hamlib support")
+
+        vbox_inner = Gtk.VBox(spacing=2)
+
+        self.sources["AUTOFILL"] = Gtk.CheckButton("Auto-fill Frequency field")
+        (section, option) = ("hamlib", "autofill")
+        if(have_config and config.has_option(section, option)):
+            self.sources["AUTOFILL"].set_active(config.get(section, option) == "True")
+        else:
+            self.sources["AUTOFILL"].set_active(False)
+        vbox_inner.pack_start(self.sources["AUTOFILL"], False, False, 2)
+
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label("Model: ")
+        label.set_alignment(0, 0.5)
+        label.set_width_chars(17)
+        hbox_temp.pack_start(label, False, False, 2)
+
+        # Get the list of rig models
+        models = ["RIG_MODEL_NONE"]
+        if(have_hamlib):
+            try:
+                for item in dir(Hamlib):
+                    if(item.startswith("RIG_MODEL_")):
+                        models.append(item)
+            except:
+                logging.error("Could not obtain rig models list via Hamlib!")
+        else:
+            logging.debug("Hamlib module not present. Could not obtain a list of rig models.")
+
+        self.sources["RIG_MODEL"] = Gtk.ComboBoxText()
+        for model in models:
+            self.sources["RIG_MODEL"].append_text(model)
+        (section, option) = ("hamlib", "rig_model")
+        if(have_config and config.has_option("hamlib", "rig_model")):
+            self.sources["RIG_MODEL"].set_active(models.index(config.get("hamlib", "rig_model")))
+        else:
+            self.sources["RIG_MODEL"].set_active(models.index("RIG_MODEL_NONE"))  # Set to RIG_MODEL_NONE as the default option.
+        hbox_temp.pack_start(self.sources["RIG_MODEL"], True, True, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # Path to rig
+        hbox_temp = Gtk.HBox()
+        label = Gtk.Label("Path to radio device: ")
+        label.set_width_chars(17)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["RIG_PATHNAME"] = Gtk.Entry()
+        (section, option) = ("hamlib", "rig_pathname")
+        if(have_config and config.has_option(section, option)):
+            self.sources["RIG_PATHNAME"].set_text(config.get(section, option))
+        hbox_temp.pack_start(self.sources["RIG_PATHNAME"], True, True, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        frame.add(vbox_inner)
+        self.pack_start(frame, True, True, 2)
+
+        logging.debug("Hamlib page of the preferences dialog ready!")
+        return
+
+    def get_data(self):
+        logging.debug("Retrieving data from the Hamlib page of the preferences dialog...")
+        data = {}
+        data["AUTOFILL"] = self.sources["AUTOFILL"].get_active()
+        data["RIG_PATHNAME"] = self.sources["RIG_PATHNAME"].get_text()
+        data["RIG_MODEL"] = self.sources["RIG_MODEL"].get_active_text()
+        return data
+
 
 class RecordsPage(Gtk.VBox):
-   
-   def __init__(self):
-      logging.debug("Setting up the Records page of the preferences dialog...")
-
-      Gtk.VBox.__init__(self, spacing=2)
-
-      # Remember that the have_config conditional in the PyQSO class may be out-of-date the next time the user opens up the preferences dialog
-      # because a configuration file may have been created after launching the application. Let's check to see if one exists again...
-      config = ConfigParser.ConfigParser()
-      have_config = (config.read(os.path.expanduser('~/.pyqso.ini')) != [])
-
-      self.sources = {}
-
-      # Autocomplete frame
-      frame = Gtk.Frame()
-      frame.set_label("Autocomplete")
-      vbox = Gtk.VBox()
-      self.sources["AUTOCOMPLETE_BAND"] = Gtk.CheckButton("Autocomplete the Band field")
-      (section, option) = ("records", "autocomplete_band")
-      if(have_config and config.has_option(section, option)):
-         self.sources["AUTOCOMPLETE_BAND"].set_active(config.get(section, option) == "True")
-      else:
-         self.sources["AUTOCOMPLETE_BAND"].set_active(True)
-      vbox.pack_start(self.sources["AUTOCOMPLETE_BAND"], False, False, 2)
-
-      self.sources["USE_UTC"] = Gtk.CheckButton("Use UTC when autocompleting the Date and Time")
-      (section, option) = ("records", "use_utc")
-      if(have_config and config.has_option(section, option)):
-         self.sources["USE_UTC"].set_active(config.get(section, option) == "True")
-      else:
-         self.sources["USE_UTC"].set_active(True)
-      vbox.pack_start(self.sources["USE_UTC"], False, False, 2)
-
-      frame.add(vbox)
-      self.pack_start(frame, False, False, 2)
-
-
-      ## Default values frame
-      frame = Gtk.Frame()
-      frame.set_label("Default values")
-      vbox = Gtk.VBox()
-      
-      # Mode
-      hbox_temp = Gtk.HBox()
-      label = Gtk.Label("Mode: ")
-      label.set_width_chars(17)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      
-      self.sources["DEFAULT_MODE"] = Gtk.ComboBoxText()
-      for mode in MODES:
-         self.sources["DEFAULT_MODE"].append_text(mode)
-      (section, option) = ("records", "default_mode")
-      if(have_config and config.has_option(section, option)):
-         self.sources["DEFAULT_MODE"].set_active(MODES.index(config.get(section, option)))
-      else:
-         self.sources["DEFAULT_MODE"].set_active(MODES.index(""))
-      hbox_temp.pack_start(self.sources["DEFAULT_MODE"], False, False, 2)
-      vbox.pack_start(hbox_temp, False, False, 2)
-
-      # Power
-      hbox_temp = Gtk.HBox()
-      label = Gtk.Label("TX Power (W): ")
-      label.set_width_chars(17)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      
-      self.sources["DEFAULT_POWER"] = Gtk.Entry()
-      (section, option) = ("records", "default_power")
-      if(have_config and config.has_option(section, option)):
-         self.sources["DEFAULT_POWER"].set_text(config.get(section, option))
-      else:
-         self.sources["DEFAULT_POWER"].set_text("")
-      hbox_temp.pack_start(self.sources["DEFAULT_POWER"], False, False, 2)
-      vbox.pack_start(hbox_temp, False, False, 2)
-      
-      frame.add(vbox)
-      self.pack_start(frame, False, False, 2)
-      
-      
-      # Callsign lookup frame
-      frame = Gtk.Frame()
-      frame.set_label("Callsign lookup")
-      vbox = Gtk.VBox()
-
-      subframe = Gtk.Frame()
-      subframe.set_label("Login details (qrz.com)")
-      inner_vbox = Gtk.VBox()
-
-      hbox = Gtk.HBox()
-      label = Gtk.Label("Username: ")
-      label.set_width_chars(9)
-      label.set_alignment(0, 0.5)
-      hbox.pack_start(label, False, False, 2)
-      self.sources["QRZ_USERNAME"] = Gtk.Entry()
-      (section, option) = ("records", "qrz_username")
-      if(have_config and config.has_option(section, option)):
-         self.sources["QRZ_USERNAME"].set_text(config.get(section, option))
-      hbox.pack_start(self.sources["QRZ_USERNAME"], False, False, 2)
-      inner_vbox.pack_start(hbox, False, False, 2)
-
-      hbox = Gtk.HBox()
-      label = Gtk.Label("Password: ")
-      label.set_width_chars(9)
-      label.set_alignment(0, 0.5)
-      hbox.pack_start(label, False, False, 2)
-      self.sources["QRZ_PASSWORD"] = Gtk.Entry()
-      self.sources["QRZ_PASSWORD"].set_visibility(False) # Mask the password with the "*" character.
-      (section, option) = ("records", "qrz_password")
-      if(have_config and config.has_option(section, option)):
-         self.sources["QRZ_PASSWORD"].set_text(base64.b64decode(config.get(section, option)))
-      hbox.pack_start(self.sources["QRZ_PASSWORD"], False, False, 2)
-      inner_vbox.pack_start(hbox, False, False, 2)
-
-      label = Gtk.Label("Warning: Login details are currently stored as\nBase64-encoded plain text in the configuration file.")
-      inner_vbox.pack_start(label, False, False, 2)
-
-      subframe.add(inner_vbox)
-      vbox.pack_start(subframe, False, False, 2)
-
-      self.sources["IGNORE_PREFIX_SUFFIX"] = Gtk.CheckButton("Ignore callsign prefixes and/or suffixes")
-      (section, option) = ("records", "ignore_prefix_suffix")
-      if(have_config and config.has_option(section, option)):
-         self.sources["IGNORE_PREFIX_SUFFIX"].set_active(config.get(section, option) == "True")
-      else:
-         self.sources["IGNORE_PREFIX_SUFFIX"].set_active(True)
-      vbox.pack_start(self.sources["IGNORE_PREFIX_SUFFIX"], False, False, 2)
-      
-      frame.add(vbox)
-      self.pack_start(frame, False, False, 2)
-      
-      logging.debug("Records page of the preferences dialog ready!")
-      return
-
-   def get_data(self):
-      logging.debug("Retrieving data from the Records page of the preferences dialog...")
-      data = {}
-      data["AUTOCOMPLETE_BAND"] = self.sources["AUTOCOMPLETE_BAND"].get_active()
-      data["USE_UTC"] = self.sources["USE_UTC"].get_active()
-      
-      data["DEFAULT_MODE"] = self.sources["DEFAULT_MODE"].get_active_text()
-      data["DEFAULT_POWER"] = self.sources["DEFAULT_POWER"].get_text()
-      
-      data["QRZ_USERNAME"] = self.sources["QRZ_USERNAME"].get_text()
-      data["QRZ_PASSWORD"] = base64.b64encode(self.sources["QRZ_PASSWORD"].get_text())
-      data["IGNORE_PREFIX_SUFFIX"] = self.sources["IGNORE_PREFIX_SUFFIX"].get_active()
-      return data
 
+    """ The section of the preferences dialog containing record-related preferences. """
+
+    def __init__(self):
+        logging.debug("Setting up the Records page of the preferences dialog...")
+
+        Gtk.VBox.__init__(self, spacing=2)
+
+        # Remember that the have_config conditional in the PyQSO class may be out-of-date the next time the user opens up the preferences dialog
+        # because a configuration file may have been created after launching the application. Let's check to see if one exists again...
+        config = configparser.ConfigParser()
+        have_config = (config.read(PREFERENCES_FILE) != [])
+
+        self.sources = {}
+
+        # Autocomplete frame
+        frame = Gtk.Frame()
+        frame.set_label("Autocomplete")
+        vbox = Gtk.VBox()
+        self.sources["AUTOCOMPLETE_BAND"] = Gtk.CheckButton("Autocomplete the Band field")
+        (section, option) = ("records", "autocomplete_band")
+        if(have_config and config.has_option(section, option)):
+            self.sources["AUTOCOMPLETE_BAND"].set_active(config.get(section, option) == "True")
+        else:
+            self.sources["AUTOCOMPLETE_BAND"].set_active(True)
+        vbox.pack_start(self.sources["AUTOCOMPLETE_BAND"], False, False, 2)
+
+        self.sources["USE_UTC"] = Gtk.CheckButton("Use UTC when autocompleting the Date and Time")
+        (section, option) = ("records", "use_utc")
+        if(have_config and config.has_option(section, option)):
+            self.sources["USE_UTC"].set_active(config.get(section, option) == "True")
+        else:
+            self.sources["USE_UTC"].set_active(True)
+        vbox.pack_start(self.sources["USE_UTC"], False, False, 2)
+
+        frame.add(vbox)
+        self.pack_start(frame, False, False, 2)
+
+        # Default values frame
+        frame = Gtk.Frame()
+        frame.set_label("Default values")
+        vbox = Gtk.VBox()
+
+        # Mode
+        hbox_temp = Gtk.HBox()
+        label = Gtk.Label("Mode: ")
+        label.set_width_chars(17)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+
+        self.sources["DEFAULT_MODE"] = Gtk.ComboBoxText()
+        for mode in sorted(MODES.keys()):
+            self.sources["DEFAULT_MODE"].append_text(mode)
+        (section, option) = ("records", "default_mode")
+        if(have_config and config.has_option(section, option)):
+            mode = config.get(section, option)
+        else:
+            mode = ""
+        self.sources["DEFAULT_MODE"].set_active(sorted(MODES.keys()).index(mode))
+        self.sources["DEFAULT_MODE"].connect("changed", self._on_mode_changed)
+        hbox_temp.pack_start(self.sources["DEFAULT_MODE"], False, False, 2)
+        vbox.pack_start(hbox_temp, False, False, 2)
+
+        # Submode
+        hbox_temp = Gtk.HBox()
+        label = Gtk.Label("Submode: ")
+        label.set_width_chars(17)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+
+        self.sources["DEFAULT_SUBMODE"] = Gtk.ComboBoxText()
+        for submode in MODES[mode]:
+            self.sources["DEFAULT_SUBMODE"].append_text(submode)
+        (section, option) = ("records", "default_submode")
+        if(have_config and config.has_option(section, option)):
+            submode = config.get(section, option)
+        else:
+            submode = ""
+        self.sources["DEFAULT_SUBMODE"].set_active(MODES[mode].index(submode))
+        hbox_temp.pack_start(self.sources["DEFAULT_SUBMODE"], False, False, 2)
+        vbox.pack_start(hbox_temp, False, False, 2)
+
+        # Power
+        hbox_temp = Gtk.HBox()
+        label = Gtk.Label("TX Power (W): ")
+        label.set_width_chars(17)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+
+        self.sources["DEFAULT_POWER"] = Gtk.Entry()
+        (section, option) = ("records", "default_power")
+        if(have_config and config.has_option(section, option)):
+            self.sources["DEFAULT_POWER"].set_text(config.get(section, option))
+        else:
+            self.sources["DEFAULT_POWER"].set_text("")
+        hbox_temp.pack_start(self.sources["DEFAULT_POWER"], False, False, 2)
+        vbox.pack_start(hbox_temp, False, False, 2)
+
+        frame.add(vbox)
+        self.pack_start(frame, False, False, 2)
+
+        # Callsign lookup frame
+        frame = Gtk.Frame()
+        frame.set_label("Callsign lookup")
+        vbox = Gtk.VBox()
+
+        # Callsign database
+        hbox_temp = Gtk.HBox()
+        label = Gtk.Label("Database: ")
+        label.set_width_chars(17)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+
+        self.sources["CALLSIGN_DATABASE"] = Gtk.ComboBoxText()
+        callsign_database = ["", "qrz.com", "hamqth.com"]
+        for database in callsign_database:
+            self.sources["CALLSIGN_DATABASE"].append_text(database)
+        (section, option) = ("records", "callsign_database")
+        if(have_config and config.has_option(section, option)):
+            self.sources["CALLSIGN_DATABASE"].set_active(callsign_database.index(config.get(section, option)))
+        else:
+            self.sources["CALLSIGN_DATABASE"].set_active(callsign_database.index(""))
+        hbox_temp.pack_start(self.sources["CALLSIGN_DATABASE"], False, False, 2)
+        vbox.pack_start(hbox_temp, False, False, 2)
+
+        # Login details
+        subframe = Gtk.Frame()
+        subframe.set_label("Login details")
+        inner_vbox = Gtk.VBox()
+
+        hbox = Gtk.HBox()
+        label = Gtk.Label("Username: ")
+        label.set_width_chars(9)
+        label.set_alignment(0, 0.5)
+        hbox.pack_start(label, False, False, 2)
+        self.sources["CALLSIGN_DATABASE_USERNAME"] = Gtk.Entry()
+        (section, option) = ("records", "callsign_database_username")
+        if(have_config and config.has_option(section, option)):
+            self.sources["CALLSIGN_DATABASE_USERNAME"].set_text(config.get(section, option))
+        hbox.pack_start(self.sources["CALLSIGN_DATABASE_USERNAME"], False, False, 2)
+        inner_vbox.pack_start(hbox, False, False, 2)
+
+        hbox = Gtk.HBox()
+        label = Gtk.Label("Password: ")
+        label.set_width_chars(9)
+        label.set_alignment(0, 0.5)
+        hbox.pack_start(label, False, False, 2)
+        self.sources["CALLSIGN_DATABASE_PASSWORD"] = Gtk.Entry()
+        self.sources["CALLSIGN_DATABASE_PASSWORD"].set_visibility(False)  # Mask the password with the "*" character.
+        (section, option) = ("records", "callsign_database_password")
+        if(have_config and config.has_option(section, option)):
+            password = base64.b64decode(config.get(section, option)).decode("utf-8")
+            self.sources["CALLSIGN_DATABASE_PASSWORD"].set_text(password)
+        hbox.pack_start(self.sources["CALLSIGN_DATABASE_PASSWORD"], False, False, 2)
+        inner_vbox.pack_start(hbox, False, False, 2)
+
+        label = Gtk.Label("Warning: Login details are currently stored as\nBase64-encoded plain text in the configuration file.")
+        inner_vbox.pack_start(label, False, False, 2)
+
+        subframe.add(inner_vbox)
+        vbox.pack_start(subframe, False, False, 2)
+
+        self.sources["IGNORE_PREFIX_SUFFIX"] = Gtk.CheckButton("Ignore callsign prefixes and/or suffixes")
+        (section, option) = ("records", "ignore_prefix_suffix")
+        if(have_config and config.has_option(section, option)):
+            self.sources["IGNORE_PREFIX_SUFFIX"].set_active(config.get(section, option) == "True")
+        else:
+            self.sources["IGNORE_PREFIX_SUFFIX"].set_active(True)
+        vbox.pack_start(self.sources["IGNORE_PREFIX_SUFFIX"], False, False, 2)
+
+        frame.add(vbox)
+        self.pack_start(frame, False, False, 2)
+
+        logging.debug("Records page of the preferences dialog ready!")
+        return
+
+    def get_data(self):
+        logging.debug("Retrieving data from the Records page of the preferences dialog...")
+        data = {}
+        data["AUTOCOMPLETE_BAND"] = self.sources["AUTOCOMPLETE_BAND"].get_active()
+        data["USE_UTC"] = self.sources["USE_UTC"].get_active()
+
+        data["DEFAULT_MODE"] = self.sources["DEFAULT_MODE"].get_active_text()
+        data["DEFAULT_SUBMODE"] = self.sources["DEFAULT_SUBMODE"].get_active_text()
+        data["DEFAULT_POWER"] = self.sources["DEFAULT_POWER"].get_text()
+
+        data["CALLSIGN_DATABASE"] = self.sources["CALLSIGN_DATABASE"].get_active_text()
+        data["CALLSIGN_DATABASE_USERNAME"] = self.sources["CALLSIGN_DATABASE_USERNAME"].get_text()
+        data["CALLSIGN_DATABASE_PASSWORD"] = base64.b64encode(self.sources["CALLSIGN_DATABASE_PASSWORD"].get_text().encode("utf-8")).decode('utf-8')  # Need to convert from bytes to str here.
+        data["IGNORE_PREFIX_SUFFIX"] = self.sources["IGNORE_PREFIX_SUFFIX"].get_active()
+        return data
+
+    def _on_mode_changed(self, combo):
+        """ If the MODE field has changed its value, then fill the SUBMODE field with all the available SUBMODE options for that new MODE. """
+        self.sources["DEFAULT_SUBMODE"].get_model().clear()
+        mode = combo.get_active_text()
+        for submode in MODES[mode]:
+            self.sources["DEFAULT_SUBMODE"].append_text(submode)
+        return
+
+
+class ADIFPage(Gtk.VBox):
+
+    """ The section of the preferences dialog containing ADIF-related preferences. """
+
+    def __init__(self):
+        logging.debug("Setting up the ADIF page of the preferences dialog...")
+
+        Gtk.VBox.__init__(self, spacing=2)
+
+        # Remember that the have_config conditional in the PyQSO class may be out-of-date the next time the user opens up the preferences dialog
+        # because a configuration file may have been created after launching the application. Let's check to see if one exists again...
+        config = configparser.ConfigParser()
+        have_config = (config.read(PREFERENCES_FILE) != [])
+
+        self.sources = {}
+
+        # Import frame
+        frame = Gtk.Frame()
+        frame.set_label("Import")
+        vbox = Gtk.VBox()
+        self.sources["MERGE_COMMENT"] = Gtk.CheckButton("Merge any text in the COMMENT field with the NOTES field.")
+        (section, option) = ("adif", "merge_comment")
+        if(have_config and config.has_option(section, option)):
+            self.sources["MERGE_COMMENT"].set_active(config.get(section, option) == "True")
+        else:
+            self.sources["MERGE_COMMENT"].set_active(False)
+        vbox.pack_start(self.sources["MERGE_COMMENT"], False, False, 2)
+
+        frame.add(vbox)
+        self.pack_start(frame, False, False, 2)
+
+        logging.debug("ADIF page of the preferences dialog ready!")
+        return
+
+    def get_data(self):
+        logging.debug("Retrieving data from the ADIF page of the preferences dialog...")
+        data = {}
+        data["MERGE_COMMENT"] = self.sources["MERGE_COMMENT"].get_active()
+        return data
diff --git a/pyqso/record_dialog.py b/pyqso/record_dialog.py
index 74625cd..4b934a3 100644
--- a/pyqso/record_dialog.py
+++ b/pyqso/record_dialog.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,556 +17,627 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject
+from gi.repository import Gtk
 import logging
-import ConfigParser
+import configparser
 from datetime import datetime
 from os.path import expanduser
 import base64
 try:
-   import Hamlib
-   have_hamlib = True
+    import Hamlib
+    have_hamlib = True
 except ImportError:
-   logging.error("Could not import the Hamlib module!")
-   have_hamlib = False
+    logging.warning("Could not import the Hamlib module!")
+    have_hamlib = False
+
+from pyqso.adif import *
+from pyqso.callsign_lookup import *
+from pyqso.auxiliary_dialogs import *
 
-from adif import AVAILABLE_FIELD_NAMES_FRIENDLY, AVAILABLE_FIELD_NAMES_ORDERED, MODES, BANDS, BANDS_RANGES
-from callsign_lookup import *
-from auxiliary_dialogs import *
 
 class RecordDialog(Gtk.Dialog):
-   """ A dialog through which users can enter information about a QSO/record. """
-   
-   def __init__(self, parent, log, index=None):
-      """ Set up the layout of the record dialog.
-      If a record index is specified in the 'index' argument, then the dialog turns into 'edit record mode' and fills the data sources with the existing data in the log.
-      If the 'index' argument is None, then the dialog starts off with nothing in the data sources (e.g. the Gtk.Entry boxes). """
-
-      logging.debug("Setting up the record dialog...")
-      
-      if(index is not None):
-         title = "Edit Record %d" % index
-      else:
-         title = "Add Record"
-      Gtk.Dialog.__init__(self, title=title, parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
-
-      # Check if a configuration file is present, since we might need it to set up the rest of the dialog.
-      config = ConfigParser.ConfigParser()
-      have_config = (config.read(expanduser('~/.pyqso.ini')) != [])
-      
-      ## QSO DATA FRAME
-      qso_frame = Gtk.Frame()
-      qso_frame.set_label("QSO Information")
-      self.vbox.add(qso_frame)
-
-      hbox_inner = Gtk.HBox(spacing=2)
-
-      vbox_inner = Gtk.VBox(spacing=2)
-      hbox_inner.pack_start(vbox_inner, True, True, 2)
-
-      # Create label:entry pairs and store them in a dictionary
-      self.sources = {}
-
-      # CALL
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["CALL"], halign=Gtk.Align.START)
-      label.set_width_chars(15)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["CALL"] = Gtk.Entry()
-      self.sources["CALL"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["CALL"], False, False, 2)
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_INFO, Gtk.IconSize.MENU)
-      button = Gtk.Button()
-      button.add(icon)
-      button.connect("clicked", self.lookup_callback) # Looks up the callsign on qrz.com for callsign and station information.
-      button.set_tooltip_text("Lookup on qrz.com")
-      hbox_temp.pack_start(button, True, True, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # DATE
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["QSO_DATE"], halign=Gtk.Align.START)
-      label.set_width_chars(15)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["QSO_DATE"] = Gtk.Entry()
-      self.sources["QSO_DATE"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["QSO_DATE"], False, False, 2)
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_GO_BACK, Gtk.IconSize.MENU)
-      button = Gtk.Button()
-      button.add(icon)
-      button.connect("clicked", self.calendar_callback)
-      button.set_tooltip_text("Select date from calendar")
-      hbox_temp.pack_start(button, True, True, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # TIME
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["TIME_ON"], halign=Gtk.Align.START)
-      label.set_alignment(0, 0.5)
-      label.set_width_chars(15)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["TIME_ON"] = Gtk.Entry()
-      self.sources["TIME_ON"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["TIME_ON"], False, False, 2)
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_MEDIA_PLAY, Gtk.IconSize.MENU)
-      button = Gtk.Button()
-      button.add(icon)
-      button.connect("clicked", self.set_current_datetime_callback)
-      button.set_tooltip_text("Use the current time and date")
-      hbox_temp.pack_start(button, True, True, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # FREQ
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["FREQ"], halign=Gtk.Align.START)
-      label.set_alignment(0, 0.5)
-      label.set_width_chars(15)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["FREQ"] = Gtk.Entry()
-      self.sources["FREQ"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["FREQ"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # BAND
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["BAND"], halign=Gtk.Align.START)
-      label.set_alignment(0, 0.5)
-      label.set_width_chars(15)
-      hbox_temp.pack_start(label, False, False, 2)
-
-      self.sources["BAND"] = Gtk.ComboBoxText()
-      for band in BANDS:
-         self.sources["BAND"].append_text(band)
-      self.sources["BAND"].set_active(0) # Set an empty string as the default option.
-      hbox_temp.pack_start(self.sources["BAND"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # MODE
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["MODE"])
-      label.set_alignment(0, 0.5)
-      label.set_width_chars(15)
-      hbox_temp.pack_start(label, False, False, 2)
-
-      self.sources["MODE"] = Gtk.ComboBoxText()
-      for mode in MODES:
-         self.sources["MODE"].append_text(mode)
-      self.sources["MODE"].set_active(0) # Set an empty string as the default option.
-      hbox_temp.pack_start(self.sources["MODE"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # POWER
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["TX_PWR"], halign=Gtk.Align.START)
-      label.set_alignment(0, 0.5)
-      label.set_width_chars(15)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["TX_PWR"] = Gtk.Entry()
-      self.sources["TX_PWR"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["TX_PWR"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      vbox_inner = Gtk.VBox(spacing=2)
-      hbox_inner.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
-      hbox_inner.pack_start(vbox_inner, True, True, 2)
-
-      # RST_SENT
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["RST_SENT"])
-      label.set_alignment(0, 0.5)
-      label.set_width_chars(15)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["RST_SENT"] = Gtk.Entry()
-      self.sources["RST_SENT"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["RST_SENT"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # RST_RCVD
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["RST_RCVD"])
-      label.set_alignment(0, 0.5)
-      label.set_width_chars(15)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["RST_RCVD"] = Gtk.Entry()
-      self.sources["RST_RCVD"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["RST_RCVD"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # QSL_SENT
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["QSL_SENT"])
-      label.set_alignment(0, 0.5)
-      label.set_width_chars(15)
-      hbox_temp.pack_start(label, False, False, 2)
-      qsl_options = ["", "Y", "N", "R", "I"]
-      self.sources["QSL_SENT"] = Gtk.ComboBoxText()
-      for option in qsl_options:
-         self.sources["QSL_SENT"].append_text(option)
-      self.sources["QSL_SENT"].set_active(0) # Set an empty string as the default option.
-      hbox_temp.pack_start(self.sources["QSL_SENT"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # QSL_RCVD
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["QSL_RCVD"])
-      label.set_alignment(0, 0.5)
-      label.set_width_chars(15)
-      hbox_temp.pack_start(label, False, False, 2)
-      qsl_options = ["", "Y", "N", "R", "I"]
-      self.sources["QSL_RCVD"] = Gtk.ComboBoxText()
-      for option in qsl_options:
-         self.sources["QSL_RCVD"].append_text(option)
-      self.sources["QSL_RCVD"].set_active(0) # Set an empty string as the default option.
-      hbox_temp.pack_start(self.sources["QSL_RCVD"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # NOTES
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["NOTES"])
-      label.set_alignment(0, 0.5)
-      label.set_width_chars(15)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.textview = Gtk.TextView()
-      sw = Gtk.ScrolledWindow()
-      sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
-      sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
-      sw.add(self.textview)
-      self.sources["NOTES"] = self.textview.get_buffer()
-      hbox_temp.pack_start(sw, True, True, 2)
-      vbox_inner.pack_start(hbox_temp, True, True, 2)
-
-      qso_frame.add(hbox_inner)
-
-
-      ## STATION INFORMATION FRAME
-      station_frame = Gtk.Frame()
-      station_frame.set_label("Station Information")
-      self.vbox.add(station_frame)
-
-      hbox_inner = Gtk.HBox(spacing=2)
-
-      vbox_inner = Gtk.VBox(spacing=2)
-      hbox_inner.pack_start(vbox_inner, True, True, 2)
-
-      # NAME
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["NAME"], halign=Gtk.Align.START)
-      label.set_width_chars(15)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["NAME"] = Gtk.Entry()
-      self.sources["NAME"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["NAME"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # ADDRESS
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["ADDRESS"], halign=Gtk.Align.START)
-      label.set_width_chars(15)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["ADDRESS"] = Gtk.Entry()
-      self.sources["ADDRESS"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["ADDRESS"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # STATE
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["STATE"], halign=Gtk.Align.START)
-      label.set_width_chars(15)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["STATE"] = Gtk.Entry()
-      self.sources["STATE"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["STATE"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # COUNTRY
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["COUNTRY"], halign=Gtk.Align.START)
-      label.set_width_chars(15)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["COUNTRY"] = Gtk.Entry()
-      self.sources["COUNTRY"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["COUNTRY"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      vbox_inner = Gtk.VBox(spacing=2)
-      hbox_inner.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
-      hbox_inner.pack_start(vbox_inner, True, True, 2)
-
-      # DXCC
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["DXCC"], halign=Gtk.Align.START)
-      label.set_width_chars(15)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["DXCC"] = Gtk.Entry()
-      self.sources["DXCC"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["DXCC"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # CQZ
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["CQZ"], halign=Gtk.Align.START)
-      label.set_width_chars(15)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["CQZ"] = Gtk.Entry()
-      self.sources["CQZ"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["CQZ"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # ITUZ
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["ITUZ"], halign=Gtk.Align.START)
-      label.set_width_chars(15)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["ITUZ"] = Gtk.Entry()
-      self.sources["ITUZ"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["ITUZ"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      # IOTA
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["IOTA"], halign=Gtk.Align.START)
-      label.set_width_chars(15)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 2)
-      self.sources["IOTA"] = Gtk.Entry()
-      self.sources["IOTA"].set_width_chars(15)
-      hbox_temp.pack_start(self.sources["IOTA"], False, False, 2)
-      vbox_inner.pack_start(hbox_temp, False, False, 2)
-
-      station_frame.add(hbox_inner)
-
-      # Populate various fields, if possible.
-      if(index is not None):
-         # The record already exists, so display its current data in the input boxes.
-         record = log.get_record_by_index(index)
-         field_names = AVAILABLE_FIELD_NAMES_ORDERED
-         for i in range(0, len(field_names)):
-            data = record[field_names[i].lower()]
-            if(data is None):
-               data = ""
-            if(field_names[i] == "BAND"):
-               self.sources[field_names[i]].set_active(BANDS.index(data))
-            elif(field_names[i] == "MODE"):
-               self.sources[field_names[i]].set_active(MODES.index(data))
-            elif(field_names[i] == "QSL_SENT" or field_names[i] == "QSL_RCVD"):
-               self.sources[field_names[i]].set_active(qsl_options.index(data))
-            elif(field_names[i] == "NOTES"):
-               # Remember to put the new line escape characters back in when displaying the data in a Gtk.TextView
-               text = data.replace("\\n", "\n") 
-               self.sources[field_names[i]].set_text(text)
+
+    """ A dialog through which users can enter information about a QSO/record. """
+
+    def __init__(self, parent, log, index=None):
+        """ Set up the layout of the record dialog, populate the various fields with the QSO details (if the record already exists), and show the dialog to the user.
+
+        :arg parent: The parent Gtk window.
+        :arg log: The log to which the record belongs (or will belong).
+        :arg int index: If specified, then the dialog turns into 'edit record mode' and fills the data sources (e.g. the Gtk.Entry boxes) with the existing data in the log. If not specified (i.e. index is None), then the dialog starts off with nothing in the data sources.
+        """
+
+        logging.debug("Setting up the record dialog...")
+
+        if(index is not None):
+            title = "Edit Record %d" % index
+        else:
+            title = "Add Record"
+        Gtk.Dialog.__init__(self, title=title, parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
+
+        # Check if a configuration file is present, since we might need it to set up the rest of the dialog.
+        config = configparser.ConfigParser()
+        have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != [])
+
+        # QSO DATA FRAME
+        qso_frame = Gtk.Frame()
+        qso_frame.set_label("QSO Information")
+        self.vbox.add(qso_frame)
+
+        hbox_inner = Gtk.HBox(spacing=2)
+
+        vbox_inner = Gtk.VBox(spacing=2)
+        hbox_inner.pack_start(vbox_inner, True, True, 2)
+
+        # Create label:entry pairs and store them in a dictionary
+        self.sources = {}
+
+        # CALL
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["CALL"], halign=Gtk.Align.START)
+        label.set_width_chars(15)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["CALL"] = Gtk.Entry()
+        self.sources["CALL"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["CALL"], False, False, 2)
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_INFO, Gtk.IconSize.MENU)
+        button = Gtk.Button()
+        button.add(icon)
+        button.connect("clicked", self.lookup_callback)  # Looks up the callsign using an online database, for callsign and station information.
+        button.set_tooltip_text("Callsign lookup")
+        hbox_temp.pack_start(button, True, True, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # DATE
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["QSO_DATE"], halign=Gtk.Align.START)
+        label.set_width_chars(15)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["QSO_DATE"] = Gtk.Entry()
+        self.sources["QSO_DATE"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["QSO_DATE"], False, False, 2)
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_GO_BACK, Gtk.IconSize.MENU)
+        button = Gtk.Button()
+        button.add(icon)
+        button.connect("clicked", self.calendar_callback)
+        button.set_tooltip_text("Select date from calendar")
+        hbox_temp.pack_start(button, True, True, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # TIME
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["TIME_ON"], halign=Gtk.Align.START)
+        label.set_alignment(0, 0.5)
+        label.set_width_chars(15)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["TIME_ON"] = Gtk.Entry()
+        self.sources["TIME_ON"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["TIME_ON"], False, False, 2)
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_MEDIA_PLAY, Gtk.IconSize.MENU)
+        button = Gtk.Button()
+        button.add(icon)
+        button.connect("clicked", self.set_current_datetime_callback)
+        button.set_tooltip_text("Use the current time and date")
+        hbox_temp.pack_start(button, True, True, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # FREQ
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["FREQ"], halign=Gtk.Align.START)
+        label.set_alignment(0, 0.5)
+        label.set_width_chars(15)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["FREQ"] = Gtk.Entry()
+        self.sources["FREQ"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["FREQ"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # BAND
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["BAND"], halign=Gtk.Align.START)
+        label.set_alignment(0, 0.5)
+        label.set_width_chars(15)
+        hbox_temp.pack_start(label, False, False, 2)
+
+        self.sources["BAND"] = Gtk.ComboBoxText()
+        for band in BANDS:
+            self.sources["BAND"].append_text(band)
+        self.sources["BAND"].set_active(0)  # Set an empty string as the default option.
+        hbox_temp.pack_start(self.sources["BAND"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # MODE
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["MODE"])
+        label.set_alignment(0, 0.5)
+        label.set_width_chars(15)
+        hbox_temp.pack_start(label, False, False, 2)
+
+        self.sources["MODE"] = Gtk.ComboBoxText()
+        for mode in sorted(MODES.keys()):
+            self.sources["MODE"].append_text(mode)
+        self.sources["MODE"].set_active(0)  # Set an empty string as the default option.
+        self.sources["MODE"].connect("changed", self._on_mode_changed)
+        hbox_temp.pack_start(self.sources["MODE"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # SUBMODE
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["SUBMODE"])
+        label.set_alignment(0, 0.5)
+        label.set_width_chars(15)
+        hbox_temp.pack_start(label, False, False, 2)
+
+        self.sources["SUBMODE"] = Gtk.ComboBoxText()
+        self.sources["SUBMODE"].append_text("")
+        self.sources["SUBMODE"].set_active(0)  # Set an empty string initially. As soon as the user selects a particular MODE, the available SUBMODES will appear.
+        hbox_temp.pack_start(self.sources["SUBMODE"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # POWER
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["TX_PWR"], halign=Gtk.Align.START)
+        label.set_alignment(0, 0.5)
+        label.set_width_chars(15)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["TX_PWR"] = Gtk.Entry()
+        self.sources["TX_PWR"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["TX_PWR"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        vbox_inner = Gtk.VBox(spacing=2)
+        hbox_inner.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
+        hbox_inner.pack_start(vbox_inner, True, True, 2)
+
+        # RST_SENT
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["RST_SENT"])
+        label.set_alignment(0, 0.5)
+        label.set_width_chars(15)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["RST_SENT"] = Gtk.Entry()
+        self.sources["RST_SENT"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["RST_SENT"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # RST_RCVD
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["RST_RCVD"])
+        label.set_alignment(0, 0.5)
+        label.set_width_chars(15)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["RST_RCVD"] = Gtk.Entry()
+        self.sources["RST_RCVD"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["RST_RCVD"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # QSL_SENT
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["QSL_SENT"])
+        label.set_alignment(0, 0.5)
+        label.set_width_chars(15)
+        hbox_temp.pack_start(label, False, False, 2)
+        qsl_options = ["", "Y", "N", "R", "I"]
+        self.sources["QSL_SENT"] = Gtk.ComboBoxText()
+        for option in qsl_options:
+            self.sources["QSL_SENT"].append_text(option)
+        self.sources["QSL_SENT"].set_active(0)  # Set an empty string as the default option.
+        hbox_temp.pack_start(self.sources["QSL_SENT"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # QSL_RCVD
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["QSL_RCVD"])
+        label.set_alignment(0, 0.5)
+        label.set_width_chars(15)
+        hbox_temp.pack_start(label, False, False, 2)
+        qsl_options = ["", "Y", "N", "R", "I"]
+        self.sources["QSL_RCVD"] = Gtk.ComboBoxText()
+        for option in qsl_options:
+            self.sources["QSL_RCVD"].append_text(option)
+        self.sources["QSL_RCVD"].set_active(0)  # Set an empty string as the default option.
+        hbox_temp.pack_start(self.sources["QSL_RCVD"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # NOTES
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["NOTES"])
+        label.set_alignment(0, 0.5)
+        label.set_width_chars(15)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.textview = Gtk.TextView()
+        sw = Gtk.ScrolledWindow()
+        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
+        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+        sw.add(self.textview)
+        self.sources["NOTES"] = self.textview.get_buffer()
+        hbox_temp.pack_start(sw, True, True, 2)
+        vbox_inner.pack_start(hbox_temp, True, True, 2)
+
+        qso_frame.add(hbox_inner)
+
+        # STATION INFORMATION FRAME
+        station_frame = Gtk.Frame()
+        station_frame.set_label("Station Information")
+        self.vbox.add(station_frame)
+
+        hbox_inner = Gtk.HBox(spacing=2)
+
+        vbox_inner = Gtk.VBox(spacing=2)
+        hbox_inner.pack_start(vbox_inner, True, True, 2)
+
+        # NAME
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["NAME"], halign=Gtk.Align.START)
+        label.set_width_chars(15)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["NAME"] = Gtk.Entry()
+        self.sources["NAME"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["NAME"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # ADDRESS
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["ADDRESS"], halign=Gtk.Align.START)
+        label.set_width_chars(15)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["ADDRESS"] = Gtk.Entry()
+        self.sources["ADDRESS"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["ADDRESS"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # STATE
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["STATE"], halign=Gtk.Align.START)
+        label.set_width_chars(15)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["STATE"] = Gtk.Entry()
+        self.sources["STATE"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["STATE"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # COUNTRY
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["COUNTRY"], halign=Gtk.Align.START)
+        label.set_width_chars(15)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["COUNTRY"] = Gtk.Entry()
+        self.sources["COUNTRY"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["COUNTRY"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        vbox_inner = Gtk.VBox(spacing=2)
+        hbox_inner.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
+        hbox_inner.pack_start(vbox_inner, True, True, 2)
+
+        # DXCC
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["DXCC"], halign=Gtk.Align.START)
+        label.set_width_chars(15)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["DXCC"] = Gtk.Entry()
+        self.sources["DXCC"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["DXCC"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # CQZ
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["CQZ"], halign=Gtk.Align.START)
+        label.set_width_chars(15)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["CQZ"] = Gtk.Entry()
+        self.sources["CQZ"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["CQZ"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # ITUZ
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["ITUZ"], halign=Gtk.Align.START)
+        label.set_width_chars(15)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["ITUZ"] = Gtk.Entry()
+        self.sources["ITUZ"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["ITUZ"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        # IOTA
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label(AVAILABLE_FIELD_NAMES_FRIENDLY["IOTA"], halign=Gtk.Align.START)
+        label.set_width_chars(15)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 2)
+        self.sources["IOTA"] = Gtk.Entry()
+        self.sources["IOTA"].set_width_chars(15)
+        hbox_temp.pack_start(self.sources["IOTA"], False, False, 2)
+        vbox_inner.pack_start(hbox_temp, False, False, 2)
+
+        station_frame.add(hbox_inner)
+
+        # Populate various fields, if possible.
+        if(index is not None):
+            # The record already exists, so display its current data in the input boxes.
+            record = log.get_record_by_index(index)
+            field_names = AVAILABLE_FIELD_NAMES_ORDERED
+            for i in range(0, len(field_names)):
+                data = record[field_names[i].lower()]
+                if(data is None):
+                    data = ""
+                if(field_names[i] == "BAND"):
+                    self.sources[field_names[i]].set_active(BANDS.index(data))
+                elif(field_names[i] == "MODE"):
+                    self.sources[field_names[i]].set_active(sorted(MODES.keys()).index(data))
+
+                    submode_data = record["submode"]
+                    if(submode_data is None):
+                        submode_data = ""
+                    self.sources["SUBMODE"].set_active(MODES[data].index(submode_data))
+                elif(field_names[i] == "SUBMODE"):
+                    continue
+                elif(field_names[i] == "QSL_SENT" or field_names[i] == "QSL_RCVD"):
+                    self.sources[field_names[i]].set_active(qsl_options.index(data))
+                elif(field_names[i] == "NOTES"):
+                    # Remember to put the new line escape characters back in when displaying the data in a Gtk.TextView
+                    text = data.replace("\\n", "\n")
+                    self.sources[field_names[i]].set_text(text)
+                else:
+                    self.sources[field_names[i]].set_text(data)
+        else:
+            # Automatically fill in the current date and time
+            self.set_current_datetime_callback()
+
+            # Set up default field values
+            # Mode
+            (section, option) = ("records", "default_mode")
+            if(have_config and config.has_option(section, option)):
+                mode = config.get(section, option)
+            else:
+                mode = ""
+            self.sources["MODE"].set_active(sorted(MODES.keys()).index(mode))
+
+            # Submode
+            (section, option) = ("records", "default_submode")
+            if(have_config and config.has_option(section, option)):
+                submode = config.get(section, option)
+            else:
+                submode = ""
+            self.sources["SUBMODE"].set_active(MODES[mode].index(submode))
+
+            # Power
+            (section, option) = ("records", "default_power")
+            if(have_config and config.has_option(section, option)):
+                power = config.get(section, option)
             else:
-               self.sources[field_names[i]].set_text(data)
-      else:
-         # Automatically fill in the current date and time
-         self.set_current_datetime_callback()
-
-         ## Set up default field values
-         # Mode
-         (section, option) = ("records", "default_mode")
-         if(have_config and config.has_option(section, option)):
-            mode = config.get(section, option)
-         else:
-            mode = ""
-         self.sources["MODE"].set_active(MODES.index(mode))
-
-         # Power
-         (section, option) = ("records", "default_power")
-         if(have_config and config.has_option(section, option)):
-            power = config.get(section, option)
-         else:
-            power = ""
-         self.sources["TX_PWR"].set_text(power)
-         
-         if(have_hamlib):
-            # If the Hamlib module is present, then use it to fill in the Frequency field if desired.
-            if(have_config and config.has_option("hamlib", "autofill") and config.has_option("hamlib", "rig_model") and config.has_option("hamlib", "rig_pathname")):
-               autofill = (config.get("hamlib", "autofill") == "True")
-               rig_model = config.get("hamlib", "rig_model")
-               rig_pathname = config.get("hamlib", "rig_pathname")
-               if(autofill):
-                  # Use Hamlib (if available) to get the frequency
-                  try:
-                     Hamlib.rig_set_debug(Hamlib.RIG_DEBUG_NONE)
-                     rig = Hamlib.Rig(Hamlib.__dict__[rig_model]) # Look up the model's numerical index in Hamlib's symbol dictionary
-                     rig.set_conf("rig_pathname", rig_pathname)
-                     rig.open()
-                     frequency = "%.6f" % (rig.get_freq()/1.0e6) # Converting to MHz here
-                     self.sources["FREQ"].set_text(frequency)
-                     rig.close()
-                  except:
-                     logging.error("Could not obtain Frequency data via Hamlib!")
-
-      # Do we want PyQSO to autocomplete the Band field based on the Frequency field?
-      (section, option) = ("records", "autocomplete_band")
-      if(have_config and config.get(section, option)):
-         autocomplete_band = (config.get(section, option) == "True")
-         if(autocomplete_band):
+                power = ""
+            self.sources["TX_PWR"].set_text(power)
+
+            if(have_hamlib):
+                # If the Hamlib module is present, then use it to fill in the Frequency field if desired.
+                if(have_config and config.has_option("hamlib", "autofill") and config.has_option("hamlib", "rig_model") and config.has_option("hamlib", "rig_pathname")):
+                    autofill = (config.get("hamlib", "autofill") == "True")
+                    rig_model = config.get("hamlib", "rig_model")
+                    rig_pathname = config.get("hamlib", "rig_pathname")
+                    if(autofill):
+                        # Use Hamlib (if available) to get the frequency
+                        try:
+                            Hamlib.rig_set_debug(Hamlib.RIG_DEBUG_NONE)
+                            rig = Hamlib.Rig(Hamlib.__dict__[rig_model])  # Look up the model's numerical index in Hamlib's symbol dictionary
+                            rig.set_conf("rig_pathname", rig_pathname)
+                            rig.open()
+                            frequency = "%.6f" % (rig.get_freq()/1.0e6)  # Converting to MHz here
+                            self.sources["FREQ"].set_text(frequency)
+                            rig.close()
+                        except:
+                            logging.error("Could not obtain Frequency data via Hamlib!")
+
+        # Do we want PyQSO to autocomplete the Band field based on the Frequency field?
+        (section, option) = ("records", "autocomplete_band")
+        if(have_config and config.get(section, option)):
+            autocomplete_band = (config.get(section, option) == "True")
+            if(autocomplete_band):
+                self.sources["FREQ"].connect("changed", self._autocomplete_band)
+        else:
+            # If no configuration file exists, autocomplete the Band field by default.
             self.sources["FREQ"].connect("changed", self._autocomplete_band)
-      else:
-         # If no configuration file exists, autocomplete the Band field by default.
-         self.sources["FREQ"].connect("changed", self._autocomplete_band)
-
-      self.show_all()
-
-      logging.debug("Record dialog ready!")
-
-      return
-
-   def get_data(self, field_name):
-      """ Return the data for a specified field (with name 'field_name') from the Gtk.Entry/Gtk.ComboBoxText/etc boxes in the record dialog. """
-      logging.debug("Retrieving the data in field %s from the record dialog..." % field_name)
-      if(field_name == "CALL"):
-         # Always show the callsigns in upper case.
-         return self.sources[field_name].get_text().upper()
-      elif(field_name == "BAND" or field_name == "MODE" or field_name == "QSL_SENT" or field_name == "QSL_RCVD"):
-         return self.sources[field_name].get_active_text()
-      elif(field_name == "NOTES"):
-         (start, end) = self.sources[field_name].get_bounds()
-         text = self.sources[field_name].get_text(start, end, True)
-         # Replace the escape characters with a slightly different new line marker.
-         # If we don't do this, the rows in the Gtk.TreeView expand based on the number of new lines.
-         text = text.replace("\n", "\\n")
-         return text
-      else:
-         return self.sources[field_name].get_text()
-
-
-   def _autocomplete_band(self, widget=None):
-      """ If a value for the Frequency is entered, this function autocompletes the Band field. """
-
-      frequency = self.sources["FREQ"].get_text()
-      # Check whether we actually have a (valid) value to use. If not, set the BAND field to an empty string ("").
-      try:
-         frequency = float(frequency)
-      except ValueError:
-         self.sources["BAND"].set_active(0)
-         return
-      
-      # Find which band the frequency lies in.
-      for i in range(1, len(BANDS)):
-         if(frequency >= BANDS_RANGES[i][0] and frequency <= BANDS_RANGES[i][1]):
-            self.sources["BAND"].set_active(i)
-            return
 
-      self.sources["BAND"].set_active(0) # If we've reached this, then the frequency does not lie in any of the specified bands.
-      return
+        self.show_all()
+
+        logging.debug("Record dialog ready!")
+
+        return
+
+    def get_data(self, field_name):
+        """ Return the data for a specified field from the Gtk.Entry/Gtk.ComboBoxText/etc boxes in the record dialog.
+
+        :arg str field_name: The name of the field containing the desired data.
+        :returns: The data in the specified field.
+        :rtype: str
+        """
+        logging.debug("Retrieving the data in field %s from the record dialog..." % field_name)
+        if(field_name == "CALL"):
+            # Always show the callsigns in upper case.
+            return self.sources[field_name].get_text().upper()
+        elif(field_name == "MODE"):
+            return self.sources["MODE"].get_active_text()
+        elif(field_name == "SUBMODE"):
+            return self.sources["SUBMODE"].get_active_text()
+        elif(field_name == "BAND" or field_name == "QSL_SENT" or field_name == "QSL_RCVD"):
+            return self.sources[field_name].get_active_text()
+        elif(field_name == "NOTES"):
+            (start, end) = self.sources[field_name].get_bounds()
+            text = self.sources[field_name].get_text(start, end, True)
+            # Replace the escape characters with a slightly different new line marker.
+            # If we don't do this, the rows in the Gtk.TreeView expand based on the number of new lines.
+            text = text.replace("\n", "\\n")
+            return text
+        else:
+            return self.sources[field_name].get_text()
+
+    def _on_mode_changed(self, combo):
+        """ If the MODE field has changed its value, then fill the SUBMODE field with all the available SUBMODE options for that new MODE. """
+        self.sources["SUBMODE"].get_model().clear()
+        text = combo.get_active_text()
+        for submode in MODES[text]:
+            self.sources["SUBMODE"].append_text(submode)
+        return
+
+    def _autocomplete_band(self, widget=None):
+        """ If a value for the Frequency is entered, this function autocompletes the Band field. """
+
+        frequency = self.sources["FREQ"].get_text()
+        # Check whether we actually have a (valid) value to use. If not, set the BAND field to an empty string ("").
+        try:
+            frequency = float(frequency)
+        except ValueError:
+            self.sources["BAND"].set_active(0)
+            return
 
+        # Find which band the frequency lies in.
+        for i in range(1, len(BANDS)):
+            if(frequency >= BANDS_RANGES[i][0] and frequency <= BANDS_RANGES[i][1]):
+                self.sources["BAND"].set_active(i)
+                return
+
+        self.sources["BAND"].set_active(0)  # If we've reached this, then the frequency does not lie in any of the specified bands.
+        return
+
+    def lookup_callback(self, widget=None):
+        """ Get the callsign-related data from an online database and store it in the relevant Gtk.Entry boxes, but return None. """
+
+        # Get the database name.
+        config = configparser.ConfigParser()
+        have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != [])
+        try:
+            if(have_config and config.has_option("records", "callsign_database")):
+                database = config.get("records", "callsign_database")
+                if(database == ""):
+                    raise ValueError
+            else:
+                raise ValueError
+        except ValueError:
+            error(parent=self, message="To perform a callsign lookup, please specify the name of the callsign database in the Preferences.")
+            return
 
-   def lookup_callback(self, widget=None):
-      """ Get the callsign-related data from the qrz.com database and store it in the relevant Gtk.Entry boxes, but return None. """
-      callsign_lookup = CallsignLookup(parent = self)
+        try:
+            if(database == "qrz.com"):
+                # QRZ.com
+                callsign_lookup = CallsignLookupQRZ(parent=self)
+            elif(database == "hamqth.com"):
+                # HamQTH
+                callsign_lookup = CallsignLookupHamQTH(parent=self)
+            else:
+                raise ValueError("Unknown callsign database: %s" % database)
+        except ValueError as e:
+            logging.exception(e)
+            error(e)
+            return
 
-      config = ConfigParser.ConfigParser()
-      have_config = (config.read(expanduser('~/.pyqso.ini')) != [])
-      if(have_config and config.has_option("records", "qrz_username") and config.has_option("records", "qrz_password")):
-         username = config.get("records", "qrz_username")
-         password = base64.b64decode(config.get("records", "qrz_password"))
-         if(username == "" or password == ""):
+        # Get username and password from configuration file
+        if(have_config and config.has_option("records", "callsign_database_username") and config.has_option("records", "callsign_database_password")):
+            username = config.get("records", "callsign_database_username")
+            password = base64.b64decode(config.get("records", "callsign_database_password")).decode("utf-8")
+            if(username == "" or password == ""):
+                details_given = False
+            else:
+                details_given = True
+        else:
             details_given = False
-         else:
-            details_given = True
-      else:
-         details_given = False
-      if(not details_given):
-         error(parent=self, message="To perform a callsign lookup, please specify your qrz.com username and password in the Preferences.")
-         return
-
-      connected = callsign_lookup.connect(username, password)
-      if(connected):
-         full_callsign = self.sources["CALL"].get_text()
-         # Check whether we want to ignore any prefixes (e.g. "IA/") or suffixes "(e.g. "/M") in the callsign
-         # before performing the lookup.
-         if(have_config and config.has_option("records", "ignore_prefix_suffix")):
-            ignore_prefix_suffix = (config.get("records", "ignore_prefix_suffix") == "True")
-         else:
-            ignore_prefix_suffix = True
-            
-         fields_and_data = callsign_lookup.lookup(full_callsign, ignore_prefix_suffix=ignore_prefix_suffix)
-         for field_name in fields_and_data.keys():
-            self.sources[field_name].set_text(fields_and_data[field_name])
-      return
-
-   def calendar_callback(self, widget):
-      """ Open up a calendar widget for easy QSO_DATE selection. Return None after the user destroys the dialog. """
-      calendar = CalendarDialog(parent = self)
-      response = calendar.run()
-      if(response == Gtk.ResponseType.OK):
-         date = calendar.get_date()
-         self.sources["QSO_DATE"].set_text(date)
-      calendar.destroy()
-      return
-      
-   def set_current_datetime_callback(self, widget=None):
-      """ Insert the current date and time. """
-      
-      # Check if a configuration file is present.
-      config = ConfigParser.ConfigParser()
-      have_config = (config.read(expanduser('~/.pyqso.ini')) != [])
-      
-      # Do we want to use UTC or the computer's local time?
-      (section, option) = ("records", "use_utc")
-      if(have_config and config.has_option(section, option)):
-         use_utc = (config.get(section, option) == "True")
-         if(use_utc):
-            dt = datetime.utcnow()
-         else:
-            dt = datetime.now()
-      else:
-         dt = datetime.utcnow() # Use UTC by default, since this is expected by ADIF.
-
-      (year, month, day) = (dt.year, dt.month, dt.day)
-      (hour, minute) = (dt.hour, dt.minute)
-      # If necessary, add on leading zeros so the YYYYMMDD and HHMM format is followed.
-      if(month < 10):
-         month = "0" + str(month) # Note: Unlike the calendar widget, the months start from an index of 1 here.
-      if(day < 10):
-         day = "0" + str(day)
-      if(hour < 10):
-         hour = "0" + str(hour)
-      if(minute < 10):
-         minute = "0" + str(minute)
-      date = str(year) + str(month) + str(day)
-      time = str(hour) + str(minute)
-      self.sources["QSO_DATE"].set_text(date)
-      self.sources["TIME_ON"].set_text(time)
-      
-      return
+        if(not details_given):
+            error(parent=self, message="To perform a callsign lookup, please specify your username and password in the Preferences.")
+            return
+
+        # Connect and look up
+        connected = callsign_lookup.connect(username, password)
+        if(connected):
+            full_callsign = self.sources["CALL"].get_text()
+            # Check whether we want to ignore any prefixes (e.g. "IA/") or suffixes "(e.g. "/M") in the callsign
+            # before performing the lookup.
+            if(have_config and config.has_option("records", "ignore_prefix_suffix")):
+                ignore_prefix_suffix = (config.get("records", "ignore_prefix_suffix") == "True")
+            else:
+                ignore_prefix_suffix = True
+
+            fields_and_data = callsign_lookup.lookup(full_callsign, ignore_prefix_suffix=ignore_prefix_suffix)
+            for field_name in list(fields_and_data.keys()):
+                self.sources[field_name].set_text(fields_and_data[field_name])
+        return
+
+    def calendar_callback(self, widget):
+        """ Open up a calendar widget for easy QSO_DATE selection. Return None after the user destroys the dialog. """
+        calendar = CalendarDialog(parent=self)
+        response = calendar.run()
+        if(response == Gtk.ResponseType.OK):
+            date = calendar.get_date()
+            self.sources["QSO_DATE"].set_text(date)
+        calendar.destroy()
+        return
+
+    def set_current_datetime_callback(self, widget=None):
+        """ Insert the current date and time. """
+
+        # Check if a configuration file is present.
+        config = configparser.ConfigParser()
+        have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != [])
+
+        # Do we want to use UTC or the computer's local time?
+        (section, option) = ("records", "use_utc")
+        if(have_config and config.has_option(section, option)):
+            use_utc = (config.get(section, option) == "True")
+            if(use_utc):
+                dt = datetime.utcnow()
+            else:
+                dt = datetime.now()
+        else:
+            dt = datetime.utcnow()  # Use UTC by default, since this is expected by ADIF.
+
+        self.sources["QSO_DATE"].set_text(dt.strftime("%Y%m%d"))
+        self.sources["TIME_ON"].set_text(dt.strftime("%H%M"))
+
+        return
+
 
 class CalendarDialog(Gtk.Dialog):
-   """ A simple dialog containing a Gtk.Calendar widget. Using this ensures the date is in the correct YYYYMMDD format required by ADIF. """ 
-   
-   def __init__(self, parent):
-      logging.debug("Setting up a calendar dialog...")
-      Gtk.Dialog.__init__(self, title="Select Date", parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
-      self.calendar = Gtk.Calendar()
-      self.vbox.add(self.calendar)
-      self.show_all()
-      logging.debug("Calendar dialog ready!")
-      return
-
-   def get_date(self):
-      """ Return the date from the Gtk.Calendar widget in YYYYMMDD format. """      
-      logging.debug("Retrieving the date from the calendar widget...")
-      (year, month, day) = self.calendar.get_date()
-      # If necessary, add on leading zeros so the YYYYMMDD format is followed.
-      if(month + 1 < 10):
-         month = "0" + str(month + 1) # Note: the months start from an index of 0 when retrieved from the calendar widget.
-      else:
-         month = month + 1
-      if(day < 10):
-         day = "0" + str(day)
-      date = str(year) + str(month) + str(day)
-      return date
 
+    """ A simple dialog containing a Gtk.Calendar widget. Using this ensures the date is in the correct YYYYMMDD format required by ADIF. """
+
+    def __init__(self, parent):
+        """ Set up the calendar widget and show it to the user.
+
+        :arg parent: The parent Gtk window/dialog.
+        """
+        logging.debug("Setting up a calendar dialog...")
+        Gtk.Dialog.__init__(self, title="Select Date", parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
+        self.calendar = Gtk.Calendar()
+        self.vbox.add(self.calendar)
+        self.show_all()
+        logging.debug("Calendar dialog ready!")
+        return
+
+    def get_date(self):
+        """ Return the date from the Gtk.Calendar widget in YYYYMMDD format.
+
+        :returns: The date from the calendar in YYYYMMDD format.
+        :rtype: str
+        """
+        logging.debug("Retrieving the date from the calendar widget...")
+        (year, month, day) = self.calendar.get_date()
+        # If necessary, add on leading zeros so the YYYYMMDD format is followed.
+        if(month + 1 < 10):
+            month = "0" + str(month + 1)  # Note: the months start from an index of 0 when retrieved from the calendar widget.
+        else:
+            month += 1
+        if(day < 10):
+            day = "0" + str(day)
+        date = str(year) + str(month) + str(day)
+        return date
diff --git a/pyqso/telnet_connection_dialog.py b/pyqso/telnet_connection_dialog.py
index 8c499b7..347c697 100644
--- a/pyqso/telnet_connection_dialog.py
+++ b/pyqso/telnet_connection_dialog.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,67 +17,77 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject
+from gi.repository import Gtk
 import logging
-import re
-import calendar
+
 
 class TelnetConnectionDialog(Gtk.Dialog):
-   """ A simple dialog through which users can specify host and login information for a Telnet server. 
-   This can be used to connect to DX clusters. """
-   
-   def __init__(self, parent):
-      logging.debug("Setting up the Telnet connection dialog...")
-      
-      Gtk.Dialog.__init__(self, title="New Telnet Connection", parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
-
-      self.sources = {}
-
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label("Host: ", halign=Gtk.Align.START)
-      label.set_width_chars(12)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 6)
-      self.sources["HOST"] = Gtk.Entry()
-      hbox_temp.pack_start(self.sources["HOST"], True, True, 6)
-      self.vbox.pack_start(hbox_temp, False, False, 6)
-
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label("Port: ", halign=Gtk.Align.START)
-      label.set_width_chars(12)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 6)
-      self.sources["PORT"] = Gtk.Entry()
-      hbox_temp.pack_start(self.sources["PORT"], True, True, 6)
-      self.vbox.pack_start(hbox_temp, False, False, 6)
-
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label("Username: ", halign=Gtk.Align.START)
-      label.set_width_chars(12)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 6)
-      self.sources["USERNAME"] = Gtk.Entry()
-      hbox_temp.pack_start(self.sources["USERNAME"], True, True, 6)
-      self.vbox.pack_start(hbox_temp, False, False, 6)
-
-      hbox_temp = Gtk.HBox(spacing=0)
-      label = Gtk.Label("Password: ", halign=Gtk.Align.START)
-      label.set_width_chars(12)
-      label.set_alignment(0, 0.5)
-      hbox_temp.pack_start(label, False, False, 6)
-      self.sources["PASSWORD"] = Gtk.Entry()
-      self.sources["PASSWORD"].set_visibility(False) # Mask the password with the "*" character.
-      hbox_temp.pack_start(self.sources["PASSWORD"], True, True, 6)
-      self.vbox.pack_start(hbox_temp, False, False, 6)
-
-      logging.debug("Telnet connection dialog ready!") 
-
-      self.show_all()
-      return
-
-   def get_connection_info(self):
-      """ Return the host and login information stored in the Gtk.Entry boxes. """
-      logging.debug("Returning Telnet connection information...") 
-      return self.sources
 
+    """ A simple dialog through which users can specify host and login information for a Telnet server.
+    This can be used to connect to DX clusters. """
+
+    def __init__(self, parent):
+        """ Set up and show the Telnet connection dialog to the user.
+
+        :arg parent: The parent Gtk window/dialog.
+        """
+
+        logging.debug("Setting up the Telnet connection dialog...")
+
+        Gtk.Dialog.__init__(self, title="New Telnet Connection", parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
+
+        self.sources = {}
+
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label("Host: ", halign=Gtk.Align.START)
+        label.set_width_chars(12)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 6)
+        self.sources["HOST"] = Gtk.Entry()
+        hbox_temp.pack_start(self.sources["HOST"], True, True, 6)
+        self.vbox.pack_start(hbox_temp, False, False, 6)
+
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label("Port: ", halign=Gtk.Align.START)
+        label.set_width_chars(12)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 6)
+        self.sources["PORT"] = Gtk.Entry()
+        hbox_temp.pack_start(self.sources["PORT"], True, True, 6)
+        self.vbox.pack_start(hbox_temp, False, False, 6)
+
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label("Username: ", halign=Gtk.Align.START)
+        label.set_width_chars(12)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 6)
+        self.sources["USERNAME"] = Gtk.Entry()
+        hbox_temp.pack_start(self.sources["USERNAME"], True, True, 6)
+        self.vbox.pack_start(hbox_temp, False, False, 6)
+
+        hbox_temp = Gtk.HBox(spacing=0)
+        label = Gtk.Label("Password: ", halign=Gtk.Align.START)
+        label.set_width_chars(12)
+        label.set_alignment(0, 0.5)
+        hbox_temp.pack_start(label, False, False, 6)
+        self.sources["PASSWORD"] = Gtk.Entry()
+        self.sources["PASSWORD"].set_visibility(False)  # Mask the password with the "*" character.
+        hbox_temp.pack_start(self.sources["PASSWORD"], True, True, 6)
+        self.vbox.pack_start(hbox_temp, False, False, 6)
+
+        self.sources["BOOKMARK"] = Gtk.CheckButton("Bookmark server details for next time")
+        self.vbox.pack_start(self.sources["BOOKMARK"], False, False, 6)
+
+        logging.debug("Telnet connection dialog ready!")
+
+        self.show_all()
+        return
+
+    def get_connection_info(self):
+        """ Return the host and login information stored in the Gtk.Entry boxes.
 
+        :returns: A dictionary of Telnet connection-related information (username, password, port, host).
+        :rtype: dict
+        """
+        logging.debug("Returning Telnet connection information...")
+        return self.sources
diff --git a/pyqso/toolbar.py b/pyqso/toolbar.py
index 61cb18c..61df885 100644
--- a/pyqso/toolbar.py
+++ b/pyqso/toolbar.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,113 +17,123 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject
+from gi.repository import Gtk
 import logging
 
-class Toolbar(Gtk.HBox):
-   
-   def __init__(self, parent):
-      logging.debug("Setting up the toolbar...")  
- 
-      Gtk.HBox.__init__(self, spacing=2)
-
-      self.buttons = {}
-
-      # Create logbook
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_NEW, Gtk.IconSize.BUTTON)
-      button = Gtk.Button()
-      button.add(icon)
-      button.set_tooltip_text('Create a New Logbook')
-      button.connect("clicked", parent.logbook.new)
-      self.pack_start(button, False, False, 0)
-      self.buttons["NEW_LOGBOOK"] = button
-      
-      # Open logbook
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.BUTTON)
-      button = Gtk.Button()
-      button.add(icon)
-      button.set_tooltip_text('Open an Existing Logbook')
-      button.connect("clicked", parent.logbook.open)
-      self.pack_start(button, False, False, 0)
-      self.buttons["OPEN_LOGBOOK"] = button
-
-      # Close logbook
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.BUTTON)
-      button = Gtk.Button()
-      button.add(icon)
-      button.set_tooltip_text('Close Logbook')
-      button.connect("clicked", parent.logbook.close)
-      self.pack_start(button, False, False, 0)
-      self.buttons["CLOSE_LOGBOOK"] = button
-
-      self.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
-
-      # Add record
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.BUTTON)
-      button = Gtk.Button()
-      button.add(icon)
-      button.set_tooltip_text('Add Record')
-      button.connect("clicked", parent.logbook.add_record_callback)
-      self.pack_start(button, False, False, 0)
-      self.buttons["ADD_RECORD"] = button
-
-      # Edit record
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.BUTTON)
-      button = Gtk.Button()
-      button.add(icon)
-      button.set_tooltip_text('Edit Record')
-      button.connect("clicked", parent.logbook.edit_record_callback, None, None)
-      self.pack_start(button, False, False, 0)
-      self.buttons["EDIT_RECORD"] = button
-
-      # Delete record
-      icon = Gtk.Image()
-      icon.set_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.BUTTON)
-      button = Gtk.Button()
-      button.add(icon)
-      button.set_tooltip_text('Delete Record')
-      button.connect("clicked", parent.logbook.delete_record_callback)
-      self.pack_start(button, False, False, 0)
-      self.buttons["DELETE_RECORD"] = button
-
-      self.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
-
-      # Filter log
-      label = Gtk.Label("Filter by callsign: ")
-      self.pack_start(label, False, False, 0)
-      self.filter_source = Gtk.Entry()
-      self.filter_source.set_width_chars(11)
-      self.filter_source.connect_after("changed", parent.logbook.filter_logs)
-      self.pack_start(self.filter_source, False, False, 0)
-
-      self.set_logbook_button_sensitive(True)
-      self.set_record_buttons_sensitive(False)
-
-      self.filter_source.set_sensitive(False)
-
-      logging.debug("Toolbar ready!") 
-
-      return
-
-   def set_logbook_button_sensitive(self, sensitive):
-      logging.debug("Setting the 'Create/Open Logbook' toolbar item's sensitivity to: %s..." % sensitive) 
-      self.buttons["NEW_LOGBOOK"].set_sensitive(sensitive)
-      self.buttons["OPEN_LOGBOOK"].set_sensitive(sensitive)
-      self.buttons["CLOSE_LOGBOOK"].set_sensitive(not sensitive)
-      logging.debug("Set the 'Create/Open Logbook' toolbar item's sensitivity to: %s." % sensitive) 
-      return
-
-   def set_record_buttons_sensitive(self, sensitive):
-      logging.debug("Setting record-related menu item sensitivity to: %s..." % sensitive) 
-      for button_name in ["ADD_RECORD", "EDIT_RECORD", "DELETE_RECORD"]:
-         self.buttons[button_name].set_sensitive(sensitive)
-      logging.debug("Set record-related menu item sensitivity to: %s." % sensitive) 
-      return
-
 
+class Toolbar(Gtk.HBox):
 
+    """ The toolbar underneath the menu bar. """
+
+    def __init__(self, parent):
+        """ Set up the various buttons in the toolbar, and connect to their corresponding functions. """
+
+        logging.debug("Setting up the toolbar...")
+
+        Gtk.HBox.__init__(self, spacing=2)
+
+        self.buttons = {}
+
+        # Create logbook
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_NEW, Gtk.IconSize.BUTTON)
+        button = Gtk.Button()
+        button.add(icon)
+        button.set_tooltip_text('Create a New Logbook')
+        button.connect("clicked", parent.logbook.new)
+        self.pack_start(button, False, False, 0)
+        self.buttons["NEW_LOGBOOK"] = button
+
+        # Open logbook
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.BUTTON)
+        button = Gtk.Button()
+        button.add(icon)
+        button.set_tooltip_text('Open an Existing Logbook')
+        button.connect("clicked", parent.logbook.open)
+        self.pack_start(button, False, False, 0)
+        self.buttons["OPEN_LOGBOOK"] = button
+
+        # Close logbook
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.BUTTON)
+        button = Gtk.Button()
+        button.add(icon)
+        button.set_tooltip_text('Close Logbook')
+        button.connect("clicked", parent.logbook.close)
+        self.pack_start(button, False, False, 0)
+        self.buttons["CLOSE_LOGBOOK"] = button
+
+        self.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
+
+        # Add record
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.BUTTON)
+        button = Gtk.Button()
+        button.add(icon)
+        button.set_tooltip_text('Add Record')
+        button.connect("clicked", parent.logbook.add_record_callback)
+        self.pack_start(button, False, False, 0)
+        self.buttons["ADD_RECORD"] = button
+
+        # Edit record
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.BUTTON)
+        button = Gtk.Button()
+        button.add(icon)
+        button.set_tooltip_text('Edit Record')
+        button.connect("clicked", parent.logbook.edit_record_callback, None, None)
+        self.pack_start(button, False, False, 0)
+        self.buttons["EDIT_RECORD"] = button
+
+        # Delete record
+        icon = Gtk.Image()
+        icon.set_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.BUTTON)
+        button = Gtk.Button()
+        button.add(icon)
+        button.set_tooltip_text('Delete Record')
+        button.connect("clicked", parent.logbook.delete_record_callback)
+        self.pack_start(button, False, False, 0)
+        self.buttons["DELETE_RECORD"] = button
+
+        self.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
+
+        # Filter log
+        label = Gtk.Label("Filter by callsign: ")
+        self.pack_start(label, False, False, 0)
+        self.filter_source = Gtk.Entry()
+        self.filter_source.set_width_chars(11)
+        self.filter_source.connect_after("changed", parent.logbook.filter_logs)
+        self.pack_start(self.filter_source, False, False, 0)
+
+        self.set_logbook_button_sensitive(True)
+        self.set_record_buttons_sensitive(False)
+
+        self.filter_source.set_sensitive(False)
+
+        logging.debug("Toolbar ready!")
+
+        return
+
+    def set_logbook_button_sensitive(self, sensitive):
+        """ Enable/disable logbook-related toolbar items.
+
+        :arg bool sensitive: If True, enable the 'new logbook' and 'open logbook' toolbar items. If False, disable them.
+        """
+        logging.debug("Setting logbook-related toolbar item sensitivity to: %s..." % sensitive)
+        self.buttons["NEW_LOGBOOK"].set_sensitive(sensitive)
+        self.buttons["OPEN_LOGBOOK"].set_sensitive(sensitive)
+        self.buttons["CLOSE_LOGBOOK"].set_sensitive(not sensitive)
+        logging.debug("Set logbook-related toolbar item sensitivity to: %s." % sensitive)
+        return
+
+    def set_record_buttons_sensitive(self, sensitive):
+        """ Enable/disable record-related toolbar items.
+
+        :arg bool sensitive: If True, enable all the record-related toolbar items. If False, disable them all.
+        """
+        logging.debug("Setting record-related toolbar item sensitivity to: %s..." % sensitive)
+        for button_name in ["ADD_RECORD", "EDIT_RECORD", "DELETE_RECORD"]:
+            self.buttons[button_name].set_sensitive(sensitive)
+        logging.debug("Set record-related toolbar item sensitivity to: %s." % sensitive)
+        return
diff --git a/pyqso/toolbox.py b/pyqso/toolbox.py
index 49241b0..f28bc9c 100644
--- a/pyqso/toolbox.py
+++ b/pyqso/toolbox.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -17,47 +17,50 @@
 #    You should have received a copy of the GNU General Public License
 #    along with PyQSO.  If not, see <http://www.gnu.org/licenses/>.
 
-from gi.repository import Gtk, GObject
+from gi.repository import Gtk
 import logging
 
 from pyqso.dx_cluster import *
 from pyqso.grey_line import *
 from pyqso.awards import *
 
+
 class Toolbox(Gtk.Frame):
-   """ Contains a Gtk.Notebook full of amateur radio-related tools. """   
 
-   def __init__(self, parent):
-      logging.debug("Setting up the toolbox...")
-         
-      Gtk.Frame.__init__(self)
-      self.set_label("Toolbox")
-      self.parent = parent
+    """ Contains a Gtk.Notebook full of amateur radio-related tools. """
+
+    def __init__(self, parent):
+        """ Instantiate and insert the various tools into the toolbox. """
+
+        logging.debug("Setting up the toolbox...")
 
-      self.tools = Gtk.Notebook()
+        Gtk.Frame.__init__(self)
+        self.set_label("Toolbox")
+        self.parent = parent
 
-      self.dx_cluster = DXCluster(self.parent)
-      self.tools.insert_page(self.dx_cluster, Gtk.Label("DX Cluster"), 0)
-      self.grey_line = GreyLine(self.parent)
-      self.tools.insert_page(self.grey_line, Gtk.Label("Grey Line"), 1)
-      self.awards = Awards(self.parent)
-      self.tools.insert_page(self.awards, Gtk.Label("Awards"), 2)
+        self.tools = Gtk.Notebook()
 
-      self.add(self.tools)
-      self.tools.connect_after("switch-page", self._on_switch_page)
+        self.dx_cluster = DXCluster(self.parent)
+        self.tools.insert_page(self.dx_cluster, Gtk.Label("DX Cluster"), 0)
+        self.grey_line = GreyLine(self.parent)
+        self.tools.insert_page(self.grey_line, Gtk.Label("Grey Line"), 1)
+        self.awards = Awards(self.parent)
+        self.tools.insert_page(self.awards, Gtk.Label("Awards"), 2)
 
-      logging.debug("Toolbox ready!")
+        self.add(self.tools)
+        self.tools.connect_after("switch-page", self._on_switch_page)
 
-      return
+        logging.debug("Toolbox ready!")
 
-   def toggle_visible_callback(self, widget=None):
-      """ Show/hide the toolbox. """
-      self.set_visible(not self.get_visible())
-      return
+        return
 
-   def _on_switch_page(self, widget, label, new_page):
-      """ Re-draw the Grey Line if the user switches to the grey line tab. """
-      if(type(label) == GreyLine):
-         label.draw() # Note that 'label' is actually a GreyLine object.
-      return
+    def toggle_visible_callback(self, widget=None):
+        """ Show/hide the toolbox. """
+        self.set_visible(not self.get_visible())
+        return
 
+    def _on_switch_page(self, widget, label, new_page):
+        """ Re-draw the Grey Line if the user switches to the grey line tab. """
+        if(isinstance(label, GreyLine)):
+            label.draw()  # Note that 'label' is actually a GreyLine object.
+        return
diff --git a/setup.py b/setup.py
index a8884dd..74ff52a 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-#    Copyright (C) 2013 Christian T. Jacobs.
+#    Copyright (C) 2013-2016 Christian T. Jacobs.
 
 #    This file is part of PyQSO.
 
@@ -20,14 +20,21 @@
 from distutils.core import setup
 
 setup(name='PyQSO',
-      version='0.2',
+      version='0.3',
       description='A contact logging tool for amateur radio operators.',
-      author='Christian T. Jacobs',
-      author_email='c.jacobs10 at imperial.ac.uk',
+      author='Christian T. Jacobs (2E0ICL)',
+      author_email='christian at christianjacobs.uk',
       url='https://github.com/ctjacobs/pyqso',
       packages=['pyqso'],
-      package_dir = {'pyqso': 'pyqso'},
+      package_dir={'pyqso': 'pyqso'},
       scripts=["bin/pyqso"],
-      data_files=[("icons", ["icons/log_64x64.png"])]
-     )
-
+      data_files=[("icons", ["icons/log_64x64.png"])],
+      classifiers=[
+          'Development Status :: 5 - Production/Stable',
+          'Intended Audience :: End Users/Desktop',
+          'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
+          'Natural Language :: English',
+          'Programming Language :: Python :: 3',
+          'Topic :: Communications :: Ham Radio',
+      ]
+      )
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..8a5f797
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,3 @@
+[flake8]
+ignore = E501,F403,E226,E402,W503
+exclude = .git,__pycache__,build

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-hamradio/pyqso.git



More information about the pkg-hamradio-commits mailing list