diff --git a/bin/pyqso b/bin/pyqso
index 6ea059a..cdd546b 100755
--- a/bin/pyqso
+++ b/bin/pyqso
@@ -44,6 +44,7 @@ sys.path.insert(0, pyqso_path)
from pyqso.adif import *
from pyqso.logbook import *
from pyqso.menu import *
+from pyqso.popup import *
from pyqso.toolbar import *
from pyqso.toolbox import *
from pyqso.preferences_dialog import *
@@ -93,8 +94,9 @@ class PyQSO:
self.logbook = Logbook(self)
self.toolbox = Toolbox(self)
- # Set up menu and tool bars. These classes depend on the Logbook and Toolbox class.
+ # Set up menu and toolbar. These classes depend on the Logbook and Toolbox class.
self.menu = Menu(self)
+ self.popup = Popup(self)
self.toolbar = Toolbar(self)
self.window.show_all()
diff --git a/pyqso/grey_line.py b/pyqso/grey_line.py
index 9dd3d88..c08bfa3 100644
--- a/pyqso/grey_line.py
+++ b/pyqso/grey_line.py
@@ -38,6 +38,30 @@ 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
+try:
+ import geocoder
+ have_geocoder = True
+except ImportError:
+ logging.warning("Could not import the geocoder module!")
+ have_geocoder = False
+
+
+class Point:
+ """ A point on the grey line map. """
+ def __init__(self, name, latitude, longitude, style="yo"):
+ """ Set up the point's attributes.
+
+ :arg str name: The name that identifies the point.
+ :arg float latitude: The latitude of the point on the map.
+ :arg float longitude: The longitude of the point on the map.
+ :arg str style: The style of the point when plotted. By default it is a filled yellow circle.
+ """
+
+ self.name = name
+ self.latitude = latitude
+ self.longitude = longitude
+ self.style = style
+ return
class GreyLine:
@@ -53,22 +77,7 @@ class GreyLine:
self.application = application
self.builder = self.application.builder
-
- # Get the QTH coordinates, if available.
- config = configparser.ConfigParser()
- have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != [])
- (section, option) = ("general", "show_qth")
- self.show_qth = False
- if(have_config and config.has_option(section, option)):
- if(config.getboolean(section, option)):
- self.show_qth = True
- try:
- self.qth_name = config.get("general", "qth_name")
- self.qth_latitude = float(config.get("general", "qth_latitude"))
- self.qth_longitude = float(config.get("general", "qth_longitude"))
- except ValueError:
- logging.warning("Unable to get the QTH name, latitude and/or longitude. The QTH will not be pinpointed on the grey line map. Check preferences?")
- self.show_qth = False
+ self.points = []
if(have_necessary_modules):
self.fig = matplotlib.figure.Figure()
@@ -76,12 +85,63 @@ class GreyLine:
self.builder.get_object("greyline").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).
+ # Plot the QTH coordinates, if available.
+ config = configparser.ConfigParser()
+ have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != [])
+ (section, option) = ("general", "show_qth")
+ if(have_config and config.has_option(section, option)):
+ if(config.getboolean(section, option)):
+ try:
+ qth_name = config.get("general", "qth_name")
+ qth_latitude = float(config.get("general", "qth_latitude"))
+ qth_longitude = float(config.get("general", "qth_longitude"))
+ self.add_point(qth_name, qth_latitude, qth_longitude, "ro")
+ except ValueError:
+ logging.warning("Unable to get the QTH name, latitude and/or longitude. The QTH will not be pinpointed on the grey line map. Check preferences?")
+
self.builder.get_object("greyline").show_all()
logging.debug("Grey line ready!")
return
+ def add_point(self, name, latitude, longitude, style="yo"):
+ """ Add a point and re-draw the map.
+
+ :arg str name: The name that identifies the point.
+ :arg float latitude: The latitude of the point on the map.
+ :arg float longitude: The longitude of the point on the map.
+ :arg str style: The style of the point when plotted. By default it is a filled yellow circle.
+ """
+ p = Point(name, latitude, longitude, style)
+ self.points.append(p)
+ self.draw()
+ return
+
+ def pinpoint(self, r):
+ """ Pinpoint the location of a QSO on the grey line map based on the COUNTRY field.
+
+ :arg r: The QSO record containing the location to pinpoint.
+ """
+
+ if(have_geocoder):
+ country = r["COUNTRY"]
+ callsign = r["CALL"]
+
+ # Get the latitude-longitude coordinates of the country.
+ if(country):
+ try:
+ g = geocoder.google(country)
+ latitude, longitude = g.latlng
+ logging.debug("QTH coordinates found: (%s, %s)", str(latitude), str(longitude))
+ self.add_point(callsign, latitude, longitude)
+ except ValueError:
+ logging.exception("Unable to lookup QTH coordinates.")
+ except Exception:
+ logging.exception("Unable to lookup QTH coordinates. Check connection to the internets? Lookup limit reached?")
+
+ return
+
def draw(self):
""" Draw the world map and the grey line on top of it.
@@ -113,11 +173,12 @@ class GreyLine:
m.nightshade(datetime.utcnow()) # Add in the grey line using UTC time. Note that this requires NetCDF.
logging.debug("Grey line drawn.")
- # Pin-point QTH on the map.
- if(self.show_qth):
- qth_x, qth_y = m(self.qth_longitude, self.qth_latitude)
- m.plot(qth_x, qth_y, "ro")
- sub.text(qth_x+0.015*qth_x, qth_y+0.015*qth_y, self.qth_name, color="white", size="medium", weight="bold")
+ # Plot points on the map.
+ if(self.points):
+ for p in self.points:
+ x, y = m(p.longitude, p.latitude)
+ m.plot(x, y, p.style)
+ sub.text(x+0.01*x, y+0.01*y, p.name, color="white", size="small", weight="bold")
return True
else:
diff --git a/pyqso/logbook.py b/pyqso/logbook.py
index 5bc725c..0b7ef61 100644
--- a/pyqso/logbook.py
+++ b/pyqso/logbook.py
@@ -247,6 +247,14 @@ class Logbook:
self.application.menu.set_record_items_sensitive(True)
return
+ def on_button_release_event(self, treeview, event):
+ """ Show a popup menu when the user right-clicks a record in the logbook. """
+
+ if(event.button == 3):
+ self.application.popup.menu.popup(None, None, None, None, event.button, event.time)
+ self.application.popup.menu.show_all()
+ return True
+
def new_log(self, widget=None):
""" Create a new log in the logbook. """
@@ -382,8 +390,10 @@ class Logbook:
self.treeview.append(Gtk.TreeView(model=self.sorter[index]))
self.treeview[index].set_grid_lines(Gtk.TreeViewGridLines.BOTH)
self.treeview[index].connect("row-activated", self.edit_record_callback)
+ self.treeview[index].connect("button-release-event", self.on_button_release_event)
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)
@@ -643,12 +653,14 @@ class Logbook:
def export_log_adif(self, widget=None):
""" Export the log (that is currently selected) to an ADIF file. """
- page_index = self.notebook.get_current_page() # Get 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!")
+ # Get the index of the selected tab in the logbook.
+ try:
+ log_index = self.get_log_index()
+ if(log_index is None):
+ raise ValueError("The log index could not be determined. Perhaps the Summary page is selected?")
+ except ValueError as e:
+ error(parent=self.application.window, message=e)
return
-
- log_index = self.get_log_index()
log = self.logs[log_index]
dialog = Gtk.FileChooserDialog("Export Log as ADIF",
@@ -703,12 +715,14 @@ class Logbook:
def export_log_cabrillo(self, widget=None):
""" Export the log (that is currently selected) to a Cabrillo file. """
- page_index = self.notebook.get_current_page() # Get 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!")
+ # Get the index of the selected tab in the logbook.
+ try:
+ log_index = self.get_log_index()
+ if(log_index is None):
+ raise ValueError("The log index could not be determined. Perhaps the Summary page is selected?")
+ except ValueError as e:
+ error(parent=self.application.window, message=e)
return
-
- log_index = self.get_log_index()
log = self.logs[log_index]
dialog = Gtk.FileChooserDialog("Export Log as Cabrillo",
@@ -775,11 +789,14 @@ class Logbook:
""" 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.notebook.get_current_page() # Get 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!")
+ # Get the index of the selected tab in the logbook.
+ try:
+ log_index = self.get_log_index()
+ if(log_index is None):
+ raise ValueError("The log index could not be determined. Perhaps the Summary page is selected?")
+ except ValueError as e:
+ error(parent=self.application.window, message=e)
return
- log_index = self.get_log_index()
log = self.logs[log_index]
# Retrieve the records.
@@ -798,7 +815,7 @@ class Logbook:
def add_record_callback(self, widget):
""" A callback function used to add a particular record/QSO. """
- # Get the log index.
+ # Get the index of the selected tab in the logbook.
try:
log_index = self.get_log_index()
if(log_index is None):
@@ -1034,6 +1051,23 @@ class Logbook:
return
+ def pinpoint_callback(self, widget=None, path=None):
+ """ A callback function used to pinpoint the callsign on the grey line map. """
+
+ try:
+ log_index = self.get_log_index()
+ row_index = self.get_record_index()
+ if(log_index is None or row_index is None):
+ raise ValueError("Could not determine the log and/or record index.")
+ r = self.logs[log_index].get_record_by_index(row_index)
+ except ValueError as e:
+ logging.error(e)
+ return
+
+ self.application.toolbox.grey_line.pinpoint(r)
+
+ return
+
@property
def log_count(self):
""" Return the total number of logs in the logbook.
@@ -1074,7 +1108,7 @@ class Logbook:
""" 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. Returns None is the log cannot be found.
+ :returns: The index of the named log in the list of Log objects. Returns None if the log cannot be found.
:rtype: int
"""
if(name is None):
@@ -1095,6 +1129,37 @@ class Logbook:
break
return log_index
+ def get_record_index(self):
+ """ Return the index of the currently selected record.
+
+ :returns: The index of the currently selected record in the currently selected log. Returns None if the record or log cannot be found.
+ :rtype: int
+ """
+
+ # Get the index of the selected log.
+ try:
+ log_index = self.get_log_index()
+ if(log_index is None):
+ raise ValueError("The log index could not be determined. Perhaps the Summary page is selected?")
+ except ValueError as e:
+ logging.error(e)
+ return None
+ log = self.logs[log_index]
+
+ # Get the selected row in the log.
+ (sort_model, path) = self.treeselection[log_index].get_selected_rows()
+ 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.error("Could not find the selected row's index!")
+ return None
+
+ return row_index
+
def get_logs(self):
""" Retrieve all the logs in the logbook file, and create Log objects that represent them.
diff --git a/pyqso/popup.py b/pyqso/popup.py
new file mode 100644
index 0000000..37d4ae5
--- /dev/null
+++ b/pyqso/popup.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2018 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 .
+
+
+class Popup:
+
+ """ The popup menu that appears when a QSO record is right-clicked. """
+
+ def __init__(self, application):
+ """ Set up popup menu items. """
+
+ self.application = application
+ self.builder = self.application.builder
+
+ self.menu = self.builder.get_object("popup")
+
+ # Collect Gtk menu items and connect signals.
+ self.items = {}
+
+ # Plot selected QSO on the grey line map.
+ self.items["PINPOINT"] = self.builder.get_object("mitem_pinpoint")
+ self.items["PINPOINT"].connect("activate", self.application.logbook.pinpoint_callback)
+
+ return
diff --git a/pyqso/preferences_dialog.py b/pyqso/preferences_dialog.py
index e93df1a..bfc88da 100644
--- a/pyqso/preferences_dialog.py
+++ b/pyqso/preferences_dialog.py
@@ -287,7 +287,7 @@ class GeneralPage:
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?")
+ error(parent=self.parent, message="Unable to lookup QTH coordinates. Check connection to the internets? Lookup limit reached?")
logging.exception(e)
return
diff --git a/pyqso/res/pyqso.glade b/pyqso/res/pyqso.glade
index d942461..1fa2b57 100644
--- a/pyqso/res/pyqso.glade
+++ b/pyqso/res/pyqso.glade
@@ -4547,6 +4547,18 @@ Base64-encoded plain text in the configuration file.
telnet_connection_ok_button
+
TrueFalse