#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2020 Free Software Foundation, Inc. # # This file is part of GNU Radio # # SPDX-License-Identifier: GPL-3.0-or-later # # from PyQt5.QtWidgets import QFrame, QVBoxLayout, QLabel from PyQt5.QtGui import QPainter, QPixmap, QFont, QFontMetrics, QBrush, QColor from PyQt5.QtCore import Qt, QSize from PyQt5 import QtCore from PyQt5.QtCore import Qt as Qtc from PyQt5.QtCore import pyqtSignal from gnuradio import gr import pmt # -------------- Support Classes --------------------------------- # # class LabeledDigitalNumberControl(QFrame): def __init__(self, lbl='', min_freq_hz=0, max_freq_hz=6000000000, parent=None, thousands_separator=',', background_color='black', fontColor='white', click_callback=None): QFrame.__init__(self, parent) self.numberControl = DigitalNumberControl(min_freq_hz, max_freq_hz, self, thousands_separator, background_color, fontColor, click_callback) layout = QVBoxLayout() self.lbl = QLabel(lbl, self) if len: self.hasLabel = True layout.addWidget(self.lbl) else: self.hasLabel = False layout.addWidget(self.numberControl) layout.setAlignment(Qtc.AlignCenter | Qtc.AlignVCenter) self.setLayout(layout) self.show() def minimumSizeHint(self): if self.hasLabel: return QSize(self.numberControl.minimumWidth()+10, 100) else: return QSize(self.numberControl.minimumWidth()+10, 50) def setReadOnly(self, b_read_only): self.numberControl.setReadOnly(b_read_only) def setFrequency(self, new_freq): self.numberControl.setFrequency(new_freq) def getFrequency(self): return self.numberControl.getFrequency() class DigitalNumberControl(QFrame): # Notifies to avoid thread conflicts on paints updateInt = pyqtSignal(int) updateFloat = pyqtSignal(float) def __init__(self, min_freq_hz=0, max_freq_hz=6000000000, parent=None, thousands_separator=',', background_color='black', fontColor='white', click_callback=None): QFrame.__init__(self, parent) self.updateInt.connect(self.onUpdateInt) self.updateFloat.connect(self.onUpdateFloat) self.min_freq = int(min_freq_hz) self.max_freq = int(max_freq_hz) self.numDigitsInFreq = len(str(max_freq_hz)) self.thousands_separator = thousands_separator self.click_callback = click_callback self.read_only = False self.setColors(QColor(background_color), QColor(fontColor)) self.numberFont = QFont("Arial", 12, QFont.Normal) self.cur_freq = min_freq_hz self.debug_click = False # Determine what our width minimum is teststr = "" for i in range(0, self.numDigitsInFreq): teststr += "0" fm = QFontMetrics(self.numberFont) if len(self.thousands_separator) > 0: # The -1 makes sure we don't count an extra for 123,456,789. Answer should be 2 not 3. numgroups = int(float(self.numDigitsInFreq-1) / 3.0) if numgroups > 0: for i in range(0, numgroups): teststr += self.thousands_separator textstr = teststr else: textstr = teststr width = fm.width(textstr) self.minwidth = width if self.minwidth < 410: self.minwidth = 410 self.setMaximumHeight(70) self.setMinimumWidth(self.minwidth) # Show the control self.show() def minimumSizeHint(self): return QSize(self.minwidth, 50) def setReadOnly(self, b_read_only): self.read_only = b_read_only def mousePressEvent(self, event): super(DigitalNumberControl, self).mousePressEvent(event) self.offset = event.pos() if self.read_only: if self.debug_click: gr.log.info("click received but read-only. Not changing frequency.") return fm = QFontMetrics(self.numberFont) if len(self.thousands_separator) > 0: if self.thousands_separator != ".": textstr = format(self.getFrequency(), self.thousands_separator) else: textstr = format(self.getFrequency(), ",") textstr = textstr.replace(",", ".") else: textstr = str(self.getFrequency()) width = fm.width(textstr) # So we know: # - the width of the text # - The mouse click position relative to 0 (pos relative to string start # will be size().width() - 2 - pos.x clickpos = self.size().width() - 2 - self.offset.x() found_number = False clicked_thousands = False for i in range(1, len(textstr)+1): width = fm.width(textstr[-i:]) charstr = textstr[-i:] widthchar = fm.width(charstr[0]) if clickpos >= (width-widthchar) and clickpos <= width: clicked_char = i-1 clicked_num_index = clicked_char found_number = True if len(self.thousands_separator) > 0: if charstr[0] != self.thousands_separator: numSeps = charstr.count(self.thousands_separator) clicked_num_index -= numSeps if self.debug_click: gr.log.info("clicked number: " + str(clicked_num_index)) else: clicked_thousands = True if self.debug_click: gr.log.info("clicked thousands separator") else: if self.debug_click: gr.log.info("clicked number: " + str(clicked_char)) # Remember y=0 is at the top so this is reversed clicked_up = False if self.offset.y() > self.size().height()/2: if self.debug_click: gr.log.info('clicked down') else: if self.debug_click: gr.log.info('clicked up') clicked_up = True if not clicked_thousands: cur_freq = self.getFrequency() increment = pow(10, clicked_num_index) if clicked_up: cur_freq += increment else: cur_freq -= increment # Cannot call setFrequency to emit. Change must happen now for # paint event when clicked. self.setFrequencyNow(cur_freq) if self.click_callback is not None: self.click_callback(self.getFrequency()) break if (not found_number) and (not clicked_thousands): # See if we clicked in the high area, if so, increment there. clicked_up = False if self.offset.y() > self.size().height()/2: if self.debug_click: gr.log.info('clicked down in the high area') else: if self.debug_click: gr.log.info('clicked up in the high area') clicked_up = True textstr = str(self.getFrequency()) numNumbers = len(textstr) increment = pow(10, numNumbers) cur_freq = self.getFrequency() if clicked_up: cur_freq += increment else: cur_freq -= increment # Cannot call setFrequency to emit. Change must happen now for # paint event when clicked. self.setFrequencyNow(cur_freq) if self.click_callback is not None: if self.debug_click: gr.log.info('Calling self.click_callback') self.click_callback(self.getFrequency()) else: if self.debug_click: gr.log.info('self.click_callback is None. Not calling callback.') def setColors(self, background, fontColor): self.background_color = background self.fontColor = fontColor def reverseString(self, astring): astring = astring[::-1] return astring def onUpdateInt(self, new_freq): if (new_freq >= self.min_freq) and (new_freq <= self.max_freq): self.cur_freq = int(new_freq) self.update() def onUpdateFloat(self, new_freq): if (new_freq >= self.min_freq) and (new_freq <= self.max_freq): self.cur_freq = int(new_freq) self.update() def setFrequencyNow(self, new_freq): # This setFrequency differs from setFrequency() in that it # it updates the display immediately rather than emitting # to the message queue as required during msg handling if (new_freq >= self.min_freq) and (new_freq <= self.max_freq): self.cur_freq = int(new_freq) self.update() def setFrequency(self, new_freq): if type(new_freq) == int: self.updateInt.emit(new_freq) else: self.updateFloat.emit(new_freq) def getFrequency(self): return self.cur_freq def resizeEvent(self, event): self.pxMap = QPixmap(self.size()) self.pxMap.fill(self.background_color) self.update() def paintEvent(self, event): super().paintEvent(event) painter = QPainter(self) size = self.size() brush = QBrush() brush.setColor(self.background_color) brush.setStyle(Qt.SolidPattern) rect = QtCore.QRect(2, 2, size.width()-4, size.height()-4) painter.fillRect(rect, brush) self.numberFont.setPixelSize(0.9 * size.height()) painter.setFont(self.numberFont) painter.setPen(self.fontColor) rect = event.rect() if len(self.thousands_separator) > 0: if self.thousands_separator != ".": textstr = format(self.getFrequency(), self.thousands_separator) else: textstr = format(self.getFrequency(), ",") textstr = textstr.replace(",", ".") else: textstr = str(self.getFrequency()) rect = QtCore.QRect(0, 0, size.width()-4, size.height()) painter.drawText(rect, Qt.AlignRight + Qt.AlignVCenter, textstr) # ################################################################################ # GNU Radio Class class MsgDigitalNumberControl(gr.sync_block, LabeledDigitalNumberControl): def __init__(self, lbl='', min_freq_hz=0, max_freq_hz=6000000000, parent=None, thousands_separator=',', background_color='black', fontColor='white', var_callback=None, outputmsgname='freq'): gr.sync_block.__init__(self, name="MsgDigitalNumberControl", in_sig=None, out_sig=None) LabeledDigitalNumberControl.__init__(self, lbl, min_freq_hz, max_freq_hz, parent, thousands_separator, background_color, fontColor, self.click_callback) self.var_callback = var_callback self.outputmsgname = outputmsgname self.message_port_register_in(pmt.intern("valuein")) self.set_msg_handler(pmt.intern("valuein"), self.msgHandler) self.message_port_register_out(pmt.intern("valueout")) def msgHandler(self, msg): try: new_val = pmt.to_python(pmt.cdr(msg)) if type(new_val) == float or type(new_val) == int: self.call_var_callback(new_val) self.setValue(new_val) else: gr.log.error("Value received was not an int or a float. %s" % str(type(new_val))) except Exception as e: gr.log.error("Error with message conversion: %s" % str(e)) def call_var_callback(self, new_value): if (self.var_callback is not None): if type(self.var_callback) is float: self.var_callback = float(new_value) else: self.var_callback(float(new_value)) def click_callback(self, new_value): self.call_var_callback(new_value) self.message_port_pub(pmt.intern("valueout"), pmt.cons(pmt.intern(self.outputmsgname), pmt.from_double(float(new_value)))) def setValue(self, new_val): self.setFrequency(new_val) self.message_port_pub(pmt.intern("valueout"), pmt.cons(pmt.intern(self.outputmsgname), pmt.from_double(float(self.getFrequency())))) def getValue(self): self.getFrequency() def setReadOnly(self, b_read_only): super().setReadOnly(b_read_only)