diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1b41599..eeb2319 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,9 @@ +Quisk Version 4.2.41 January 2025 +================================== +I made some improvements to the top-level Config/Radios screen. There is now an option to +make a new radio by copying settings from an existing radio. I worked around a bug in wxPython +and gtk3 that caused drawing problems on the config screens. This problem is not present on Windows. + Quisk Version 4.2.40 November 2024 =================================== Python 3.13 eliminated the telnetlib module, so I wrote a replacement. I fixed a bug in window sizing. diff --git a/__init__.py b/__init__.py index 4b6958f..f7f82fa 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,2 @@ -#Quisk version 4.2.40 +#Quisk version 4.2.41 from .quisk import main diff --git a/configure.py b/configure.py index fa631f2..370b105 100644 --- a/configure.py +++ b/configure.py @@ -48,6 +48,7 @@ name2format = { # Increasing the software version will display a message to re-read the soapy device. soapy_software_version = 3 +wxpython_gtk3_bug = 0 def FormatKhz(dnum): # Round to 3 decimal places; remove ending ".000" t = "%.3f" % dnum @@ -384,10 +385,20 @@ class Configuration: app.bottom_widgets = dct['BottomWidgets'](app, hardware, conf, frame, gbs, vertBox) return True def OnPageChanging(self, event): # Called when the top level page changes (not RadioNotebook pages) + global wxpython_gtk3_bug event.Skip() notebook = event.GetEventObject() index = event.GetSelection() page = notebook.GetPage(index) + if sys.platform != 'win32': # Work around a bug in wxPython 4.2.1 and gtk3 + w, h = application.main_frame.GetSize() # Change main window size by +/- one pixel to recalculate sizes + if wxpython_gtk3_bug: + wxpython_gtk3_bug = 0 + h += 1 + else: + wxpython_gtk3_bug = 1 + h -= 1 + application.main_frame.SetSize((w, h)) if isinstance(page, RadioNotebook): if not page.pages: page.MakePages() @@ -419,6 +430,14 @@ class Configuration: elif udp > 0: return 'HiQSDR' return 'SoftRock USB' + def FindNotebookPage(self, name): + for index in range(self.radios_page_start, self.notebk.GetPageCount()): + tab_text = self.notebk.GetPageText(index) + if tab_text[0] == '*': + tab_text = tab_text[1:-1] + if tab_text == name: + return index + return None def AddRadio(self, radio_name, typ): radio_dict = {} radio_dict['hardware_file_type'] = typ @@ -435,13 +454,24 @@ class Configuration: page = RadioNotebook(self.notebk, radio_name) self.notebk.AddPage(page, radio_name) return True + def CopyRadio(self, old_name, new_name): + radio_dict = {} + index = Settings[2].index(old_name) + radio_dict.update(Settings[3][index]) + Settings[2].append(new_name) + Settings[3].append(radio_dict) + page = RadioNotebook(self.notebk, new_name) + self.notebk.AddPage(page, new_name) + return True def RenameRadio(self, old, new): - index = Settings[2].index(old) - n = self.radios_page_start + index + n = self.FindNotebookPage(old) + if n is None: + return if old == Settings[1]: self.notebk.SetPageText(n, "*%s*" % new) else: self.notebk.SetPageText(n, new) + index = Settings[2].index(old) Settings[2][index] = new self.notebk.GetPage(n).NewName(new) if old == "ConfigFileRadio": @@ -452,12 +482,10 @@ class Configuration: index = Settings[2].index(name) del Settings[2][index] del Settings[3][index] - try: - n = self.radios_page_start + index + n = self.FindNotebookPage(name) + if n is not None: self.notebk.DeletePage(n) - except: - pass - return True + return True def GetRadioDict(self, radio_name=None): # None radio_name means the current radio if radio_name: index = Settings[2].index(radio_name) @@ -1295,6 +1323,7 @@ class RadioNotebook(wx.Notebook): # The second-level notebook for each radio nam self.SetBackgroundColour(parent.bg_color) self.radio_name = radio_name self.pages = [] + self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanging) def MakePages(self): radio_name = self.radio_name radio_dict = local_conf.GetRadioDict(radio_name) @@ -1325,6 +1354,20 @@ class RadioNotebook(wx.Notebook): # The second-level notebook for each radio nam self.radio_name = new_name for page in self.pages: page.radio_name = new_name + def OnPageChanging(self, event): + global wxpython_gtk3_bug + event.Skip() + index = event.GetSelection() + page = self.GetPage(index) + if sys.platform != 'win32': # Work around a bug in wxPython 4.2.1 and gtk3 + w, h = application.main_frame.GetSize() # Change main window size by +/- one pixel to recalculate sizes + if wxpython_gtk3_bug: + wxpython_gtk3_bug = 0 + h += 1 + else: + wxpython_gtk3_bug = 1 + h -= 1 + application.main_frame.SetSize((w, h)) class ChoiceCombo(wx.Choice): text_for_blank = "-blank-" @@ -1439,7 +1482,13 @@ class ComboCtrl(wxcombo.ComboCtrl): wm = w wm += charx * 5 self.SetSizeHints(wm, self.height, 9999, self.height) + def SetValue(self, value): + wxcombo.ComboCtrl.SetValue(self, value) + self.SetBackgroundColour(self.bgColor) + self.Refresh() def SetSelection(self, n): # Set text to item in list box. Name conflict with wxcombo.ComboCtrl. + self.SetBackgroundColour(self.bgColor) + self.Refresh() try: text = self.choices[n] except IndexError: @@ -2328,9 +2377,9 @@ class ControlMixin: else: return value -class BaseWindow(wx.ScrolledCanvas, ControlMixin): +class BaseWindow(wx.ScrolledWindow, ControlMixin): def __init__(self, parent): - wx.ScrolledCanvas.__init__(self, parent) + wx.ScrolledWindow.__init__(self, parent) ControlMixin.__init__(self, parent) class ConfigConfig(BaseWindow): @@ -2653,17 +2702,21 @@ class Radios(BaseWindow): # The "Radios" first-level page def __init__(self, parent): BaseWindow.__init__(self, parent) self.SetBackgroundColour(parent.bg_color) - self.num_cols = 8 + self.num_cols = 5 self.radio_name = None - self.cur_radio_text = self.AddTextL(1, 'xx', self.num_cols - 1) - self.SetCurrentRadioText() + self.index = 0 + txt = 'Each "radio" is a block of settings that describes a radio to use with Quisk.' + self.AddTextL(1, txt, self.num_cols - 1) self.NextRow() self.NextRow() - item = self.AddTextL(1, "When Quisk starts, use the radio") + self.radio_buttons = [] + rb = self.AddRadioButton(1, "When Quisk starts, use the radio", self.NewTop, start=True) + rb.SetValue(True) self.start_radio = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) self.start_radio.handler = self.OnChoiceStartup + self.radio_buttons.append((rb, self.start_radio)) self.NextRow() - item = self.AddTextL(1, "Add a new radio with the general type") + rb = self.AddRadioButton(1, "Add a new radio with the general type", self.NewTop) choices = [] for name, data in local_conf.receiver_data: choices.append(name) @@ -2671,30 +2724,60 @@ class Radios(BaseWindow): # The "Radios" first-level page self.add_type = self.AddComboCtrl(2, '', choices=choices, no_edit=True) self.add_type.SetSelection(0) item = self.AddTextL(3, "and name the new radio") - self.add_name = self.AddComboCtrl(4, '', choices=["My Radio", "SR with XVtr", "SoftRock"]) - item = self.AddPushButton(5, "Add", self.OnBtnAdd) + self.add_name = self.AddComboCtrl(4, '', choices=["My Radio", "Transverter", "SoftRock", "HL2"]) + self.radio_buttons.append((rb, self.add_type, self.add_name)) self.NextRow() - item = self.AddTextL(1, "Rename the radio named") + rb = self.AddRadioButton(1, "Add a new radio copied from radio", self.NewTop) + self.add_from = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) + item = self.AddTextL(3, "and name the new radio") + self.add_name2 = self.AddComboCtrl(4, '', choices=["My Radio", "Transverter", "SoftRock", "HL2"]) + self.radio_buttons.append((rb, self.add_from, self.add_name2)) + self.NextRow() + rb = self.AddRadioButton(1, "Rename the radio named", self.NewTop) self.rename_old = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) item = self.AddTextL(3, "to the new name") - self.rename_new = self.AddComboCtrl(4, '', choices=["My Radio", "SR with XVtr", "SoftRock"]) - item = self.AddPushButton(5, "Rename", self.OnBtnRename) + self.rename_new = self.AddComboCtrl(4, '', choices=["My Radio", "Transverter", "SoftRock", "HL2"]) + self.radio_buttons.append((rb, self.rename_old, self.rename_new)) self.NextRow() - item = self.AddTextL(1, "Delete the radio named") + rb = self.AddRadioButton(1, "Delete the radio named", self.NewTop) self.delete_name = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) - item = self.AddPushButton(3, "Delete", self.OnBtnDelete) + self.radio_buttons.append((rb, self.delete_name)) self.NextRow() - self.FitInside() - self.SetScrollRate(1, 1) + self.NextRow() + self.apply_btn = self.AddPushButton(3, "Apply", self.OnBtnApply) self.NewRadioNames() - def SetCurrentRadioText(self): - radio_dict = local_conf.GetRadioDict(self.radio_name) - radio_type = radio_dict['hardware_file_type'] - if Settings[1] == "ConfigFileRadio": - text = 'The current radio is ConfigFileRadio, so all settings come from the config file. The hardware type is %s.' % radio_type + self.NewTop() + self.SetScrollRate(1, 1) + def NewTop(self, event=None): + if event: + btn = event.GetEventObject() + else: # set first button + btn = self.radio_buttons[0][0] + for i in range(len(self.radio_buttons)): + data = self.radio_buttons[i] + rb = data[0] + if rb == btn: + self.index = i + for c in data[1:]: + c.Enable(rb == btn) + if self.index == 0: + self.apply_btn.Enable(False) else: - text = "Quisk is running with settings from the radio %s. The hardware type is %s." % (Settings[1], radio_type) - self.cur_radio_text.SetLabel(text) + self.apply_btn.Enable(True) + def OnBtnApply(self, event): + index = self.index + ok = False + if index == 1: # add from hardware type + ok = self.OnBtnAdd() + elif index == 2: # add from existing radio + ok = self.OnBtnCopy() + elif index == 3: # rename + ok = self.OnBtnRename() + elif index == 4: # delete + ok = self.OnBtnDelete() + if ok: + self.radio_buttons[0][0].SetValue(True) + self.NewTop() def DuplicateName(self, name): if name in Settings[2] or name == "ConfigFileRadio": dlg = wx.MessageDialog(self, "The name already exists. Please choose a different name.", @@ -2703,34 +2786,39 @@ class Radios(BaseWindow): # The "Radios" first-level page dlg.Destroy() return True return False - def OnBtnAdd(self, event): + def OnBtnAdd(self): name = self.add_name.GetValue().strip() if not name or self.DuplicateName(name): return self.add_name.SetValue('') typ = self.add_type.GetValue().strip() if local_conf.AddRadio(name, typ): - if Settings[0] != "Ask me": - Settings[0] = name self.NewRadioNames() local_conf.settings_changed = True - def OnBtnRename(self, event): + return True + def OnBtnCopy(self): + new_name = self.add_name2.GetValue().strip() + if not new_name or self.DuplicateName(new_name): + return + self.add_name2.SetValue('') + old_name = self.add_from.GetValue() + if local_conf.CopyRadio(old_name, new_name): + self.NewRadioNames() + local_conf.settings_changed = True + return True + def OnBtnRename(self): old = self.rename_old.GetValue() new = self.rename_new.GetValue().strip() if not old or not new or self.DuplicateName(new): return self.rename_new.SetValue('') if local_conf.RenameRadio(old, new): - if old == 'ConfigFileRadio' and Settings[1] == "ConfigFileRadio": + if Settings[1] == old: Settings[1] = new - elif Settings[1] == old: - Settings[1] = new - self.SetCurrentRadioText() - if Settings[0] != "Ask me": - Settings[0] = new self.NewRadioNames() local_conf.settings_changed = True - def OnBtnDelete(self, event): + return True + def OnBtnDelete(self): name = self.delete_name.GetValue() if not name: return @@ -2742,6 +2830,7 @@ class Radios(BaseWindow): # The "Radios" first-level page if ret == wx.ID_OK and local_conf.DeleteRadio(name): self.NewRadioNames() local_conf.settings_changed = True + return True def OnChoiceStartup(self, ctrl): choice = self.start_radio.GetValue() if Settings[0] != choice: @@ -2751,6 +2840,8 @@ class Radios(BaseWindow): # The "Radios" first-level page choices = Settings[2][:] # can rename any available radio self.rename_old.SetItems(choices) self.rename_old.SetSelection(0) + self.add_from.SetItems(choices) + self.add_from.SetSelection(0) if "ConfigFileRadio" in choices: # can not delete ConfigFileRadio choices.remove("ConfigFileRadio") self.delete_name.SetItems(choices) @@ -2762,13 +2853,13 @@ class Radios(BaseWindow): # The "Radios" first-level page try: # Set text in control index = choices.index(Settings[0]) # last used radio, or new or renamed radio except: - num = len(Settings[2]) - if len == 0: - index = 1 - elif num == 1: - index = 0 + num = len(Settings[2]) # number of radios + if num == 0: # no radios + index = 1 # use ConfigFileRadio + elif num == 1: # one radio + index = 0 # use the one radio else: - index = len(choices) - 2 + index = len(choices) - 2 # use the last radio Settings[0] = choices[index] self.start_radio.SetSelection(index) diff --git a/docs.html b/docs.html index 16b4df2..b9ad1c7 100644 --- a/docs.html +++ b/docs.html @@ -194,7 +194,7 @@ Quisk is free open source software written in Python and C. It is hosted on the

