Building an amateur radio app for Linux, part 2

In the previous post I showed how to add some icons to an application showing OpenStreetMap, with Python and GTK+. Next, I’ll show how to make a listing of nearest repeaters to selected area. The listing of all repeaters is in the local array, and since there is a .distance(lat,lon) that gives the distance to a point, the list of repeaters can be sorted by closest to a certain point that is selected – in the on_button_release function. The Python Gtk guide shows an example that can be integrated in to the code to add a Listbox. The listbox should be “self.listbox” so various functions can access and change it. Below the other widget/control code, this must be added, within a gtkScrollWindow or adding many items will expand the window awkwardly:

        self.listbox = Gtk.ListBox()

        #listbox.set_selection_mode(Gtk.SelectionMode.NONE)

        scrolled = Gtk.ScrolledWindow()
        scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        scrolled.add(self.listbox)
        self.vbox.pack_start(scrolled, True, True, 0)
        self.GTKListRows = []

One thing you may notice in the docs for GTKListbox is that there is no apparent ability to clear it out and re add items, for when the map center is changed and different nearby repeaters should be listed out. I add “print(dir(self.listbox))” and no clear/removechildren or similar function shows in the .function or .property listing of the object:

['__bool__', '__class__', '__contains__', '__copy__', '__deepcopy__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__gdoc__', '__ge__', '__getattribute__', '__gpointer__', '__grefcount__', '__gsignals__', '__gt__', '__gtype__', '__hash__', '__info__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_force_floating', '_ref', '_ref_sink', '_unref', '_unsupported_data_method', '_unsupported_method', 'activate', 'add', 'add_accelerator', 'add_child', 'add_device_events', 'add_events', 'add_mnemonic_label', 'add_tick_callback', 'bind_property', 'bind_property_full', 'bind_template_callback_full', 'bind_template_child_full', 'can_activate_accel', 'chain', 'changed', 'check_resize', 'child_focus', 'child_get', 'child_get_property', 'child_notify', 'child_notify_by_pspec', 'child_set', 'child_set_property', 'child_type', 'class_path', 'compat_control', 'compute_expand', 'connect', 'connect_after', 'connect_data', 'connect_object', 'connect_object_after', 'construct_child', 'container', 'create_pango_context', 'create_pango_layout', 'custom_finished', 'custom_tag_end', 'custom_tag_start', 'destroy', 'destroyed', 'device_is_shadowed', 'disconnect', 'disconnect_by_func', 'do_activate', 'do_add', 'do_adjust_baseline_allocation', 'do_adjust_baseline_request', 'do_adjust_size_allocation', 'do_adjust_size_request', 'do_button_press_event', 'do_button_release_event', 'do_can_activate_accel', 'do_check_resize', 'do_child_notify', 'do_child_type', 'do_composite_name', 'do_composited_changed', 'do_compute_expand', 'do_configure_event', 'do_damage_event', 'do_delete_event', 'do_destroy', 'do_destroy_event', 'do_direction_changed', 'do_dispatch_child_properties_changed', 'do_drag_begin', 'do_drag_data_delete', 'do_drag_data_get', 'do_drag_data_received', 'do_drag_drop', 'do_drag_end', 'do_drag_failed', 'do_drag_leave', 'do_drag_motion', 'do_draw', 'do_enter_notify_event', 'do_event', 'do_focus', 'do_focus_in_event', 'do_focus_out_event', 'do_forall', 'do_get_accessible', 'do_get_child_property', 'do_get_path_for_child', 'do_get_preferred_height', 'do_get_preferred_height_and_baseline_for_width', 'do_get_preferred_height_for_width', 'do_get_preferred_width', 'do_get_preferred_width_for_height', 'do_get_request_mode', 'do_grab_broken_event', 'do_grab_focus', 'do_grab_notify', 'do_hide', 'do_hierarchy_changed', 'do_key_press_event', 'do_key_release_event', 'do_keynav_failed', 'do_leave_notify_event', 'do_map', 'do_map_event', 'do_mnemonic_activate', 'do_motion_notify_event', 'do_move_focus', 'do_parent_set', 'do_popup_menu', 'do_property_notify_event', 'do_proximity_in_event', 'do_proximity_out_event', 'do_query_tooltip', 'do_queue_draw_region', 'do_realize', 'do_remove', 'do_screen_changed', 'do_scroll_event', 'do_selection_clear_event', 'do_selection_get', 'do_selection_notify_event', 'do_selection_received', 'do_selection_request_event', 'do_set_child_property', 'do_set_focus_child', 'do_show', 'do_show_all', 'do_show_help', 'do_size_allocate', 'do_state_changed', 'do_state_flags_changed', 'do_style_set', 'do_style_updated', 'do_touch_event', 'do_unmap', 'do_unmap_event', 'do_unrealize', 'do_visibility_notify_event', 'do_window_state_event', 'drag_begin', 'drag_begin_with_coordinates', 'drag_check_threshold', 'drag_dest_add_image_targets', 'drag_dest_add_text_targets', 'drag_dest_add_uri_targets', 'drag_dest_find_target', 'drag_dest_get_target_list', 'drag_dest_get_track_motion', 'drag_dest_set', 'drag_dest_set_proxy', 'drag_dest_set_target_list', 'drag_dest_set_track_motion', 'drag_dest_unset', 'drag_get_data', 'drag_highlight', 'drag_source_add_image_targets', 'drag_source_add_text_targets', 'drag_source_add_uri_targets', 'drag_source_get_target_list', 'drag_source_set', 'drag_source_set_icon_gicon', 'drag_source_set_icon_name', 'drag_source_set_icon_pixbuf', 'drag_source_set_icon_stock', 'drag_source_set_target_list', 'drag_source_unset', 'drag_unhighlight', 'draw', 'emit', 'emit_stop_by_name', 'ensure_style', 'error_bell', 'event', 'find_child_property', 'find_property', 'find_style_property', 'forall', 'force_floating', 'foreach', 'freeze_child_notify', 'freeze_notify', 'g_type_instance', 'get_accessible', 'get_action_group', 'get_activatable', 'get_allocated_baseline', 'get_allocated_height', 'get_allocated_width', 'get_allocation', 'get_ancestor', 'get_app_paintable', 'get_border_width', 'get_can_default', 'get_can_focus', 'get_child', 'get_child_requisition', 'get_child_visible', 'get_children', 'get_clip', 'get_clipboard', 'get_composite_name', 'get_data', 'get_default_direction', 'get_default_style', 'get_device_enabled', 'get_device_events', 'get_direction', 'get_display', 'get_double_buffered', 'get_events', 'get_focus_chain', 'get_focus_child', 'get_focus_hadjustment', 'get_focus_vadjustment', 'get_font_map', 'get_font_options', 'get_frame_clock', 'get_halign', 'get_has_tooltip', 'get_has_window', 'get_header', 'get_hexpand', 'get_hexpand_set', 'get_index', 'get_internal_child', 'get_mapped', 'get_margin_bottom', 'get_margin_end', 'get_margin_left', 'get_margin_right', 'get_margin_start', 'get_margin_top', 'get_modifier_mask', 'get_modifier_style', 'get_name', 'get_no_show_all', 'get_opacity', 'get_pango_context', 'get_parent', 'get_parent_window', 'get_path', 'get_path_for_child', 'get_pointer', 'get_preferred_height', 'get_preferred_height_and_baseline_for_width', 'get_preferred_height_for_width', 'get_preferred_size', 'get_preferred_width', 'get_preferred_width_for_height', 'get_properties', 'get_property', 'get_qdata', 'get_realized', 'get_receives_default', 'get_request_mode', 'get_requisition', 'get_resize_mode', 'get_root_window', 'get_scale_factor', 'get_screen', 'get_selectable', 'get_sensitive', 'get_settings', 'get_size_request', 'get_state', 'get_state_flags', 'get_style', 'get_style_context', 'get_support_multidevice', 'get_template_child', 'get_tooltip_markup', 'get_tooltip_text', 'get_tooltip_window', 'get_toplevel', 'get_valign', 'get_valign_with_baseline', 'get_vexpand', 'get_vexpand_set', 'get_visible', 'get_visual', 'get_window', 'grab_add', 'grab_default', 'grab_focus', 'grab_remove', 'handle_border_width', 'handler_block', 'handler_block_by_func', 'handler_disconnect', 'handler_is_connected', 'handler_unblock', 'handler_unblock_by_func', 'has_default', 'has_focus', 'has_grab', 'has_rc_style', 'has_screen', 'has_visible_focus', 'hide', 'hide_on_delete', 'in_destruction', 'init_template', 'input_shape_combine_region', 'insert_action_group', 'install_child_properties', 'install_child_property', 'install_properties', 'install_property', 'install_style_property', 'interface_find_property', 'interface_install_property', 'interface_list_properties', 'intersect', 'is_ancestor', 'is_composited', 'is_drawable', 'is_floating', 'is_focus', 'is_selected', 'is_sensitive', 'is_toplevel', 'is_visible', 'keynav_failed', 'list_accel_closures', 'list_action_prefixes', 'list_child_properties', 'list_mnemonic_labels', 'list_properties', 'list_style_properties', 'map', 'mnemonic_activate', 'modify_base', 'modify_bg', 'modify_cursor', 'modify_fg', 'modify_font', 'modify_style', 'modify_text', 'new', 'notify', 'notify_by_pspec', 'override_background_color', 'override_color', 'override_cursor', 'override_font', 'override_property', 'override_symbolic_color', 'parent_instance', 'parser_finished', 'path', 'pop_composite_child', 'priv', 'propagate_draw', 'props', 'push_composite_child', 'qdata', 'queue_compute_expand', 'queue_draw', 'queue_draw_area', 'queue_draw_region', 'queue_resize', 'queue_resize_no_redraw', 'realize', 'ref', 'ref_count', 'ref_sink', 'region_intersect', 'register_window', 'remove', 'remove_accelerator', 'remove_mnemonic_label', 'remove_tick_callback', 'render_icon', 'render_icon_pixbuf', 'reparent', 'replace_data', 'replace_qdata', 'reset_rc_styles', 'reset_style', 'resize_children', 'run_dispose', 'send_expose', 'send_focus_change', 'set_accel_path', 'set_accessible_role', 'set_accessible_type', 'set_activatable', 'set_allocation', 'set_app_paintable', 'set_border_width', 'set_buildable_property', 'set_can_default', 'set_can_focus', 'set_child_visible', 'set_clip', 'set_composite_name', 'set_connect_func', 'set_data', 'set_default_direction', 'set_device_enabled', 'set_device_events', 'set_direction', 'set_double_buffered', 'set_events', 'set_focus_chain', 'set_focus_child', 'set_focus_hadjustment', 'set_focus_vadjustment', 'set_font_map', 'set_font_options', 'set_halign', 'set_has_tooltip', 'set_has_window', 'set_header', 'set_hexpand', 'set_hexpand_set', 'set_mapped', 'set_margin_bottom', 'set_margin_end', 'set_margin_left', 'set_margin_right', 'set_margin_start', 'set_margin_top', 'set_name', 'set_no_show_all', 'set_opacity', 'set_parent', 'set_parent_window', 'set_properties', 'set_property', 'set_realized', 'set_reallocate_redraws', 'set_receives_default', 'set_redraw_on_allocate', 'set_resize_mode', 'set_selectable', 'set_sensitive', 'set_size_request', 'set_state', 'set_state_flags', 'set_style', 'set_support_multidevice', 'set_template', 'set_template_from_resource', 'set_tooltip_markup', 'set_tooltip_text', 'set_tooltip_window', 'set_valign', 'set_vexpand', 'set_vexpand_set', 'set_visible', 'set_visual', 'set_window', 'shape_combine_region', 'show', 'show_all', 'show_now', 'size_allocate', 'size_allocate_with_baseline', 'size_request', 'steal_data', 'steal_qdata', 'stop_emission', 'stop_emission_by_name', 'style_attach', 'style_get_property', 'thaw_child_notify', 'thaw_notify', 'translate_coordinates', 'trigger_tooltip_query', 'unmap', 'unparent', 'unrealize', 'unref', 'unregister_window', 'unset_focus_chain', 'unset_state_flags', 'watch_closure', 'weak_ref', 'widget']

