pyqso/pyqso/preferences_dialog.py

604 wiersze
27 KiB
Python

#!/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 <http://www.gnu.org/licenses/>.
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