If you have Python 3 installed on your computer, just continue to use that version. -On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11 or 3.12. +On Windows Quisk requires Python 3.8, 3.9, 3.10, 3.11, 3.12 or 3.13. If you want to install a more recent version of Python, uninstall the old version first. Running multiple versions of Python is possible but can be confusing and is not necessary. A Quisk installation is needed for each version of Python you have. @@ -204,7 +204,7 @@ A Quisk installation is needed for each version of Python you have.

Windows does not include Python, so you must first install a recent version of 64-bit Python 3. If you already have 64-bit Python 3 installed, just keep using it. -On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11 or 3.12. +On Windows Quisk requires Python 3.8, 3.9, 3.10, 3.11, 3.12 or 3.13. There may be newer versions of Python, but until support is added, use the versions listed above. Python is available from http://www.python.org. There is an option to Add Python to PATH. You MUST select @@ -256,6 +256,7 @@ If you change to that directory you can run Quisk with "pythonw quisk.py".

To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios. +Add a radio from the list of hardware radios available. Or add the best match. Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration is (mostly) from the Config button. Ignore old directions and don't bother with a config file.

@@ -385,6 +386,7 @@ You run Quisk by running the file quisk.py with python3. For example, "python3 / You can create a desktop icon to do that, but the method depends on your version of Linux. Never run Quisk with root privelege by using sudo. Fix the permission problem instead. To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios. +Add a radio from the list of hardware radios available. Or add the best match. Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration is (mostly) from the Config button. Ignore old directions and don't bother with a config file. To uninstall Quisk, just delete the quisk directory. @@ -532,7 +534,7 @@ When you first install Quisk, you will not have any settings for your radio. Press the Config button and go to the Radios screen. Then create a radio by specifying the general hardware type and give it a name of your choosing. For a Hermes-Lite, specify "Hermes" as the hardware type and call it "HL2" (or some other name). -Press "Add" and a new tab for your radio will appear. Look through the various settings on the HL2 tab. +Press "Apply" and a new tab for your radio will appear. Look through the various settings on the HL2 tab. The parameters for the radios are stored in the file quisk_settings.json.