However, every widget has a .destroy() that can remove it. So to clear it, just have an array holding each element that currently exists, the self.GTKListRows class variable set empty above.

Note that to set some of the labels bigger there is a modify_font feature to call to change the label, but it is different as you can see by exploring in Python3 with its tab completion:

>>> from gi.repository import Pango
>>> Pango.font[tab]

and it completes as Pango.font_description_from_string(.

You can see the details for this in Pango docs. font weight and size are things that can be changed. After trying various sizes I decided on this for an addToList that adds it to the listbox:

    def addToList(self, repeater, lat, lon):
        row = Gtk.ListBoxRow()
        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
        row.add(hbox)
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        hbox.pack_start(vbox, True, True, 0)
        innerhbox = Gtk.HBox()
        if float(repeater.freq) == 0:
            label1 = Gtk.Label("Node %s Reflector %s" % (repeater.node, repeater.callsign), xalign=0)
        else:
            label1 = Gtk.Label("Node %s, %s at %smhz" % (repeater.node, repeater.callsign, repeater.freq), xalign=0)
        try:
            int(repeater.status)
            for item in self.irlps:
                if item.node == repeater.status:
                    gothere = Gtk.Button("Linked to "+item.node)
                    innerhbox.pack_start(gothere, False, False, 0)
                    gothere.connect('clicked', self.goLinkIRLP)
            label2 = Gtk.Label("PL %s, Offset %s, %s" % (repeater.pl, repeater.offset, repeater.url), xalign=0)
        except ValueError:
            #Not connected to number node
            if float(repeater.freq) == 0:
                label2 = Gtk.Label(repeater.city, xalign=0)
            else:
                label2 = Gtk.Label("%s. PL %s, Offset %s, %s" % (repeater.status, repeater.pl, repeater.offset, repeater.url), xalign=0)
        if float(repeater.freq) == 0:
            label3 = Gtk.Label("Owned by %s" % (repeater.owner,), xalign=0)
        else:
            label3 = Gtk.Label("Owned by %s in %s" % (repeater.owner, repeater.city), xalign=0)
        innerhbox.pack_start(label2, True, True, 0)
        label1.modify_font(Pango.font_description_from_string("Ubuntu Bold 22"))
        vbox.pack_start(label1, True, True, 5)
        vbox.pack_start(innerhbox, True, True, 0)
        vbox.pack_start(label3, True, True, 5)
        km = repeater.distance(lat,lon)
        if self.unit == 'mi':
            km = km*.62137119
        distlbl = Gtk.Label( '%s %s ' % ( int(km*10)/10, self.unit ))
        hbox.pack_start(distlbl, False, True, 0)
        
        self.GTKListRows.append(row)
        self.listbox.add(row)

And note the case of a linked button, for when an IRLP repeater is linked to another IRLP repeater – this allows linking using the api:

    def goLinkIRLP(self, btn):
        label = btn.get_label()
        print(label)
        label = label.replace('Linked to ','')
        for item in self.irlps:
            if item.node == label:
                self.osm.set_center(item.lat, item.lon)

The addToList function is called to add one item to the list box, when all of the previous ones have been removed, by calling destroy:

            maxkm = 500
            self.irlps = sorted(self.irlps, key = lambda repeater : repeater.distance(lat,lon))
            for r in self.GTKListRows:
                r.destroy()
            for item in self.irlps:
                distance = item.distance(lat,lon)
                if( distance < maxkm):
                    self.addToList(item, lat,lon)
            self.listbox.show_all()

This sorts by distance to the chosen map center point, and shows them in the listbox. Note that there is a bug with the example where it gives you the mouse up and not the changed event, which can result in not entirely accurate map center values at the time, it is better to use the map changed event. I added logic to make it only run the render when the map center changed, as well. Including the above code, the onchange function should be:

    def on_map_change(self, event):
        if self.renderedLat != self.osm.props.latitude or self.renderedLon != self.osm.props.longitude:
            #Center changed.
            self.renderedLat = self.osm.props.latitude
            self.renderedLon = self.osm.props.longitude

            t = time.time()
            self.latlon_entry.set_text(
                'Map Center: latitude %s longitude %s' % (
                    self.osm.props.latitude,
                    self.osm.props.longitude
                )
            )
            # cursor lat,lon = self.osm.get_event_location(event).get_degrees()
            lat, lon = self.osm.props.latitude, self.osm.props.longitude
            maxkm = 500
            self.irlps = sorted(self.irlps, key = lambda repeater : repeater.distance(lat,lon))
            for r in self.GTKListRows:
                r.destroy()
            for item in self.irlps:
                distance = item.distance(lat,lon)
                if( distance < maxkm):
                    self.addToList(item, lat,lon)
            self.listbox.show_all()
            print('time: %s' % (time.time()  - t))

Connect that function to the change event in the same way the button_release_event is connected:

self.osm.connect('changed', self.on_map_change)

…and there you have it, a nearby repeaters app in the same style as the Repeaterbook or RFinder!

Display of GPS map window

The full source code can now be browsed on Github – check it out at https://github.com/programmin1/Repeater-START!

Check out the next post for optimization for smaller-screen devices like the Librem 5!

Leave a Reply

Your email address will not be published. Required fields are marked *

× one = three