diff --git a/bin/pyqso b/bin/pyqso
index 71b68d1..49c6b8c 100755
--- a/bin/pyqso
+++ b/bin/pyqso
@@ -85,6 +85,7 @@ class PyQSO:
# Kills the application if the close button is clicked on the main window itself.
self.window.connect("delete-event", Gtk.main_quit)
+ # Status bar.
self.statusbar = self.builder.get_object("statusbar")
context_id = self.statusbar.get_context_id("Status")
self.statusbar.push(context_id, "No logbook is currently open.")
diff --git a/pyqso/adif.py b/pyqso/adif.py
index 2869f2a..4655a28 100644
--- a/pyqso/adif.py
+++ b/pyqso/adif.py
@@ -27,6 +27,8 @@ except ImportError:
import ConfigParser as configparser
from os.path import expanduser
+from pyqso.modes import Modes
+
# ADIF field names and their associated data types available in PyQSO.
AVAILABLE_FIELD_NAMES_TYPES = {"CALL": "S",
"QSO_DATE": "D",
@@ -98,98 +100,6 @@ AVAILABLE_FIELD_NAMES_FRIENDLY = {"CALL": "Callsign",
# E: Enumerated
DATA_TYPES = ["A", "B", "N", "S", "I", "D", "T", "M", "G", "L", "E"]
-# 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": ("",),
- "FT8": ("",),
- "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", "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.
@@ -207,6 +117,7 @@ class ADIF:
def __init__(self):
""" Initialise class for I/O of files using the Amateur Data Interchange Format (ADIF). """
+ self.modes = Modes()
return
def read(self, path):
@@ -521,9 +432,9 @@ class ADIF:
elif(data_type == "E" or data_type == "A"):
# Enumeration, AwardList.
if(field_name == "MODE"):
- return (data in list(MODES.keys()))
+ return (data in list(self.modes.all.keys()))
elif(field_name == "SUBMODE"):
- submodes = [submode for mode in list(MODES.keys()) for submode in MODES[mode]]
+ submodes = [submode for mode in list(self.modes.all.keys()) for submode in self.modes.all[mode]]
return (data in submodes)
elif(field_name == "BAND"):
return (data in BANDS)
diff --git a/pyqso/logbook.py b/pyqso/logbook.py
index 972e784..a84bc49 100644
--- a/pyqso/logbook.py
+++ b/pyqso/logbook.py
@@ -38,6 +38,7 @@ from pyqso.summary import Summary
from pyqso.blank import Blank
from pyqso.printer import Printer
from pyqso.compare import compare_date_and_time, compare_default
+from pyqso.update_modes_dialog import UpdateModesDialog
class Logbook:
@@ -907,7 +908,7 @@ class Logbook:
return
log = self.logs[log_index]
- (sort_model, path) = self.treeselection[log_index].get_selected_rows() # Get the selected row in the log
+ (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)
@@ -1113,6 +1114,16 @@ class Logbook:
return
+ def update_modes_callback(self, widget=None, path=None):
+ umd = UpdateModesDialog(self.application)
+ response = umd.dialog.run()
+ if(response == Gtk.ResponseType.OK):
+ modes = Modes()
+ modes.update(url=umd.url)
+ umd.dialog.destroy()
+ return
+
+
@property
def log_count(self):
""" Return the total number of logs in the logbook.
diff --git a/pyqso/menu.py b/pyqso/menu.py
index fceeb83..1ad858f 100644
--- a/pyqso/menu.py
+++ b/pyqso/menu.py
@@ -109,6 +109,10 @@ class Menu:
self.items["RECORD_COUNT"] = self.builder.get_object("mitem_record_count")
self.items["RECORD_COUNT"].connect("activate", self.application.logbook.record_count_callback)
+ # Record count
+ self.items["UPDATE_MODES"] = self.builder.get_object("mitem_update_modes")
+ self.items["UPDATE_MODES"].connect("activate", self.application.logbook.update_modes_callback)
+
# View toolbox
self.items["TOOLBOX"] = self.builder.get_object("mitem_toolbox")
config = configparser.ConfigParser()
diff --git a/pyqso/modes.py b/pyqso/modes.py
index ac0a07f..ca57325 100644
--- a/pyqso/modes.py
+++ b/pyqso/modes.py
@@ -17,24 +17,192 @@
# You should have received a copy of the GNU General Public License
# along with PyQSO. If not, see .
+import sqlite3
+import os
from urllib.request import urlopen
from bs4 import BeautifulSoup
+import logging
-page = urlopen('http://www.adif.org/307/ADIF_307.htm').read()
-soup = BeautifulSoup(page, "html.parser")
+MODES_FILE = os.path.expanduser("~/.config/pyqso/modes.db")
-# Remove the tags but keep the tags' contents.
-for match in soup.findAll('span'):
- match.unwrap()
-# Find the MODES table.
-rows = soup.find(id="Enumeration_Mode").find_all('tr')
+class Modes:
+
+ def __init__(self):
+
+ try:
+ connection = sqlite3.connect(MODES_FILE)
+ c = connection.cursor()
+ c.execute("""CREATE TABLE IF NOT EXISTS modes (
+ mode TEXT NOT NULL,
+ submode TEXT NOT NULL,
+ UNIQUE(mode, submode)
+ ); """)
+
+ # Fill the new table with the basic list of modes and submodes.
+ for mode in self.basic:
+ for submode in self.basic[mode]:
+ c.execute("""REPLACE INTO modes(mode, submode) VALUES(?, ?)""", (mode, submode))
+ connection.commit()
+ connection.close()
+ except sqlite3.Error as e:
+ logging.exception(e)
+
+ #self.update("http://www.adif.org/309/ADIF_309.htm")
+ return
+
+ @property
+ def basic(self):
+ """ A basic list of 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": ("",),
+ "FT8": ("",),
+ "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.
+ 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(deprecated)
+ return modes
+
+ @property
+ def all(self):
+ try:
+ connection = sqlite3.connect(MODES_FILE)
+ c = connection.cursor()
+ result = c.execute("""SELECT * FROM modes""")
+ rows = result.fetchall()
+
+ modes = {}
+ for row in rows:
+ mode = row[0]
+ submode = row[1]
+ if(mode in modes.keys()):
+ modes[mode].append(submode)
+ else:
+ modes[mode] = [submode]
+ connection.close()
+ except sqlite3.Error as e:
+ logging.exception(e)
+ return modes
+
+ def update(self, url):
+ modes = self.parse(url)
+ try:
+ connection = sqlite3.connect(MODES_FILE)
+ c = connection.cursor()
+ for mode in modes:
+ for submode in modes[mode]:
+ c.execute("REPLACE INTO modes(mode, submode) VALUES(?,?)", (mode, submode))
+ connection.commit()
+ connection.close()
+ except sqlite3.Error as e:
+ logging.exception(e)
+
+ return
+
+ def parse(self, url):
+ page = urlopen(url).read()
+ soup = BeautifulSoup(page, "html.parser")
+
+ # Remove the tags but keep the tags' contents.
+ for match in soup.findAll("span"):
+ match.unwrap()
+
+ # Find the MODES table.
+ rows = soup.find(id="Enumeration_Mode").find_all("tr")
+
+ # Extract modes and submodes.
+ modes = {}
+ for row in rows[1:]: # Ignores the header row.
+ mode, submode = row.find_all("td")[0:2]
+ mode = mode.text.split(" (import-only)")[0].strip()
+ submode = tuple(submode.text.strip().split(", "))
+ if(mode not in modes):
+ modes[mode] = submode
+
+ return modes
-# Extract modes and submodes.
-modes = {}
-for row in rows[1:]:
- mode, submode, description = row.find_all('td')
- mode = mode.text.split(" (import-only)")[0].strip()
- submode = tuple(submode.text.strip().split(", "))
- modes[mode] = submode
-print(modes)
diff --git a/pyqso/preferences_dialog.py b/pyqso/preferences_dialog.py
index 2fc8bf2..29b485d 100644
--- a/pyqso/preferences_dialog.py
+++ b/pyqso/preferences_dialog.py
@@ -38,7 +38,8 @@ except ImportError:
logging.warning("Could not import the geocoder module!")
have_geocoder = False
-from pyqso.adif import AVAILABLE_FIELD_NAMES_ORDERED, MODES
+from pyqso.adif import AVAILABLE_FIELD_NAMES_ORDERED
+from pyqso.modes import Modes
from pyqso.auxiliary_dialogs import error
PREFERENCES_FILE = os.path.expanduser("~/.config/pyqso/preferences.ini")
@@ -289,27 +290,28 @@ class RecordsPage:
# Default values
# Mode
+ self.modes = Modes()
self.sources["DEFAULT_MODE"] = self.builder.get_object("default_values_mode_combo")
- for mode in sorted(MODES.keys()):
+ for mode in sorted(self.modes.all.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"].set_active(sorted(self.modes.all.keys()).index(mode))
self.sources["DEFAULT_MODE"].connect("changed", self.on_mode_changed)
# Submode
self.sources["DEFAULT_SUBMODE"] = self.builder.get_object("default_values_submode_combo")
- for submode in MODES[mode]:
+ for submode in self.modes.all[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))
+ self.sources["DEFAULT_SUBMODE"].set_active(self.modes.all[mode].index(submode))
# Power
self.sources["DEFAULT_POWER"] = self.builder.get_object("default_values_tx_power_entry")
@@ -384,9 +386,9 @@ class RecordsPage:
""" 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]:
+ for submode in self.modes.all[mode]:
self.sources["DEFAULT_SUBMODE"].append_text(submode)
- self.sources["DEFAULT_SUBMODE"].set_active(MODES[mode].index(""))
+ self.sources["DEFAULT_SUBMODE"].set_active(self.modes.all[mode].index(""))
return
@@ -406,8 +408,8 @@ class ImportExportPage:
config = configparser.ConfigParser()
have_config = (config.read(PREFERENCES_FILE) != [])
- # Import
- self.sources["MERGE_COMMENT"] = self.builder.get_object("adif_import_merge_comment_checkbutton")
+ # ADIF
+ self.sources["MERGE_COMMENT"] = self.builder.get_object("adif_merge_comment_checkbutton")
(section, option) = ("import_export", "merge_comment")
if(have_config and config.has_option(section, option)):
self.sources["MERGE_COMMENT"].set_active(config.getboolean(section, option))
diff --git a/pyqso/record_dialog.py b/pyqso/record_dialog.py
index c6f7fc1..40c3f52 100644
--- a/pyqso/record_dialog.py
+++ b/pyqso/record_dialog.py
@@ -104,8 +104,9 @@ class RecordDialog:
self.sources["BAND"].set_active(0) # Set an empty string as the default option.
# MODE
+ self.modes = Modes().all
self.sources["MODE"] = self.builder.get_object("qso_mode_combo")
- for mode in sorted(MODES.keys()):
+ for mode in sorted(self.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)
@@ -199,12 +200,12 @@ class RecordDialog:
converted = self.convert_frequency(data, from_unit="MHz", to_unit=self.frequency_unit)
self.sources[field_names[i]].set_text(str(converted))
elif(field_names[i] == "MODE"):
- self.sources[field_names[i]].set_active(sorted(MODES.keys()).index(data))
+ self.sources[field_names[i]].set_active(sorted(self.modes.keys()).index(data))
# Handle SUBMODE at the same time.
submode_data = record["submode"]
if(submode_data is None):
submode_data = ""
- self.sources["SUBMODE"].set_active(MODES[data].index(submode_data))
+ self.sources["SUBMODE"].set_active(self.modes[data].index(submode_data))
elif(field_names[i] == "SUBMODE"):
# Skip, because this has been (or will be) handled when populating the MODE field.
continue
@@ -227,7 +228,7 @@ class RecordDialog:
mode = config.get(section, option)
else:
mode = ""
- self.sources["MODE"].set_active(sorted(MODES.keys()).index(mode))
+ self.sources["MODE"].set_active(sorted(self.modes.keys()).index(mode))
# Submode
(section, option) = ("records", "default_submode")
@@ -235,7 +236,7 @@ class RecordDialog:
submode = config.get(section, option)
else:
submode = ""
- self.sources["SUBMODE"].set_active(MODES[mode].index(submode))
+ self.sources["SUBMODE"].set_active(self.modes[mode].index(submode))
# Power
(section, option) = ("records", "default_power")
@@ -303,9 +304,9 @@ class RecordDialog:
""" 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()
mode = combo.get_active_text()
- for submode in MODES[mode]:
+ for submode in self.modes[mode]:
self.sources["SUBMODE"].append_text(submode)
- self.sources["SUBMODE"].set_active(MODES[mode].index("")) # Set the submode to an empty string.
+ self.sources["SUBMODE"].set_active(self.modes[mode].index("")) # Set the submode to an empty string.
return
def on_key_press(self, widget, event):
@@ -375,10 +376,10 @@ class RecordDialog:
if(mode == "USB" or mode == "LSB"):
submode = mode
mode = "SSB"
- self.sources["MODE"].set_active(sorted(MODES.keys()).index(mode))
- self.sources["SUBMODE"].set_active(MODES[mode].index(submode))
+ self.sources["MODE"].set_active(sorted(self.modes.keys()).index(mode))
+ self.sources["SUBMODE"].set_active(self.modes[mode].index(submode))
else:
- self.sources["MODE"].set_active(sorted(MODES.keys()).index(mode))
+ self.sources["MODE"].set_active(sorted(self.modes.keys()).index(mode))
except:
logging.error("Could not obtain the current mode (e.g. FM, AM, CW) via Hamlib!")
diff --git a/pyqso/res/pyqso.glade b/pyqso/res/pyqso.glade
index 47c8730..dd6ec56 100644
--- a/pyqso/res/pyqso.glade
+++ b/pyqso/res/pyqso.glade
@@ -1,5 +1,5 @@
-
+
+
+
+
+