@@ -1493,16 +1495,24 @@ at his site https://dk1mi.radio/quis If your digital program only uses Alsa devices or you don't want to use PulseAudio, you can use the ALSA loopback device. It works the same way as the Windows VAC. First create the loopback device with this command: +
+
 sudo modprobe snd-aloop index=19
 
+
+

The "index" is the card number, and it is best to use a number higher than your regular cards. The loopback device will be gone on the next restart. You can create the loopback device every time the system starts, but the way to do that depends on your version of Linux. You could add this line to /etc/modprobe.d/alsa-base.conf: +

+
 options snd-aloop index=19
 
+
+

Or you could add snd-aloop to /etc/modules or put the modprobe command in /etc/rc.local instead. Restart Linux. Now you can enter "cat /proc/asound/cards" to print out your sound cards, and you should see a "Loopback" card listed. @@ -1517,6 +1527,7 @@ Your audio is now connected and you should be able to receive digital signals. Be sure to test your transmit signal off the air. You may need to reduce power to improve linearity. +



diff --git a/setup.py b/setup.py index fe45cc1..0b419ba 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ import os # You must define the version here. A title string including # the version will be written to __init__.py and read by quisk.py. -Version = '4.2.40' +Version = '4.2.41' fp = open("__init__.py", "w") # write title string fp.write("#Quisk version %s\n" % Version)