#!/usr/bin/env python3 # Copyright (C) 2013-2017 Christian Thomas Jacobs. # 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 # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # PyQSO is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyQSO. If not, see . from gi.repository import Gtk import logging try: import configparser except ImportError: import ConfigParser as configparser import os.path import base64 try: import Hamlib have_hamlib = True except ImportError: logging.warning("Could not import the Hamlib module!") have_hamlib = False try: import geocoder have_geocoder = True except ImportError: logging.warning("Could not import the geocoder module!") have_geocoder = False from pyqso.adif import AVAILABLE_FIELD_NAMES_ORDERED, MODES from pyqso.auxiliary_dialogs import error PREFERENCES_FILE = os.path.expanduser("~/.config/pyqso/preferences.ini") class PreferencesDialog: """ A dialog to specify the PyQSO preferences. """ def __init__(self, application): """ Set up the various pages of the preferences dialog. :arg application: The PyQSO application containing the main Gtk window, etc. """ logging.debug("Setting up the preferences dialog...") self.application = application self.builder = self.application.builder glade_file_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "res", "pyqso.glade") self.builder.add_objects_from_file(glade_file_path, ("preferences_dialog",)) self.dialog = self.builder.get_object("preferences_dialog") self.general = GeneralPage(self.dialog, self.builder) self.view = ViewPage(self.dialog, self.builder) self.records = RecordsPage(self.dialog, self.builder) self.import_export = ImportExportPage(self.dialog, self.builder) self.hamlib = HamlibPage(self.dialog, self.builder) self.world_map = WorldMapPage(self.dialog, self.builder) self.dialog.show_all() logging.debug("Preferences dialog ready!") return def commit(self): """ Commit the user preferences to the configuration file. """ logging.debug("Committing the user preferences to the configuration file...") config = configparser.ConfigParser() # General config.add_section("general") for key in list(self.general.data.keys()): config.set("general", key.lower(), str(self.general.data[key])) # View config.add_section("view") for key in list(self.view.data.keys()): config.set("view", key.lower(), str(self.view.data[key])) # Records config.add_section("records") for key in list(self.records.data.keys()): config.set("records", key.lower(), str(self.records.data[key])) # Import/Export config.add_section("import_export") for key in list(self.import_export.data.keys()): config.set("import_export", key.lower(), str(self.import_export.data[key])) # Hamlib config.add_section("hamlib") for key in list(self.hamlib.data.keys()): config.set("hamlib", key.lower(), str(self.hamlib.data[key])) # World Map config.add_section("world_map") for key in list(self.world_map.data.keys()): config.set("world_map", key.lower(), str(self.world_map.data[key])) # Write the preferences to file. with open(os.path.expanduser(PREFERENCES_FILE), 'w') as f: config.write(f) return class GeneralPage: """ The section of the preferences dialog containing general preferences. """ def __init__(self, parent, builder): """ Set up the General page of the Preferences dialog. """ self.parent = parent self.builder = builder self.sources = {} # 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) != []) # Show toolbox. self.sources["SHOW_TOOLBOX"] = self.builder.get_object("general_show_toolbox_checkbutton") (section, option) = ("general", "show_toolbox") if(have_config and config.has_option(section, option)): self.sources["SHOW_TOOLBOX"].set_active(config.getboolean(section, option)) else: self.sources["SHOW_TOOLBOX"].set_active(False) # Show statistics. self.sources["SHOW_YEARLY_STATISTICS"] = self.builder.get_object("general_show_yearly_statistics_checkbutton") (section, option) = ("general", "show_yearly_statistics") if(have_config and config.has_option(section, option)): self.sources["SHOW_YEARLY_STATISTICS"].set_active(config.getboolean(section, option)) else: self.sources["SHOW_YEARLY_STATISTICS"].set_active(False) # Default logbook. self.sources["DEFAULT_LOGBOOK"] = self.builder.get_object("general_default_logbook_checkbutton") (section, option) = ("general", "default_logbook") if(have_config and config.has_option(section, option)): self.sources["DEFAULT_LOGBOOK"].set_active(config.getboolean(section, option)) else: self.sources["DEFAULT_LOGBOOK"].set_active(False) self.sources["DEFAULT_LOGBOOK"].connect("toggled", self.on_default_logbook_toggled) self.sources["DEFAULT_LOGBOOK_PATH"] = self.builder.get_object("general_default_logbook_entry") (section, option) = ("general", "default_logbook") # Disable the text entry box if the default logbook checkbox is not checked. if(have_config and config.has_option(section, option)): self.sources["DEFAULT_LOGBOOK_PATH"].set_sensitive(self.sources["DEFAULT_LOGBOOK"].get_active()) self.builder.get_object("general_default_logbook_button").set_sensitive(self.sources["DEFAULT_LOGBOOK"].get_active()) else: self.sources["DEFAULT_LOGBOOK_PATH"].set_sensitive(False) self.builder.get_object("general_default_logbook_button").set_sensitive(False) (section, option) = ("general", "default_logbook_path") if(have_config and config.has_option(section, option)): self.sources["DEFAULT_LOGBOOK_PATH"].set_text(config.get(section, option)) self.builder.get_object("general_default_logbook_button").connect("clicked", self.on_default_logbook_clicked) # Keep 'Add Record' dialog open. self.sources["KEEP_OPEN"] = self.builder.get_object("general_keep_open_checkbutton") (section, option) = ("general", "keep_open") if(have_config and config.has_option(section, option)): self.sources["KEEP_OPEN"].set_active(config.getboolean(section, option)) else: self.sources["KEEP_OPEN"].set_active(False) return @property def data(self): """ User preferences regarding General settings. """ data = {} data["SHOW_TOOLBOX"] = self.sources["SHOW_TOOLBOX"].get_active() data["SHOW_YEARLY_STATISTICS"] = self.sources["SHOW_YEARLY_STATISTICS"].get_active() data["DEFAULT_LOGBOOK"] = self.sources["DEFAULT_LOGBOOK"].get_active() data["DEFAULT_LOGBOOK_PATH"] = os.path.expanduser(self.sources["DEFAULT_LOGBOOK_PATH"].get_text()) data["KEEP_OPEN"] = self.sources["KEEP_OPEN"].get_active() return data def on_default_logbook_toggled(self, widget, data=None): if(widget.get_active()): self.sources["DEFAULT_LOGBOOK_PATH"].set_sensitive(True) self.builder.get_object("general_default_logbook_button").set_sensitive(True) else: self.sources["DEFAULT_LOGBOOK_PATH"].set_sensitive(False) self.builder.get_object("general_default_logbook_button").set_sensitive(False) return def on_default_logbook_clicked(self, widget): """ Let the user select the default logbook file via a file chooser dialog, and set the path in the adjacent entry box. """ dialog = Gtk.FileChooserDialog("Select 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() self.sources["DEFAULT_LOGBOOK_PATH"].set_text(path) dialog.destroy() return class ViewPage: """ The section of the preferences dialog containing view-related preferences. """ def __init__(self, parent, builder): """ Set up the View page of the Preferences dialog. """ self.parent = parent self.builder = builder self.sources = {} config = configparser.ConfigParser() have_config = (config.read(PREFERENCES_FILE) != []) # Visible fields for field_name in AVAILABLE_FIELD_NAMES_ORDERED: self.sources[field_name] = self.builder.get_object("visible_fields_%s" % (field_name.lower())) if(have_config and config.has_option("view", field_name.lower())): self.sources[field_name].set_active(config.getboolean("view", field_name.lower())) else: self.sources[field_name].set_active(True) return @property def data(self): """ User preferences regarding View settings. """ data = {} for field_name in AVAILABLE_FIELD_NAMES_ORDERED: data[field_name] = self.sources[field_name].get_active() return data class RecordsPage: """ The section of the preferences dialog containing record-related preferences. """ def __init__(self, parent, builder): """ Set up the Record page of the Preferences dialog. """ self.parent = parent self.builder = builder self.sources = {} # 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) != []) # Autocomplete self.sources["AUTOCOMPLETE_BAND"] = self.builder.get_object("records_autocomplete_band_checkbutton") (section, option) = ("records", "autocomplete_band") if(have_config and config.has_option(section, option)): self.sources["AUTOCOMPLETE_BAND"].set_active(config.getboolean(section, option)) else: self.sources["AUTOCOMPLETE_BAND"].set_active(True) self.sources["USE_UTC"] = self.builder.get_object("records_autocomplete_utc_checkbutton") (section, option) = ("records", "use_utc") if(have_config and config.has_option(section, option)): self.sources["USE_UTC"].set_active(config.getboolean(section, option)) else: self.sources["USE_UTC"].set_active(True) # Default values # Mode self.sources["DEFAULT_MODE"] = self.builder.get_object("default_values_mode_combo") 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) # Submode self.sources["DEFAULT_SUBMODE"] = self.builder.get_object("default_values_submode_combo") 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)) # Power self.sources["DEFAULT_POWER"] = self.builder.get_object("default_values_tx_power_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("") # Frequency unit self.sources["DEFAULT_FREQUENCY_UNIT"] = self.builder.get_object("default_values_frequency_unit_combo") units = ["Hz", "kHz", "MHz", "GHz"] for unit in units: self.sources["DEFAULT_FREQUENCY_UNIT"].append_text(unit) (section, option) = ("records", "default_frequency_unit") if(have_config and config.has_option(section, option)): self.sources["DEFAULT_FREQUENCY_UNIT"].set_active(units.index(config.get(section, option))) else: self.sources["DEFAULT_FREQUENCY_UNIT"].set_active(units.index("MHz")) # Callsign lookup self.sources["CALLSIGN_DATABASE"] = self.builder.get_object("callsign_lookup_database_combo") 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("")) # Login details self.sources["CALLSIGN_DATABASE_USERNAME"] = self.builder.get_object("callsign_lookup_login_details_username_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)) self.sources["CALLSIGN_DATABASE_PASSWORD"] = self.builder.get_object("callsign_lookup_login_details_password_entry") (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) self.sources["IGNORE_PREFIX_SUFFIX"] = self.builder.get_object("callsign_lookup_ignore_prefix_suffix_checkbutton") (section, option) = ("records", "ignore_prefix_suffix") if(have_config and config.has_option(section, option)): self.sources["IGNORE_PREFIX_SUFFIX"].set_active(config.getboolean(section, option)) else: self.sources["IGNORE_PREFIX_SUFFIX"].set_active(True) return @property def data(self): """ User preferences regarding Records settings. """ 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["DEFAULT_FREQUENCY_UNIT"] = self.sources["DEFAULT_FREQUENCY_UNIT"].get_active_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) self.sources["DEFAULT_SUBMODE"].set_active(MODES[mode].index("")) return class ImportExportPage: """ The section of the preferences dialog containing import/export-related preferences. """ def __init__(self, parent, builder): """ Set up the Import/Export page of the Preferences dialog. """ self.parent = parent self.builder = builder self.sources = {} # 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) != []) # Import self.sources["MERGE_COMMENT"] = self.builder.get_object("adif_import_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)) else: self.sources["MERGE_COMMENT"].set_active(False) return @property def data(self): """ User preferences regarding Import/Export settings. """ data = {} data["MERGE_COMMENT"] = self.sources["MERGE_COMMENT"].get_active() return data class HamlibPage: """ The section of the preferences dialog containing Hamlib-related preferences. """ def __init__(self, parent, builder): """ Set up the Hamlib page of the Preferences dialog. """ self.parent = parent self.builder = builder self.sources = {} config = configparser.ConfigParser() have_config = (config.read(PREFERENCES_FILE) != []) self.sources["AUTOFILL"] = self.builder.get_object("hamlib_support_checkbutton") (section, option) = ("hamlib", "autofill") if(have_config and config.has_option(section, option)): self.sources["AUTOFILL"].set_active(config.getboolean(section, option)) else: self.sources["AUTOFILL"].set_active(False) # 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"] = self.builder.get_object("hamlib_support_model_combo") 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. # Path to rig self.sources["RIG_PATHNAME"] = self.builder.get_object("hamlib_support_path_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)) return @property def data(self): """ User preferences regarding Hamlib settings. """ 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 WorldMapPage: """ The section of the preferences dialog containing World Map preferences. """ def __init__(self, parent, builder): """ Set up the World Map page of the Preferences dialog. """ self.parent = parent self.builder = builder self.sources = {} # 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) != []) # Option to pinpoint QTH on grey line map. self.sources["SHOW_QTH"] = self.builder.get_object("world_map_show_qth_checkbutton") (section, option) = ("world_map", "show_qth") if(have_config and config.has_option(section, option)): self.sources["SHOW_QTH"].set_active(config.getboolean(section, option)) else: self.sources["SHOW_QTH"].set_active(False) self.sources["QTH_NAME"] = self.builder.get_object("world_map_qth_name_entry") button = self.builder.get_object("world_map_qth_lookup") button.connect("clicked", self.lookup_callback) # Uses geocoding to find the latitude-longitude coordinates. self.sources["QTH_LATITUDE"] = self.builder.get_object("world_map_qth_coordinates_latitude_entry") self.sources["QTH_LONGITUDE"] = self.builder.get_object("world_map_qth_coordinates_longitude_entry") (section, option) = ("world_map", "show_qth") # Disable the text entry boxes if the SHOW_QTH checkbox is not checked. if(have_config and config.has_option(section, option)): self.sources["QTH_NAME"].set_sensitive(self.sources["SHOW_QTH"].get_active()) self.sources["QTH_LATITUDE"].set_sensitive(self.sources["SHOW_QTH"].get_active()) self.sources["QTH_LONGITUDE"].set_sensitive(self.sources["SHOW_QTH"].get_active()) button.set_sensitive(self.sources["SHOW_QTH"].get_active()) else: self.sources["QTH_NAME"].set_sensitive(False) self.sources["QTH_LATITUDE"].set_sensitive(False) self.sources["QTH_LONGITUDE"].set_sensitive(False) button.set_sensitive(False) (section, option) = ("world_map", "qth_name") if(have_config and config.has_option(section, option)): self.sources["QTH_NAME"].set_text(config.get(section, option)) (section, option) = ("world_map", "qth_latitude") if(have_config and config.has_option(section, option)): self.sources["QTH_LATITUDE"].set_text(config.get(section, option)) (section, option) = ("world_map", "qth_longitude") if(have_config and config.has_option(section, option)): self.sources["QTH_LONGITUDE"].set_text(config.get(section, option)) self.sources["SHOW_QTH"].connect("toggled", self.on_show_qth_toggled) # Option to show Maidenhead grid squares. self.sources["SHOW_GRID_SQUARES"] = self.builder.get_object("world_map_show_grid_squares_checkbutton") (section, option) = ("world_map", "show_grid_squares") if(have_config and config.has_option(section, option)): self.sources["SHOW_GRID_SQUARES"].set_active(config.getboolean(section, option)) else: self.sources["SHOW_GRID_SQUARES"].set_active(False) # Option to shade in worked Maidenhead grid squares. self.sources["SHADE_WORKED_GRID_SQUARES"] = self.builder.get_object("world_map_shade_worked_grid_squares_checkbutton") (section, option) = ("world_map", "shade_worked_grid_squares") if(have_config and config.has_option(section, option)): self.sources["SHADE_WORKED_GRID_SQUARES"].set_active(config.getboolean(section, option)) else: self.sources["SHADE_WORKED_GRID_SQUARES"].set_active(False) return @property def data(self): """ User preferences regarding World Map settings. """ data = {} data["SHOW_QTH"] = self.sources["SHOW_QTH"].get_active() data["QTH_NAME"] = self.sources["QTH_NAME"].get_text() data["QTH_LATITUDE"] = self.sources["QTH_LATITUDE"].get_text() data["QTH_LONGITUDE"] = self.sources["QTH_LONGITUDE"].get_text() data["SHOW_GRID_SQUARES"] = self.sources["SHOW_GRID_SQUARES"].get_active() data["SHADE_WORKED_GRID_SQUARES"] = self.sources["SHADE_WORKED_GRID_SQUARES"].get_active() return data def on_show_qth_toggled(self, widget, data=None): if(widget.get_active()): self.sources["QTH_NAME"].set_sensitive(True) self.sources["QTH_LATITUDE"].set_sensitive(True) self.sources["QTH_LONGITUDE"].set_sensitive(True) self.builder.get_object("world_map_qth_lookup").set_sensitive(True) else: self.sources["QTH_NAME"].set_sensitive(False) self.sources["QTH_LATITUDE"].set_sensitive(False) self.sources["QTH_LONGITUDE"].set_sensitive(False) self.builder.get_object("world_map_qth_lookup").set_sensitive(False) return def lookup_callback(self, widget=None): """ Perform geocoding of the QTH location to obtain latitude-longitude coordinates. """ if(not have_geocoder): error(parent=self.parent, message="Geocoder module could not be imported. Geocoding aborted.") return logging.debug("Geocoding QTH location...") name = self.sources["QTH_NAME"].get_text() try: g = geocoder.google(name) latitude, longitude = g.latlng self.sources["QTH_LATITUDE"].set_text(str(latitude)) self.sources["QTH_LONGITUDE"].set_text(str(longitude)) logging.debug("QTH coordinates found: (%s, %s)", str(latitude), str(longitude)) except ValueError as e: error(parent=self.parent, message="Unable to lookup QTH coordinates. Is the QTH name correct?") logging.exception(e) except Exception as e: error(parent=self.parent, message="Unable to lookup QTH coordinates. Check connection to the internets? Lookup limit reached?") logging.exception(e) return