diff --git a/NanoVNASaver/Charts/Capacitance.py b/NanoVNASaver/Charts/Capacitance.py index b0647b0..68829d2 100644 --- a/NanoVNASaver/Charts/Capacitance.py +++ b/NanoVNASaver/Charts/Capacitance.py @@ -16,16 +16,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import math import logging -from decimal import InvalidOperation -from typing import List - -from PyQt5 import QtWidgets, QtGui - -from NanoVNASaver.RFTools import Datapoint -from NanoVNASaver.SITools import Format, Value -from NanoVNASaver.Charts.Chart import Chart from NanoVNASaver.Charts.Frequency import FrequencyChart logger = logging.getLogger(__name__) @@ -34,264 +25,7 @@ logger = logging.getLogger(__name__) class CapacitanceChart(FrequencyChart): def __init__(self, name=""): super().__init__(name) - self.leftMargin = 45 - self.dim.width = 250 - self.dim.height = 250 self.minDisplayValue = 0 self.maxDisplayValue = 100 - - self.minValue = -1 - self.maxValue = 1 - self.span = 1 - - self.setMinimumSize(self.dim.width + self.rightMargin + self.leftMargin, - self.dim.height + self.topMargin + self.bottomMargin) - self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding)) - pal = QtGui.QPalette() - pal.setColor(QtGui.QPalette.Background, Chart.color.background) - self.setPalette(pal) - self.setAutoFillBackground(True) - - def drawChart(self, qp: QtGui.QPainter): - qp.setPen(QtGui.QPen(Chart.color.text)) - qp.drawText(3, 15, self.name + " (F)") - qp.setPen(QtGui.QPen(Chart.color.foreground)) - qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.dim.height+5) - qp.drawLine(self.leftMargin-5, self.topMargin+self.dim.height, - self.leftMargin+self.dim.width, self.topMargin + self.dim.height) - self.drawTitle(qp) - - def drawValues(self, qp: QtGui.QPainter): - if len(self.data) == 0 and len(self.reference) == 0: - return - pen = QtGui.QPen(Chart.color.sweep) - pen.setWidth(self.dim.point) - line_pen = QtGui.QPen(Chart.color.sweep) - line_pen.setWidth(self.dim.line) - highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255)) - highlighter.setWidth(1) - if not self.fixedSpan: - if len(self.data) > 0: - fstart = self.data[0].freq - fstop = self.data[len(self.data)-1].freq - else: - fstart = self.reference[0].freq - fstop = self.reference[len(self.reference) - 1].freq - self.fstart = fstart - self.fstop = fstop - else: - fstart = self.fstart = self.minFrequency - fstop = self.fstop = self.maxFrequency - - # Draw bands if required - if self.bands.enabled: - self.drawBands(qp, fstart, fstop) - - if self.fixedValues: - maxValue = self.maxDisplayValue / 10e11 - minValue = self.minDisplayValue / 10e11 - self.maxValue = maxValue - self.minValue = minValue - else: - # Find scaling - minValue = 1 - maxValue = -1 - for d in self.data: - val = d.capacitiveEquivalent() - if val > maxValue: - maxValue = val - if val < minValue: - minValue = val - for d in self.reference: # Also check min/max for the reference sweep - if d.freq < self.fstart or d.freq > self.fstop: - continue - val = d.capacitiveEquivalent() - if val > maxValue: - maxValue = val - if val < minValue: - minValue = val - self.maxValue = maxValue - self.minValue = minValue - - span = maxValue - minValue - if span == 0: - logger.info("Span is zero for CapacitanceChart, setting to a small value.") - span = 1e-15 - self.span = span - - target_ticks = math.floor(self.dim.height / 60) - fmt = Format(max_nr_digits=3) - for i in range(target_ticks): - val = minValue + (i / target_ticks) * span - try: - y = self.topMargin + round((self.maxValue - val) / self.span * self.dim.height) - qp.setPen(Chart.color.text) - if val != minValue: - valstr = str(Value(val, fmt=fmt)) - qp.drawText(3, y + 3, valstr) - except (ValueError, InvalidOperation) as exc: - logger.debug("Drawing wrent wrong: %s", exc) - y = self.topMargin - qp.setPen(QtGui.QPen(Chart.color.foreground)) - qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.dim.width, y) - - qp.setPen(QtGui.QPen(Chart.color.foreground)) - qp.drawLine(self.leftMargin - 5, self.topMargin, - self.leftMargin + self.dim.width, self.topMargin) - qp.setPen(Chart.color.text) - qp.drawText(3, self.topMargin + 4, str(Value(maxValue, fmt=fmt))) - qp.drawText(3, self.dim.height+self.topMargin, str(Value(minValue, fmt=fmt))) - self.drawFrequencyTicks(qp) - - self.drawData(qp, self.data, Chart.color.sweep) - self.drawData(qp, self.reference, Chart.color.reference) - self.drawMarkers(qp) - - def getYPosition(self, d: Datapoint) -> int: - try: - return ( - self.topMargin + - round((self.maxValue - d.capacitiveEquivalent()) / - self.span * self.dim.height)) - except ValueError: - return self.topMargin - - def valueAtPosition(self, y) -> List[float]: - absy = y - self.topMargin - val = -1 * ((absy / self.dim.height * self.span) - self.maxValue) - return [val * 10e11] - - def copy(self): - new_chart: CapacitanceChart = super().copy() - new_chart.span = self.span - return new_chart - - -class InductanceChart(FrequencyChart): - def __init__(self, name=""): - super().__init__(name) - self.leftMargin = 30 - self.dim.width = 250 - self.dim.height = 250 - self.minDisplayValue = 0 - self.maxDisplayValue = 100 - - self.minValue = -1 - self.maxValue = 1 - self.span = 1 - - self.setMinimumSize(self.dim.width + self.rightMargin + self.leftMargin, - self.dim.height + self.topMargin + self.bottomMargin) - self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding)) - pal = QtGui.QPalette() - pal.setColor(QtGui.QPalette.Background, Chart.color.background) - self.setPalette(pal) - self.setAutoFillBackground(True) - - def drawChart(self, qp: QtGui.QPainter): - qp.setPen(QtGui.QPen(Chart.color.text)) - qp.drawText(3, 15, self.name + " (H)") - qp.setPen(QtGui.QPen(Chart.color.foreground)) - qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.dim.height+5) - qp.drawLine(self.leftMargin-5, self.topMargin+self.dim.height, - self.leftMargin+self.dim.width, self.topMargin + self.dim.height) - self.drawTitle(qp) - - def drawValues(self, qp: QtGui.QPainter): - if len(self.data) == 0 and len(self.reference) == 0: - return - pen = QtGui.QPen(Chart.color.sweep) - pen.setWidth(self.dim.point) - line_pen = QtGui.QPen(Chart.color.sweep) - line_pen.setWidth(self.dim.line) - highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255)) - highlighter.setWidth(1) - if not self.fixedSpan: - if len(self.data) > 0: - fstart = self.data[0].freq - fstop = self.data[len(self.data)-1].freq - else: - fstart = self.reference[0].freq - fstop = self.reference[len(self.reference) - 1].freq - self.fstart = fstart - self.fstop = fstop - else: - fstart = self.fstart = self.minFrequency - fstop = self.fstop = self.maxFrequency - - # Draw bands if required - if self.bands.enabled: - self.drawBands(qp, fstart, fstop) - - if self.fixedValues: - maxValue = self.maxDisplayValue / 10e11 - minValue = self.minDisplayValue / 10e11 - self.maxValue = maxValue - self.minValue = minValue - else: - # Find scaling - minValue = 1 - maxValue = -1 - for d in self.data: - val = d.inductiveEquivalent() - if val > maxValue: - maxValue = val - if val < minValue: - minValue = val - for d in self.reference: # Also check min/max for the reference sweep - if d.freq < self.fstart or d.freq > self.fstop: - continue - val = d.inductiveEquivalent() - if val > maxValue: - maxValue = val - if val < minValue: - minValue = val - self.maxValue = maxValue - self.minValue = minValue - - span = maxValue - minValue - if span == 0: - logger.info("Span is zero for CapacitanceChart, setting to a small value.") - span = 1e-15 - self.span = span - - target_ticks = math.floor(self.dim.height / 60) - fmt = Format(max_nr_digits=3) - for i in range(target_ticks): - val = minValue + (i / target_ticks) * span - y = self.topMargin + round((self.maxValue - val) / self.span * self.dim.height) - qp.setPen(Chart.color.text) - if val != minValue: - valstr = str(Value(val, fmt=fmt)) - qp.drawText(3, y + 3, valstr) - qp.setPen(QtGui.QPen(Chart.color.foreground)) - qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.dim.width, y) - - qp.setPen(QtGui.QPen(Chart.color.foreground)) - qp.drawLine(self.leftMargin - 5, self.topMargin, - self.leftMargin + self.dim.width, self.topMargin) - qp.setPen(Chart.color.text) - qp.drawText(3, self.topMargin + 4, str(Value(maxValue, fmt=fmt))) - qp.drawText(3, self.dim.height+self.topMargin, str(Value(minValue, fmt=fmt))) - self.drawFrequencyTicks(qp) - - self.drawData(qp, self.data, Chart.color.sweep) - self.drawData(qp, self.reference, Chart.color.reference) - self.drawMarkers(qp) - - def getYPosition(self, d: Datapoint) -> int: - return (self.topMargin + - round((self.maxValue - d.inductiveEquivalent()) / - self.span * self.dim.height)) - - def valueAtPosition(self, y) -> List[float]: - absy = y - self.topMargin - val = -1 * ((absy / self.dim.height * self.span) - self.maxValue) - return [val * 10e11] - - def copy(self): - new_chart: InductanceChart = super().copy() - new_chart.span = self.span - return new_chart + self.name_unit = "F" + self.value_function = lambda x: x.capacitiveEquivalent() diff --git a/NanoVNASaver/Charts/Chart.py b/NanoVNASaver/Charts/Chart.py index a5cfb33..312176e 100644 --- a/NanoVNASaver/Charts/Chart.py +++ b/NanoVNASaver/Charts/Chart.py @@ -24,6 +24,7 @@ from typing import List, Set, Tuple, ClassVar, Any from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtCore import pyqtSignal +from NanoVNASaver import Defaults from NanoVNASaver.RFTools import Datapoint from NanoVNASaver.Marker import Marker @@ -64,23 +65,15 @@ class ChartFlags: draw_lines: bool = False is_popout: bool = False -@dataclass -class ChartMarkerConfig: - draw_label: bool = False - fill: bool = False - at_tip: bool = False - size: int = 3 class ChartMarker(QtWidgets.QWidget): - cfg: ClassVar[ChartMarkerConfig] = ChartMarkerConfig() - def __init__(self, qp: QtGui.QPaintDevice): super().__init__() self.qp = qp def draw(self, x: int, y: int, color: QtGui.QColor, text: str = ""): - offset = self.cfg.size // 2 - if self.cfg.at_tip: + offset = Defaults.cfg.chart.marker_size // 2 + if Defaults.cfg.chart.marker_at_tip: y -= offset pen = QtGui.QPen(color) self.qp.setPen(pen) @@ -90,12 +83,12 @@ class ChartMarker(QtWidgets.QWidget): qpp.lineTo(x + offset, y - offset) qpp.lineTo(x, y + offset) - if self.cfg.fill: + if Defaults.cfg.chart.marker_filled: self.qp.fillPath(qpp, color) else: self.qp.drawPath(qpp) - if text and self.cfg.draw_label: + if text and Defaults.cfg.chart.marker_label: text_width = self.qp.fontMetrics().horizontalAdvance(text) self.qp.drawText(x - text_width // 2, y - 3 - offset, text) @@ -104,7 +97,6 @@ class Chart(QtWidgets.QWidget): bands: ClassVar[Any] = None popoutRequested: ClassVar[Any] = pyqtSignal(object) color: ClassVar[ChartColors] = ChartColors() - marker_cfg: ClassVar[ChartMarkerConfig] = ChartMarkerConfig() def __init__(self, name): super().__init__() @@ -160,7 +152,7 @@ class Chart(QtWidgets.QWidget): self.update() def setMarkerSize(self, size): - ChartMarker.cfg.size = size + Defaults.cfg.chart.marker_size = size self.update() def setSweepTitle(self, title): @@ -249,7 +241,6 @@ class Chart(QtWidgets.QWidget): new_chart.reference = self.reference new_chart.dim = replace(self.dim) new_chart.flag = replace(self.flag) - new_chart.marker_cfg = replace(self.marker_cfg) new_chart.markers = self.markers new_chart.swrMarkers = self.swrMarkers new_chart.bands = self.bands diff --git a/NanoVNASaver/Charts/Frequency.py b/NanoVNASaver/Charts/Frequency.py index e9ae60b..107c5f2 100644 --- a/NanoVNASaver/Charts/Frequency.py +++ b/NanoVNASaver/Charts/Frequency.py @@ -2,7 +2,7 @@ # # A python program to view and export Touchstone data from a NanoVNA # Copyright (C) 2019, 2020 Rune B. Broberg -# Copyright (C) 2020,2021 NanoVNA-Saver Authors +# Copyright (C) 2020ff NanoVNA-Saver Authors # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -35,30 +35,22 @@ logger = logging.getLogger(__name__) class FrequencyChart(Chart): - fstart = 0 - fstop = 0 - maxFrequency = 100000000 - minFrequency = 1000000 - - # TODO: use unscaled values instead of unit dependend ones - minDisplayValue = -1 - maxDisplayValue = 1 - - fixedSpan = False - fixedValues = False - - logarithmicX = False - logarithmicY = False - - leftMargin = 30 - rightMargin = 20 - bottomMargin = 20 - topMargin = 30 def __init__(self, name): super().__init__(name) + self.maxFrequency = 100000000 + self.minFrequency = 1000000 + self.fixedSpan = False + self.fixedValues = False + self.logarithmicX = False + self.logarithmicY = False + self.leftMargin = 30 + self.rightMargin = 20 + self.bottomMargin = 20 + self.topMargin = 30 + self.dim.width = 250 self.dim.height = 250 self.fstart = 0 @@ -67,6 +59,10 @@ class FrequencyChart(Chart): self.name_unit = "" self.value_function = lambda x: 0.0 + # TODO: use unscaled values instead of unit dependend ones + self.minDisplayValue = -1 + self.maxDisplayValue = 1 + self.minValue = -1 self.maxValue = 1 self.span = 1 @@ -189,17 +185,15 @@ class FrequencyChart(Chart): def _set_start_stop(self): if self.fixedSpan: - fstart = self.minFrequency - fstop = self.maxFrequency - else: - if len(self.data) > 0: - fstart = self.data[0].freq - fstop = self.data[len(self.data) - 1].freq - else: - fstart = self.reference[0].freq - fstop = self.reference[len(self.reference) - 1].freq - self.fstart = fstart - self.fstop = fstop + self.fstart = self.minFrequency + self.fstop = self.maxFrequency + return + if self.data: + self.fstart = self.data[0].freq + self.fstop = self.data[len(self.data) - 1].freq + return + self.fstart = self.reference[0].freq + self.fstop = self.reference[len(self.reference) - 1].freq def contextMenuEvent(self, event): self.action_set_fixed_start.setText( @@ -336,10 +330,13 @@ class FrequencyChart(Chart): return math.floor(self.width() / 2) def getYPosition(self, d: Datapoint) -> int: - return ( - self.topMargin + - round((self.maxValue - d.capacitiveEquivalent()) / - self.span * self.dim.height)) + try: + return ( + self.topMargin + + round((self.maxValue - self.value_function(d) / + self.span * self.dim.height))) + except ValueError: + return self.topMargin def frequencyAtPosition(self, x, limit=True) -> int: """ @@ -352,20 +349,21 @@ class FrequencyChart(Chart): and the value is before or after the chart, returns minimum or maximum frequencies. """ - if self.fstop - self.fstart > 0: - absx = x - self.leftMargin - if limit and absx < 0: + if self.fstop - self.fstart <= 0: + return -1 + absx = x - self.leftMargin + if limit: + if absx < 0: return self.fstart - if limit and absx > self.dim.width: + if absx > self.dim.width: return self.fstop - if self.logarithmicX: - span = math.log(self.fstop) - math.log(self.fstart) - step = span / self.dim.width - return round(math.exp(math.log(self.fstart) + absx * step)) - span = self.fstop - self.fstart + if self.logarithmicX: + span = math.log(self.fstop) - math.log(self.fstart) step = span / self.dim.width - return round(self.fstart + absx * step) - return -1 + return round(math.exp(math.log(self.fstart) + absx * step)) + span = self.fstop - self.fstart + step = span / self.dim.width + return round(self.fstart + absx * step) def valueAtPosition(self, y) -> List[float]: """ @@ -574,26 +572,21 @@ class FrequencyChart(Chart): self.drawData(qp, self.reference, Chart.color.reference) self.drawMarkers(qp) - def _find_scaling(self) -> Tuple[int, int]: + def _find_scaling(self) -> Tuple[float, float]: + min_value = self.minDisplayValue / 10e11 + max_value = self.maxDisplayValue / 10e11 if self.fixedValues: - return (self.minDisplayValue / 10e11, - self.maxDisplayValue / 10e11) - min_value = 1 - max_value = -1 + return (min_value, max_value) for d in self.data: val = self.value_function(d) - if val > max_value: - max_value = val - if val < min_value: - min_value = val + min_value = min(min_value, val) + max_value = max(max_value, val) for d in self.reference: # Also check min/max for the reference sweep if d.freq < self.fstart or d.freq > self.fstop: continue val = self.value_function(d) - if val > max_value: - max_value = val - if val < min_value: - min_value = val + min_value = min(min_value, val) + max_value = max(max_value, val) return (min_value, max_value) def drawFrequencyTicks(self, qp): diff --git a/NanoVNASaver/Charts/Inductance.py b/NanoVNASaver/Charts/Inductance.py index 4b56c45..85d131e 100644 --- a/NanoVNASaver/Charts/Inductance.py +++ b/NanoVNASaver/Charts/Inductance.py @@ -2,7 +2,7 @@ # # A python program to view and export Touchstone data from a NanoVNA # Copyright (C) 2019, 2020 Rune B. Broberg -# Copyright (C) 2020 NanoVNA-Saver Authors +# Copyright (C) 2020ff NanoVNA-Saver Authors # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,15 +16,8 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import math import logging -from typing import List -from PyQt5 import QtWidgets, QtGui - -from NanoVNASaver.RFTools import Datapoint -from NanoVNASaver.SITools import Format, Value -from NanoVNASaver.Charts.Chart import Chart from NanoVNASaver.Charts.Frequency import FrequencyChart logger = logging.getLogger(__name__) @@ -33,127 +26,7 @@ logger = logging.getLogger(__name__) class InductanceChart(FrequencyChart): def __init__(self, name=""): super().__init__(name) - self.leftMargin = 45 - self.dim.width = 250 - self.dim.height = 250 self.minDisplayValue = 0 self.maxDisplayValue = 100 - - self.minValue = -1 - self.maxValue = 1 - self.span = 1 - - self.setMinimumSize(self.dim.width + self.rightMargin + self.leftMargin, - self.dim.height + self.topMargin + self.bottomMargin) - self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding)) - pal = QtGui.QPalette() - pal.setColor(QtGui.QPalette.Background, Chart.color.background) - self.setPalette(pal) - self.setAutoFillBackground(True) - - def drawChart(self, qp: QtGui.QPainter): - qp.setPen(QtGui.QPen(Chart.color.text)) - qp.drawText(3, 15, self.name + " (H)") - qp.setPen(QtGui.QPen(Chart.color.foreground)) - qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.dim.height+5) - qp.drawLine(self.leftMargin-5, self.topMargin+self.dim.height, - self.leftMargin+self.dim.width, self.topMargin + self.dim.height) - self.drawTitle(qp) - - def drawValues(self, qp: QtGui.QPainter): - if len(self.data) == 0 and len(self.reference) == 0: - return - pen = QtGui.QPen(Chart.color.sweep) - pen.setWidth(self.dim.point) - line_pen = QtGui.QPen(Chart.color.sweep) - line_pen.setWidth(self.dim.line) - highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255)) - highlighter.setWidth(1) - if not self.fixedSpan: - if len(self.data) > 0: - fstart = self.data[0].freq - fstop = self.data[len(self.data)-1].freq - else: - fstart = self.reference[0].freq - fstop = self.reference[len(self.reference) - 1].freq - self.fstart = fstart - self.fstop = fstop - else: - fstart = self.fstart = self.minFrequency - fstop = self.fstop = self.maxFrequency - - # Draw bands if required - if self.bands.enabled: - self.drawBands(qp, fstart, fstop) - - if self.fixedValues: - maxValue = self.maxDisplayValue / 10e11 - minValue = self.minDisplayValue / 10e11 - self.maxValue = maxValue - self.minValue = minValue - else: - # Find scaling - minValue = 1 - maxValue = -1 - for d in self.data: - val = d.inductiveEquivalent() - if val > maxValue: - maxValue = val - if val < minValue: - minValue = val - for d in self.reference: # Also check min/max for the reference sweep - if d.freq < self.fstart or d.freq > self.fstop: - continue - val = d.inductiveEquivalent() - if val > maxValue: - maxValue = val - if val < minValue: - minValue = val - self.maxValue = maxValue - self.minValue = minValue - - span = maxValue - minValue - if span == 0: - logger.info("Span is zero for CapacitanceChart, setting to a small value.") - span = 1e-15 - self.span = span - - target_ticks = math.floor(self.dim.height / 60) - fmt = Format(max_nr_digits=3) - for i in range(target_ticks): - val = minValue + (i / target_ticks) * span - y = self.topMargin + round((self.maxValue - val) / self.span * self.dim.height) - qp.setPen(Chart.color.text) - if val != minValue: - valstr = str(Value(val, fmt=fmt)) - qp.drawText(3, y + 3, valstr) - qp.setPen(QtGui.QPen(Chart.color.foreground)) - qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.dim.width, y) - - qp.setPen(QtGui.QPen(Chart.color.foreground)) - qp.drawLine(self.leftMargin - 5, self.topMargin, - self.leftMargin + self.dim.width, self.topMargin) - qp.setPen(Chart.color.text) - qp.drawText(3, self.topMargin + 4, str(Value(maxValue, fmt=fmt))) - qp.drawText(3, self.dim.height+self.topMargin, str(Value(minValue, fmt=fmt))) - self.drawFrequencyTicks(qp) - - self.drawData(qp, self.data, Chart.color.sweep) - self.drawData(qp, self.reference, Chart.color.reference) - self.drawMarkers(qp) - - def getYPosition(self, d: Datapoint) -> int: - return (self.topMargin + - round((self.maxValue - d.inductiveEquivalent()) / - self.span * self.dim.height)) - - def valueAtPosition(self, y) -> List[float]: - absy = y - self.topMargin - val = -1 * ((absy / self.dim.height * self.span) - self.maxValue) - return [val * 10e11] - - def copy(self): - new_chart: InductanceChart = super().copy() - new_chart.span = self.span - return new_chart + self.name_unit = "H" + self.value_function = lambda x: x.inductiveEquivalent() diff --git a/NanoVNASaver/Charts/MagnitudeZ.py b/NanoVNASaver/Charts/MagnitudeZ.py index 90a073a..d325b12 100644 --- a/NanoVNASaver/Charts/MagnitudeZ.py +++ b/NanoVNASaver/Charts/MagnitudeZ.py @@ -23,7 +23,8 @@ from typing import List from PyQt5 import QtGui from NanoVNASaver.RFTools import Datapoint -from NanoVNASaver.SITools import Format, Value +from NanoVNASaver.SITools import ( + Format, Value, round_ceil, round_floor) from NanoVNASaver.Charts.Chart import Chart from NanoVNASaver.Charts.Frequency import FrequencyChart from NanoVNASaver.Charts.LogMag import LogMagChart @@ -54,48 +55,34 @@ class MagnitudeZChart(FrequencyChart): self.drawBands(qp, self.fstart, self.fstop) if self.fixedValues: - maxValue = self.maxDisplayValue - minValue = self.minDisplayValue - self.maxValue = maxValue - if self.logarithmicY and minValue <= 0: - self.minValue = 0.01 - else: - self.minValue = minValue + self.maxValue = self.maxDisplayValue + self.minValue = max( + self.minDisplayValue, 0.01) if self.logarithmicY else self.minDisplayValue else: # Find scaling - minValue = 100 - maxValue = 0 + self.minValue = 100 + self.maxValue = 0 for d in self.data: mag = self.magnitude(d) - if math.isinf(mag): # Avoid infinite scales + if math.isinf(mag): # Avoid infinite scales continue - if mag > maxValue: - maxValue = mag - if mag < minValue: - minValue = mag + self.maxValue = max(self.maxValue, mag) + self.minValue = min(self.minValue, mag) for d in self.reference: # Also check min/max for the reference sweep if d.freq < self.fstart or d.freq > self.fstop: continue mag = self.magnitude(d) - if math.isinf(mag): # Avoid infinite scales + if math.isinf(mag): # Avoid infinite scales continue - if mag > maxValue: - maxValue = mag - if mag < minValue: - minValue = mag + self.maxValue = max(self.maxValue, mag) + self.minValue = min(self.minValue, mag) - minValue = 10*math.floor(minValue/10) - if self.logarithmicY and minValue <= 0: - minValue = 0.01 - self.minValue = minValue + self.minValue = round_floor(self.minValue, 2) + if self.logarithmicY and self.minValue <= 0: + self.minValue = 0.01 + self.maxValue = round_ceil(self.maxValue, 2) - maxValue = 10*math.ceil(maxValue/10) - self.maxValue = maxValue - - span = maxValue-minValue - if span == 0: - span = 0.01 - self.span = span + self.span = (self.maxValue - self.minValue) or 0.01 # We want one horizontal tick per 50 pixels, at most horizontal_ticks = math.floor(self.dim.height/50) diff --git a/NanoVNASaver/Controls/MarkerControl.py b/NanoVNASaver/Controls/MarkerControl.py index 8f6fafe..86f0508 100644 --- a/NanoVNASaver/Controls/MarkerControl.py +++ b/NanoVNASaver/Controls/MarkerControl.py @@ -21,6 +21,7 @@ import logging from PyQt5 import QtWidgets, QtCore from PyQt5.QtWidgets import QCheckBox +from NanoVNASaver import Defaults from NanoVNASaver.Marker import Marker from NanoVNASaver.Controls.Control import Control @@ -28,13 +29,21 @@ from NanoVNASaver.Controls.Control import Control logger = logging.getLogger(__name__) +class ShowButton(QtWidgets.QPushButton): + def setText(self, text: str=''): + if not text: + text = ("Show data" + if Defaults.cfg.gui.markers_hidden else "Hide data") + super().setText(text) + self.setToolTip("Toggle visibility of marker readings area") + + class MarkerControl(Control): def __init__(self, app: QtWidgets.QWidget): super().__init__(app, "Markers") - marker_count = max(self.app.settings.value("MarkerCount", 3, int), 1) - for i in range(marker_count): + for i in range(Defaults.cfg.chart.marker_count): marker = Marker("", self.app.settings) # marker.setFixedHeight(20) marker.updated.connect(self.app.markerUpdated) @@ -56,12 +65,9 @@ class MarkerControl(Control): self.layout.addRow(layout2) - self.showMarkerButton = QtWidgets.QPushButton() + self.showMarkerButton = ShowButton() self.showMarkerButton.setFixedHeight(20) - if self.app.marker_frame.isHidden(): - self.showMarkerButton.setText("Show data") - else: - self.showMarkerButton.setText("Hide data") + self.showMarkerButton.setText() self.showMarkerButton.clicked.connect(self.toggle_frame) lock_radiobutton = QtWidgets.QRadioButton("Locked") @@ -76,10 +82,10 @@ class MarkerControl(Control): def toggle_frame(self): def settings(hidden: bool): - self.app.marker_frame.setHidden(not hidden) - self.app.settings.setValue("MarkersVisible", hidden) - self.showMarkerButton.setText( - "Hide data" if hidden else "Show data") + Defaults.cfg.gui.markers_hidden = not hidden + self.app.marker_frame.setHidden( + Defaults.cfg.gui.markers_hidden) + self.showMarkerButton.setText() self.showMarkerButton.repaint() settings(self.app.marker_frame.isHidden()) diff --git a/NanoVNASaver/Defaults.py b/NanoVNASaver/Defaults.py new file mode 100644 index 0000000..ed6cc2f --- /dev/null +++ b/NanoVNASaver/Defaults.py @@ -0,0 +1,125 @@ +# NanoVNASaver +# +# A python program to view and export Touchstone data from a NanoVNA +# Copyright (C) 2019, 2020 Rune B. Broberg +# Copyright (C) 2020ff NanoVNA-Saver Authors +# +# This program 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. +# +# This program 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 this program. If not, see . + +import dataclasses as DC +import logging +import json + +from PyQt5.QtCore import QSettings + + +logger = logging.getLogger(__name__) + +# pylint: disable=too-few-public-methods +# pylint: disable=too-many-instance-attributes +@DC.dataclass +class GUI: + window_height: int = 950 + window_width: int = 1433 + font_size: int = 8 + dark_mode: bool = False + # TODO: implement QByteArray + splitter_sizes: bytearray = DC.field(default_factory=bytearray) + markers_hidden: bool = False + + +@DC.dataclass +class Chart: + point_size: int = 2 + show_lines: bool = False + line_thickness: int = 1 + marker_count: int = 3 + marker_label: bool = False + marker_filled: bool = False + marker_at_tip: bool = False + marker_size: int = 8 + returnloss_is_positive: bool = False + + +@DC.dataclass +class CFG: + gui: object = GUI() + chart: object = Chart() + + +cfg = CFG() + +def restore(settings: 'AppSettings') -> CFG: + result = CFG() + for field in DC.fields(result): + value = settings.restore_dataclass(field.name.upper(), + getattr(result, field.name)) + setattr(result, field.name, value) + logger.debug("restored\n(\n%s\n)", result) + return result + + +def store(settings: 'AppSettings', data: CFG) -> None: + logger.debug("storing\n(\n%s\n)", data) + assert isinstance(data, CFG) + for field in DC.fields(data): + data_class = getattr(data, field.name) + assert DC.is_dataclass(data_class) + settings.store_dataclass(field.name.upper(), data_class) + + +class AppSettings(QSettings): + def store_dataclass(self, name: str, data: object) -> None: + assert DC.is_dataclass(data) + self.beginGroup(name) + for field in DC.fields(data): + value = getattr(data, field.name) + try: + assert isinstance(value, field.type) + except AssertionError: + logger.error("%s: %s is not a %s", name, field.name, + field.type) + continue + if field.type not in (int, float, str, bool): + try: + value = json.dumps(value) + except TypeError: + value = field.type(value).hex() + self.setValue(field.name, value) + self.endGroup() + + def restore_dataclass(self, name: str, data: object) -> object: + assert DC.is_dataclass(data) + + result = DC.replace(data) + self.beginGroup(name) + for field in DC.fields(data): + value = None + if field.type in (int, float, str, bool): + value = self.value(field.name, + type=field.type, + defaultValue=field.default) + else: + default = getattr(data, field.name) + try: + value = json.loads( + self.value(field.name, type=str, + defaultValue=json.dumps(default))) + except TypeError: + value = self.value(field.name) + value = bytes.fromhex(value) if value is str else default + setattr(result, field.name, field.type(value)) + self.endGroup() + + return result diff --git a/NanoVNASaver/NanoVNASaver.py b/NanoVNASaver/NanoVNASaver.py index a6a3317..5b535c5 100644 --- a/NanoVNASaver/NanoVNASaver.py +++ b/NanoVNASaver/NanoVNASaver.py @@ -24,6 +24,7 @@ from time import strftime, localtime from PyQt5 import QtWidgets, QtCore, QtGui +from NanoVNASaver import Defaults from .Windows import ( AboutWindow, AnalysisWindow, CalibrationWindow, DeviceSettingsWindow, DisplaySettingsWindow, SweepSettingsWindow, @@ -69,10 +70,11 @@ class NanoVNASaver(QtWidgets.QWidget): else: self.icon = QtGui.QIcon("icon_48x48.png") self.setWindowIcon(self.icon) - self.settings = QtCore.QSettings(QtCore.QSettings.IniFormat, + self.settings = Defaults.AppSettings(QtCore.QSettings.IniFormat, QtCore.QSettings.UserScope, "NanoVNASaver", "NanoVNASaver") logger.info("Settings from: %s", self.settings.fileName()) + Defaults.cfg = Defaults.restore(self.settings) self.threadpool = QtCore.QThreadPool() self.sweep = Sweep() self.worker = SweepWorker(self) @@ -121,9 +123,8 @@ class NanoVNASaver(QtWidgets.QWidget): outer.addWidget(scrollarea) self.setLayout(outer) scrollarea.setWidgetResizable(True) - window_width = self.settings.value("WindowWidth", 1350, type=int) - window_height = self.settings.value("WindowHeight", 950, type=int) - self.resize(window_width, window_height) + self.resize(Defaults.cfg.gui.window_width, + Defaults.cfg.gui.window_height) scrollarea.setSizePolicy( QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, @@ -203,18 +204,14 @@ class NanoVNASaver(QtWidgets.QWidget): left_column = QtWidgets.QVBoxLayout() right_column = QtWidgets.QVBoxLayout() right_column.addLayout(self.charts_layout) - self.marker_frame.setHidden( - not self.settings.value("MarkersVisible", True, bool)) + self.marker_frame.setHidden(Defaults.cfg.gui.markers_hidden) chart_widget = QtWidgets.QWidget() chart_widget.setLayout(right_column) self.splitter = QtWidgets.QSplitter() self.splitter.addWidget(self.marker_frame) self.splitter.addWidget(chart_widget) - try: - self.splitter.restoreState(self.settings.value("SplitterSizes")) - except TypeError: - pass + self.splitter.restoreState(Defaults.cfg.gui.splitter_sizes) layout.addLayout(left_column) layout.addWidget(self.splitter, 2) @@ -638,17 +635,18 @@ class NanoVNASaver(QtWidgets.QWidget): def closeEvent(self, a0: QtGui.QCloseEvent) -> None: self.worker.stopped = True - self.settings.setValue("MarkerCount", Marker.count()) for marker in self.markers: marker.update_settings() - - self.settings.setValue("WindowHeight", self.height()) - self.settings.setValue("WindowWidth", self.width()) - self.settings.setValue("SplitterSizes", self.splitter.saveState()) - self.settings.sync() self.bands.saveSettings() self.threadpool.waitForDone(2500) + + Defaults.cfg.chart.marker_count = Marker.count() + Defaults.cfg.gui.window_width = self.width() + Defaults.cfg.gui.window_height = self.height() + Defaults.cfg.gui.splitter_sizes = bytearray(self.splitter.saveState()) + Defaults.store(self.settings, Defaults.cfg) + a0.accept() sys.exit() diff --git a/NanoVNASaver/SITools.py b/NanoVNASaver/SITools.py index b952e05..5bd03f9 100644 --- a/NanoVNASaver/SITools.py +++ b/NanoVNASaver/SITools.py @@ -34,6 +34,13 @@ def clamp_value(value: Real, rmin: Real, rmax: Real) -> Real: return rmax return value +def round_ceil(value: Real, digits: int=0) -> Real: + factor = 10 ** digits + return factor * math.ceil(value / factor) + +def round_floor(value: Real, digits: int=0) -> Real: + factor = 10 ** digits + return factor * math.floor(value / factor) class Format(NamedTuple): max_nr_digits: int = 6 @@ -74,8 +81,8 @@ class Value: self._value = decimal.Decimal(value, context=Value.CTX) def __repr__(self) -> str: - return (f"{self.__class__.__name__}(" + repr(self._value) + - f", '{self._unit}', {self.fmt})") + return (f"{self.__class__.__name__}(" + f"{repr(self._value)}, '{self._unit}', {self.fmt})") def __str__(self) -> str: fmt = self.fmt @@ -100,10 +107,10 @@ class Value: max_digits = fmt.max_nr_digits + ( (1 if not fmt.fix_decimals and abs(real) < 10 else 0) + (1 if not fmt.fix_decimals and abs(real) < 100 else 0)) - formstr = "." + str(max_digits - 3) + "f" + formstr = f".{max_digits - 3}f" if self.fmt.allways_signed: - formstr = "+" + formstr + formstr = f"+{formstr}" result = format(real, formstr) if float(result) == 0.0: diff --git a/NanoVNASaver/Windows/About.py b/NanoVNASaver/Windows/About.py index f269a96..0d8f30f 100644 --- a/NanoVNASaver/Windows/About.py +++ b/NanoVNASaver/Windows/About.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import contextlib import logging from time import strftime, localtime from urllib import request, error @@ -54,7 +55,7 @@ class AboutWindow(QtWidgets.QWidget): layout.addWidget(QtWidgets.QLabel("")) layout.addWidget(QtWidgets.QLabel( "\N{COPYRIGHT SIGN} Copyright 2019, 2020 Rune B. Broberg\n" - "\N{COPYRIGHT SIGN} Copyright 2020 NanoVNA-Saver Authors" + "\N{COPYRIGHT SIGN} Copyright 2020ff NanoVNA-Saver Authors" )) layout.addWidget(QtWidgets.QLabel( "This program comes with ABSOLUTELY NO WARRANTY")) @@ -79,21 +80,6 @@ class AboutWindow(QtWidgets.QWidget): btn_check_version.clicked.connect(self.findUpdates) self.updateLabel = QtWidgets.QLabel("Last checked: ") - self.updateCheckBox = QtWidgets.QCheckBox( - "Check for updates on startup") - - self.updateCheckBox.toggled.connect(self.updateSettings) - - check_for_updates = self.app.settings.value( - "CheckForUpdates", "Ask") - if check_for_updates == "Yes": - self.updateCheckBox.setChecked(True) - self.findUpdates(automatic=True) - elif check_for_updates == "No": - self.updateCheckBox.setChecked(False) - else: - logger.debug("Starting timer") - QtCore.QTimer.singleShot(2000, self.askAboutUpdates) update_hbox = QtWidgets.QHBoxLayout() update_hbox.addWidget(btn_check_version) @@ -101,7 +87,6 @@ class AboutWindow(QtWidgets.QWidget): update_hbox.addLayout(update_form) update_hbox.addStretch() update_form.addRow(self.updateLabel) - update_form.addRow(self.updateCheckBox) layout.addLayout(update_hbox) layout.addStretch() @@ -115,39 +100,10 @@ class AboutWindow(QtWidgets.QWidget): self.updateLabels() def updateLabels(self): - try: + with contextlib.suppress(IOError, AttributeError): self.versionLabel.setText( f"NanoVNA Firmware Version: {self.app.vna.name} " f"v{self.app.vna.version}") - except (IOError, AttributeError): - pass - - def updateSettings(self): - if self.updateCheckBox.isChecked(): - self.app.settings.setValue("CheckForUpdates", "Yes") - else: - self.app.settings.setValue("CheckForUpdates", "No") - - def askAboutUpdates(self): - logger.debug("Asking about automatic update checks") - selection = QtWidgets.QMessageBox.question( - self.app, - "Enable checking for updates?", - "Would you like NanoVNA-Saver to" - " check for updates automatically?") - if selection == QtWidgets.QMessageBox.Yes: - self.updateCheckBox.setChecked(True) - self.app.settings.setValue("CheckForUpdates", "Yes") - self.findUpdates() - elif selection == QtWidgets.QMessageBox.No: - self.updateCheckBox.setChecked(False) - self.app.settings.setValue("CheckForUpdates", "No") - QtWidgets.QMessageBox.information( - self.app, - "Checking for updates disabled", - 'You can check for updates using the "About" window.') - else: - self.app.settings.setValue("CheckForUpdates", "Ask") def findUpdates(self, automatic=False): latest_version = Version() diff --git a/NanoVNASaver/Windows/DisplaySettings.py b/NanoVNASaver/Windows/DisplaySettings.py index e681906..27c892f 100644 --- a/NanoVNASaver/Windows/DisplaySettings.py +++ b/NanoVNASaver/Windows/DisplaySettings.py @@ -2,7 +2,7 @@ # # A python program to view and export Touchstone data from a NanoVNA # Copyright (C) 2019, 2020 Rune B. Broberg -# Copyright (C) 2020,2021 NanoVNA-Saver Authors +# Copyright (C) 2020ff NanoVNA-Saver Authors # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,8 +21,9 @@ from typing import List from PyQt5 import QtWidgets, QtCore, QtGui +from NanoVNASaver import Defaults from NanoVNASaver.Charts.Chart import ( - Chart, ChartColors, ChartMarker, ChartMarkerConfig) + Chart, ChartColors) from NanoVNASaver.Windows.Bands import BandsWindow from NanoVNASaver.Windows.MarkerSettings import MarkerSettingsWindow from NanoVNASaver.Marker import Marker @@ -37,7 +38,6 @@ class DisplaySettingsWindow(QtWidgets.QWidget): self.app = app self.setWindowTitle("Display settings") self.setWindowIcon(self.app.icon) - self.marker_cfg = ChartMarkerConfig() self.marker_window = MarkerSettingsWindow(self.app) self.callback_params = {} @@ -61,10 +61,10 @@ class DisplaySettingsWindow(QtWidgets.QWidget): display_options_layout.addRow("Return loss is:", self.returnloss_is_negative) display_options_layout.addRow("", self.returnloss_is_positive) - if self.app.settings.value("ReturnLossPositive", False, bool): - self.returnloss_is_positive.setChecked(True) - else: - self.returnloss_is_negative.setChecked(True) + self.returnloss_is_positive.setChecked( + Defaults.cfg.chart.returnloss_is_positive) + self.returnloss_is_negative.setChecked( + not Defaults.cfg.chart.returnloss_is_positive) self.returnloss_is_positive.toggled.connect(self.changeReturnLoss) self.changeReturnLoss() @@ -83,7 +83,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget): self.pointSizeInput = QtWidgets.QSpinBox() self.pointSizeInput.setMinimumHeight(20) - pointsize = self.app.settings.value("PointSize", 2, int) + pointsize = Defaults.cfg.chart.point_size self.pointSizeInput.setValue(pointsize) self.changePointSize(pointsize) self.pointSizeInput.setMinimum(1) @@ -95,7 +95,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget): self.lineThicknessInput = QtWidgets.QSpinBox() self.lineThicknessInput.setMinimumHeight(20) - linethickness = self.app.settings.value("LineThickness", 1, int) + linethickness = Defaults.cfg.chart.line_thickness self.lineThicknessInput.setValue(linethickness) self.changeLineThickness(linethickness) self.lineThicknessInput.setMinimum(1) @@ -107,10 +107,9 @@ class DisplaySettingsWindow(QtWidgets.QWidget): self.markerSizeInput = QtWidgets.QSpinBox() self.markerSizeInput.setMinimumHeight(20) - markersize = self.app.settings.value("MarkerSize", 6, int) + markersize = Defaults.cfg.chart.marker_size self.markerSizeInput.setValue(markersize) self.markerSizeInput.setMinimum(4) - self.markerSizeInput.setMinimum(4) self.markerSizeInput.setMaximum(20) self.markerSizeInput.setSingleStep(2) self.markerSizeInput.setSuffix(" px") @@ -137,10 +136,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget): display_options_layout.addRow("Data point is:", self.marker_at_center) display_options_layout.addRow("", self.marker_at_tip) - if self.app.settings.value("MarkerAtTip", False, bool): - self.marker_at_tip.setChecked(True) - else: - self.marker_at_center.setChecked(True) + self.marker_at_tip.setChecked(Defaults.cfg.chart.marker_at_tip) + self.marker_at_center.setChecked(not Defaults.cfg.chart.marker_at_tip) self.marker_at_tip.toggled.connect(self.changeMarkerAtTip) self.changeMarkerAtTip() @@ -162,10 +159,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget): self.font_dropdown = QtWidgets.QComboBox() self.font_dropdown.setMinimumHeight(20) self.font_dropdown.addItems(["7", "8", "9", "10", "11", "12"]) - font_size = self.app.settings.value("FontSize", - defaultValue="8", - type=str) - self.font_dropdown.setCurrentText(font_size) + self.font_dropdown.setCurrentText(str(Defaults.cfg.gui.font_size)) self.changeFont() self.font_dropdown.currentTextChanged.connect(self.changeFont) @@ -196,11 +190,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget): self.vswrMarkers: List[float] = self.app.settings.value("VSWRMarkers", [], float) if isinstance(self.vswrMarkers, float): - if self.vswrMarkers == 0: - self.vswrMarkers = [] - else: - # Single values from the .ini become floats rather than lists. Convert them. - self.vswrMarkers = [self.vswrMarkers] + # Single values from the .ini become floats rather than lists. Convert them. + self.vswrMarkers = [] if self.vswrMarkers == 0.0 else [self.vswrMarkers] vswr_marker_layout.addRow( "VSWR Markers",self.color_picker("VSWRColor", "swr")) @@ -209,7 +200,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget): self.vswr_marker_dropdown.setMinimumHeight(20) vswr_marker_layout.addRow(self.vswr_marker_dropdown) - if len(self.vswrMarkers) == 0: + if not self.vswrMarkers: self.vswr_marker_dropdown.addItem("None") else: for m in self.vswrMarkers: @@ -262,10 +253,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget): # "S21 Phase", # "None"] - selections = [] - - for c in self.app.selectable_charts: - selections.append(c.name) + selections = [c.name for c in self.app.selectable_charts] selections.append("None") chart00_selection = QtWidgets.QComboBox() @@ -364,14 +352,11 @@ class DisplaySettingsWindow(QtWidgets.QWidget): "VSWRColor", defaultValue=ChartColors.swr, type=QtGui.QColor) - self.dark_mode_option.setChecked( - self.app.settings.value("DarkMode", False, bool)) - self.show_lines_option.setChecked( - self.app.settings.value("ShowLines", False, bool)) + self.dark_mode_option.setChecked(Defaults.cfg.gui.dark_mode) + self.show_lines_option.setChecked(Defaults.cfg.chart.show_lines) self.show_marker_number_option.setChecked( - self.app.settings.value("ShowMarkerNumbers", ChartMarkerConfig.draw_label, bool)) - self.filled_marker_option.setChecked( - self.app.settings.value("FilledMarkers", ChartMarkerConfig.fill, bool)) + Defaults.cfg.chart.marker_label) + self.filled_marker_option.setChecked(Defaults.cfg.chart.marker_filled) if self.app.settings.value("UseCustomColors", defaultValue=False, type=bool): @@ -450,8 +435,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget): def changeReturnLoss(self): state = self.returnloss_is_positive.isChecked() - self.app.settings.setValue("ReturnLossPositive", state) - + Defaults.cfg.chart.returnloss_is_positive = bool(state) for m in self.app.markers: m.returnloss_is_positive = state m.updateLabels(self.app.data.s11, self.app.data.s21) @@ -462,56 +446,51 @@ class DisplaySettingsWindow(QtWidgets.QWidget): def changeShowLines(self): state = self.show_lines_option.isChecked() - self.app.settings.setValue("ShowLines", state) + Defaults.cfg.chart.show_lines = bool(state) for c in self.app.subscribing_charts: c.setDrawLines(state) def changeShowMarkerNumber(self): - state = self.show_marker_number_option.isChecked() - self.app.settings.setValue("ShowMarkerNumbers", state) - ChartMarker.cfg.draw_label = state + Defaults.cfg.chart.marker_label = bool( + self.show_marker_number_option.isChecked()) self.updateCharts() def changeFilledMarkers(self): - state = self.filled_marker_option.isChecked() - self.app.settings.setValue("FilledMarkers", state) - ChartMarker.cfg.fill = state + Defaults.cfg.chart.marker_filled = bool( + self.filled_marker_option.isChecked()) self.updateCharts() def changeMarkerAtTip(self): - state = self.marker_at_tip.isChecked() - self.app.settings.setValue("MarkerAtTip", state) - ChartMarker.cfg.at_tip = state + Defaults.cfg.chart.marker_at_tip = bool( + self.marker_at_tip.isChecked()) self.updateCharts() def changePointSize(self, size: int): - self.app.settings.setValue("PointSize", size) + Defaults.cfg.chart.point_size = size for c in self.app.subscribing_charts: c.setPointSize(size) def changeLineThickness(self, size: int): - self.app.settings.setValue("LineThickness", size) + Defaults.cfg.chart.line_thickness = int(size) for c in self.app.subscribing_charts: c.setLineThickness(size) def changeMarkerSize(self, size: int): - self.app.settings.setValue("MarkerSize", size) - ChartMarker.cfg.size = size + Defaults.cfg.chart.marker_size = size self.markerSizeInput.setValue(size) self.updateCharts() def changeDarkMode(self): state = self.dark_mode_option.isChecked() - self.app.settings.setValue("DarkMode", state) + Defaults.cfg.gui.dark_mode = bool(state) Chart.color.foreground = QtGui.QColor(QtCore.Qt.lightGray) if state: Chart.color.background = QtGui.QColor(QtCore.Qt.black) Chart.color.text = QtGui.QColor(QtCore.Qt.white) - Chart.color.swr = Chart.color.swr else: Chart.color.background = QtGui.QColor(QtCore.Qt.white) Chart.color.text = QtGui.QColor(QtCore.Qt.black) - Chart.color.swr = Chart.color.swr + Chart.color.swr = Chart.color.swr self.updateCharts() def changeSetting(self, setting: str, value: str): @@ -547,11 +526,11 @@ class DisplaySettingsWindow(QtWidgets.QWidget): c.update() def changeFont(self): - font_size = self.font_dropdown.currentText() - self.app.settings.setValue("FontSize", font_size) + font_size = int(self.font_dropdown.currentText()) + Defaults.cfg.gui.font_size = font_size app: QtWidgets.QApplication = QtWidgets.QApplication.instance() font = app.font() - font.setPointSize(int(font_size)) + font.setPointSize(font_size) app.setFont(font) self.app.changeFont(font) @@ -628,3 +607,4 @@ class DisplaySettingsWindow(QtWidgets.QWidget): def updateCharts(self): for c in self.app.subscribing_charts: c.update() + Defaults.store(self.app.settings, Defaults.cfg) \ No newline at end of file diff --git a/debug.sh b/debug.sh new file mode 100755 index 0000000..9385eb2 --- /dev/null +++ b/debug.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec python -m debugpy --listen 5678 --wait-for-client $@ diff --git a/nanovna-saver.py b/nanovna-saver.py index f76c875..44adaa6 100755 --- a/nanovna-saver.py +++ b/nanovna-saver.py @@ -15,10 +15,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -try: +from contextlib import suppress + +with suppress(ImportError): + # pylint: disable=no-name-in-module,import-error,unused-import + # pyright: reportMissingImports=false import pkg_resources.py2_warn -except ImportError: - pass + from NanoVNASaver.__main__ import main if __name__ == '__main__': diff --git a/test/test_settings.py b/test/test_settings.py new file mode 100644 index 0000000..d6f0f08 --- /dev/null +++ b/test/test_settings.py @@ -0,0 +1,88 @@ +# NanoVNASaver +# +# A python program to view and export Touchstone data from a NanoVNA +# Copyright (C) 2019, 2020 Rune B. Broberg +# Copyright (C) 2020,2021 NanoVNA-Saver Authors +# +# This program 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. +# +# This program 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 this program. If not, see . +import unittest +from dataclasses import dataclass, field + +import NanoVNASaver.Defaults as CFG + + +@dataclass +class TConfig: + my_int: int = 3 + my_float: float = 3.14 + my_str: str = "Hello World" + my_bool: bool = True + my_list: list = field(default_factory=lambda: [1, 2, 3]) + my_bytearray: bytearray = field(default_factory=lambda: bytearray((1,2,3))) + + +class TestCases(unittest.TestCase): + + def setUp(self) -> None: + self.settings_1 = CFG.AppSettings( + CFG.QSettings.IniFormat, + CFG.QSettings.UserScope, + "NanoVNASaver", "Test_1") + self.settings_2 = CFG.AppSettings( + CFG.QSettings.IniFormat, + CFG.QSettings.UserScope, + "NanoVNASaver", "Test_2") + self.config_1 = TConfig() + self.config_2 = TConfig( + my_int=4, + my_float=3.0, + my_str="Goodbye World", + my_bool=False, + my_list=[4, 5, 6]) + + def test_store_dataclass(self): + self.settings_1.store_dataclass("Section1", self.config_1) + self.settings_1.store_dataclass("Section2", self.config_2) + illegal_config = TConfig( + my_int=4, my_float=3.0, my_str="Goodbye World", + my_bool="False", my_list=(4, 5, 6)) + with self.assertRaises(AssertionError): + self.settings_1.store_dataclass("SectionX", illegal_config) + + def test_restore_dataclass(self): + tc_1 = self.settings_1.restore_dataclass("Section1", TConfig()) + tc_2 = self.settings_1.restore_dataclass("Section2", TConfig()) + self.assertNotEqual(tc_1, tc_2) + self.assertEqual(tc_1, self.config_1) + self.assertEqual(tc_2, self.config_2) + self.assertEqual(tc_2.my_int, 4) + self.assertEqual(tc_2.my_float, 3.0) + self.assertEqual(tc_2.my_str, "Goodbye World") + self.assertEqual(tc_2.my_bool, False) + self.assertEqual(tc_2.my_list, [4, 5, 6]) + self.assertIsInstance(tc_2.my_int, int) + self.assertIsInstance(tc_2.my_float, float) + + def test_restore_empty(self): + tc_3 = self.settings_1.restore_dataclass("Section3", TConfig()) + self.assertEqual(tc_3, TConfig()) + + def test_store(self): + tc_1 = CFG.CFG() + tc_1.gui.dark_mode = not tc_1.gui.dark_mode + CFG.store(self.settings_2, tc_1) + tc_2 = CFG.restore(self.settings_2) + print(f"\n{tc_1}\n{tc_2}\n") + self.assertEqual(tc_1, tc_2) + self.assertNotEqual(tc_2.gui, CFG.GUI()) \ No newline at end of file