diff --git a/grc/CMakeLists.txt b/grc/CMakeLists.txt index e931ab7..59e19f5 100644 --- a/grc/CMakeLists.txt +++ b/grc/CMakeLists.txt @@ -29,5 +29,6 @@ install(FILES crfa_decoder_compare.xml crfa_diff_add_sync_decim.xml crfa_sync_decim.xml - crfa_rds_decoder_redsea.xml DESTINATION share/gnuradio/grc/blocks + crfa_rds_decoder_redsea.xml + crfa_qtgui_range.xml DESTINATION share/gnuradio/grc/blocks ) diff --git a/grc/crfa_qtgui_range.xml b/grc/crfa_qtgui_range.xml new file mode 100644 index 0000000..42691dd --- /dev/null +++ b/grc/crfa_qtgui_range.xml @@ -0,0 +1,130 @@ + + + + qtgui_range (cr) + variable_crfa_qtgui_range + [crfa] + import crfa + from crfa.qtgui_range import qtgui_range, RangeWidget + self.$(id) = $(id) = $value + #set $win = 'self._%s_win'%$id + #set $range = 'self.%s_range'%$id +#if not $label() + #set $label = '"%s"'%$id +#end if +$(range) = qtgui_range($start, $stop, $step, $value, $min_len) +$(win) = RangeWidget($range, self.set_$(id), $label, "$widget", $rangeType) +$(gui_hint()($win)) + self.freq2_range.set_test($value); + self.set_$(id)($value) + + + + Label + label + + string + #if $label() then 'none' else 'part'# + + + + Type + rangeType + "float" + enum + part + + + + + + Default Value + value + 50 + $rangeType.type + + + + Start + start + 0 + $rangeType.type + + + + Stop + stop + 100 + $rangeType.type + + + + Step + step + 1 + $rangeType.type + + + + Widget + widget + counter_slider + enum + part + + + + + + + + Orientation + orient + Qt.Horizontal + enum + #if $widget() == "slider" then 'part' else 'all'# + + + + + + Minimum Length + min_len + 200 + int + part + + + + + GUI Hint + gui_hint + + gui_hint + part + + + $start <= $value <= $stop + $start < $stop + + + This block creates a variable with a slider. \ + Leave the label blank to use the variable id as the label. \ + The value must be a real number. \ + The value must be between the start and the stop. + + The GUI hint can be used to position the widget within the application. \ + The hint is of the form [tab_id@tab_index]: [row, col, row_span, col_span]. \ + Both the tab specification and the grid position are optional. + + diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index ad66a93..6c8ac50 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -40,7 +40,8 @@ GR_PYTHON_INSTALL( chart.py stream_selector.py vector_cutter.py - decoder_compare.py DESTINATION ${GR_PYTHON_DIR}/crfa + decoder_compare.py + qtgui_range.py DESTINATION ${GR_PYTHON_DIR}/crfa ) ######################################################################## diff --git a/python/__init__.py b/python/__init__.py index e9a4272..7cb96be 100644 --- a/python/__init__.py +++ b/python/__init__.py @@ -41,4 +41,5 @@ from chart import Chart from stream_selector import stream_selector from vector_cutter import vector_cutter from decoder_compare import decoder_compare +from qtgui_range import qtgui_range # diff --git a/python/qtgui_range.py b/python/qtgui_range.py new file mode 100644 index 0000000..1a0bc1d --- /dev/null +++ b/python/qtgui_range.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2015 Free Software Foundation, Inc. +# +# This file is part of GNU Radio +# +# GNU Radio 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, or (at your option) +# any later version. +# +# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# + +from PyQt4 import Qt, QtCore, QtGui + + +class qtgui_range(object): + def set_test(self,value): + print("test callback invoked") + print(value) + def __init__(self, minv, maxv, step, default, min_length): + self.min = float(minv) + self.max = float(maxv) + self.step = float(step) + self.default = float(default) + self.min_length = min_length + self.find_precision() + self.find_nsteps() + + def find_precision(self): + # Get the decimal part of the step + temp = str(float(self.step) - int(self.step))[2:] + precision = len(temp) if temp is not '0' else 0 + precision = min(precision, 13) + + if precision == 0 and self.max < 100: + self.precision = 1 # Always have a decimal in this case + else: + self.precision = (precision + 2) if precision > 0 else 0 + + def find_nsteps(self): + self.nsteps = (self.max + self.step - self.min)/self.step + + def demap_range(self, val): + if val > self.max: + val = self.max + if val < self.min: + val = self.min + return ((val-self.min)/self.step) + + def map_range(self, val): + if val > self.nsteps: + val = self.max + if val < 0: + val = 0 + return (val*self.step+self.min) + + +class RangeWidget(QtGui.QWidget): + def set_test(value): + print("test callback invoked on widget") + print(value) + def __init__(self, ranges, slot, label, style, rangeType=float): + """ Creates the QT Range widget """ + QtGui.QWidget.__init__(self) + + self.range = ranges + self.style = style + + # rangeType tells the block how to return the value as a standard + self.rangeType = rangeType + + # Top-block function to call when any value changes + # Some widgets call this directly when their value changes. + # Others have intermediate functions to map the value into the right range. + self.notifyChanged = slot + + layout = Qt.QHBoxLayout() + label = Qt.QLabel(label) + layout.addWidget(label) + + if style == "dial": + self.d_widget = self.Dial(self, self.range, self.notifyChanged, rangeType) + elif style == "slider": + self.d_widget = self.Slider(self, self.range, self.notifyChanged, rangeType) + elif style == "counter": + # The counter widget can be directly wired to the notifyChanged slot + self.d_widget = self.Counter(self, self.range, self.notifyChanged, rangeType) + else: + # The CounterSlider needs its own internal handlers before calling notifyChanged + self.d_widget = self.CounterSlider(self, self.range, self.notifyChanged, rangeType) + + layout.addWidget(self.d_widget) + self.setLayout(layout) + + class Dial(QtGui.QDial): + """ Creates the range using a dial """ + def __init__(self, parent, ranges, slot, rangeType=float): + QtGui.QDial.__init__(self, parent) + + self.rangeType = rangeType + + # Setup the dial + self.setRange(0, ranges.nsteps-1) + self.setSingleStep(1) + self.setNotchesVisible(True) + self.range = ranges + + # Round the initial value to the closest tick + temp = int(round(ranges.demap_range(ranges.default), 0)) + self.setValue(temp) + + # Setup the slots + self.valueChanged.connect(self.changed) + self.notifyChanged = slot + + def changed(self, value): + """ Handles maping the value to the right range before calling the slot. """ + val = self.range.map_range(value) + self.notifyChanged(self.rangeType(val)) + + class Slider(QtGui.QSlider): + """ Creates the range using a slider """ + def __init__(self, parent, ranges, slot, rangeType=float): + QtGui.QSlider.__init__(self, QtCore.Qt.Horizontal, parent) + + self.rangeType = rangeType + + # Setup the slider + #self.setFocusPolicy(QtCore.Qt.NoFocus) + self.setRange(0, ranges.nsteps - 1) + self.setTickPosition(2) + self.setSingleStep(1) + self.range = ranges + + # Round the initial value to the closest tick + temp = int(round(ranges.demap_range(ranges.default), 0)) + self.setValue(temp) + + if ranges.nsteps > ranges.min_length: + interval = int(ranges.nsteps/ranges.min_length) + self.setTickInterval(interval) + self.setPageStep(interval) + else: + self.setTickInterval(1) + self.setPageStep(1) + + # Setup the handler function + self.valueChanged.connect(self.changed) + self.notifyChanged = slot + + def changed(self, value): + """ Handle the valueChanged signal and map the value into the correct range """ + print("gui changed") + val = self.range.map_range(value) + self.notifyChanged(self.rangeType(val)) + + def mousePressEvent(self, event): + if((event.button() == QtCore.Qt.LeftButton)): + new = self.minimum() + ((self.maximum()-self.minimum()) * event.x()) / self.width() + self.setValue(new) + event.accept() + # Use repaint rather than calling the super mousePressEvent. + # Calling super causes issue where slider jumps to wrong value. + QtGui.QSlider.repaint(self) + + def mouseMoveEvent(self, event): + new = self.minimum() + ((self.maximum()-self.minimum()) * event.x()) / self.width() + self.setValue(new) + event.accept() + QtGui.QSlider.repaint(self) + + class Counter(QtGui.QDoubleSpinBox): + """ Creates the range using a counter """ + def __init__(self, parent, ranges, slot, rangeType=float): + QtGui.QDoubleSpinBox.__init__(self, parent) + + self.rangeType = rangeType + + # Setup the counter + self.setRange(ranges.min, ranges.max) + self.setValue(ranges.default) + self.setSingleStep(ranges.step) + self.setKeyboardTracking(False) + self.setDecimals(ranges.precision) + + # The counter already handles floats and can be connected directly. + self.valueChanged.connect(self.changed) + self.notifyChanged = slot + + def changed(self, value): + """ Handle the valueChanged signal by converting to the right type """ + self.notifyChanged(self.rangeType(value)) + + class CounterSlider(QtGui.QWidget): + """ Creates the range using a counter and slider """ + def __init__(self, parent, ranges, slot, rangeType=float): + QtGui.QWidget.__init__(self, parent) + + self.rangeType = rangeType + + # Slot to call in the parent + self.notifyChanged = slot + + self.slider = RangeWidget.Slider(parent, ranges, self.sliderChanged, rangeType) + self.counter = RangeWidget.Counter(parent, ranges, self.counterChanged, rangeType) + + # Need another horizontal layout to wrap the other widgets. + layout = Qt.QHBoxLayout() + layout.addWidget(self.slider) + layout.addWidget(self.counter) + self.setLayout(layout) + + # Flag to ignore the slider event caused by a change to the counter. + self.ignoreSlider = False + self.range = ranges + + def sliderChanged(self, value): + """ Handles changing the counter when the slider is updated """ + # If the counter was changed, ignore any of these events + if not self.ignoreSlider: + # Value is already float. Just set the counter + self.counter.setValue(self.rangeType(value)) + self.notifyChanged(self.rangeType(value)) + self.ignoreSlider = False + + def counterChanged(self, value): + """ Handles changing the slider when the counter is updated """ + # Get the current slider value and check to see if the new value changes it + current = self.slider.value() + new = int(round(self.range.demap_range(value), 0)) + + # If it needs to change, ignore the slider event + # Otherwise, the slider will cause the counter to round to the nearest tick + if current != new: + self.ignoreSlider = True + self.slider.setValue(new) + + self.notifyChanged(self.rangeType(value)) + + +if __name__ == "__main__": + from PyQt4 import Qt + import sys + + def valueChanged(frequency): + print("Value updated - " + str(frequency)) + + app = Qt.QApplication(sys.argv) + widget = RangeWidget(qtgui_range(0, 100, 10, 1, 100), valueChanged, "Test", "counter_slider", int) + + widget.show() + widget.setWindowTitle("Test Qt Range") + app.exec_() + + widget = None diff --git a/python/qtguitest.py b/python/qtguitest.py index ceada01..e1f5043 100644 --- a/python/qtguitest.py +++ b/python/qtguitest.py @@ -125,6 +125,7 @@ class CRWidget(QtGui.QWidget): self.location_filter=QtGui.QLineEdit() self.button = QtGui.QPushButton("i am a button") + self.button.clicked.connect(self.onCLick) layout.addWidget(self.button) #self.filter_label=QtGui.QLabel() diff --git a/python/rds_parser_table_qt.py b/python/rds_parser_table_qt.py index 1f92c06..4e90299 100644 --- a/python/rds_parser_table_qt.py +++ b/python/rds_parser_table_qt.py @@ -31,7 +31,7 @@ from PyQt4 import Qt, QtCore, QtGui import pprint,code,pickle#for easier testing pp = pprint.PrettyPrinter() import cProfile, pstats, StringIO #for profiling -pr = cProfile.Profile()#disabled-internal-profiling +pr = cProfile.Profile() #from threading import Timer#to periodically save DB @@ -1846,16 +1846,16 @@ class rds_parser_table_qt_Widget(QtGui.QWidget): details_button=QtGui.QPushButton("Detail") details_button.clicked.connect(functools.partial(self.getDetails, row=rowPosition)) button_layout.addWidget(details_button) - left_button=QtGui.QPushButton("L") - left_button.clicked.connect(functools.partial(self.setAudio, row=rowPosition,audio_channel="left")) - button_layout.addWidget(left_button) - right_button=QtGui.QPushButton("R") - right_button.clicked.connect(functools.partial(self.setAudio, row=rowPosition,audio_channel="right")) - button_layout.addWidget(right_button) - #self.table.setCellWidget(rowPosition,self.table.columnCount()-1,button_layout) + #2017-03-17 disabled LR buttons + #left_button=QtGui.QPushButton("L") + #left_button.clicked.connect(functools.partial(self.setAudio, row=rowPosition,audio_channel="left")) + #button_layout.addWidget(left_button) + #right_button=QtGui.QPushButton("R") + #right_button.clicked.connect(functools.partial(self.setAudio, row=rowPosition,audio_channel="right")) + #button_layout.addWidget(right_button) + cellWidget = QtGui.QWidget() cellWidget.setLayout(button_layout) - #button_col=self.table.columnCount()-1 button_col=3 self.table.setCellWidget(rowPosition,button_col,cellWidget) def display_data(self, event): @@ -1897,7 +1897,9 @@ class rds_parser_table_qt_Widget(QtGui.QWidget): item.setText(quality_string) if event.has_key('PTY'): item=self.table.cellWidget(row,self.colorder.index('PTY')) + tt=item.toolTip() item.setText(event['PTY']) + item.setToolTip(tt) if event.has_key('flags'): item=self.table.cellWidget(row,self.colorder.index('PTY')) item.setToolTip(Qt.QString(event['flags']))