]> sigrok.org Git - sigrok-meter.git/blobdiff - mainwindow.py
Minor cosmetics and typo fixes.
[sigrok-meter.git] / mainwindow.py
index 680dd97b5eb5cded0d7cb7ba4b2ad304298975d7..560dfd3c02327a665f45c005bbe28203c9655bae 100644 (file)
 
 import acquisition
 import datamodel
+import datetime
 import icons
 import multiplotwidget
 import os.path
 import qtcompat
 import settings
+import sigrok.core as sr
+import sys
 import textwrap
 import time
 import util
@@ -35,7 +38,7 @@ QtGui = qtcompat.QtGui
 pyqtgraph = qtcompat.pyqtgraph
 
 class EmptyMessageListView(QtGui.QListView):
-    '''List view that shows a message if the model im empty.'''
+    '''List view that shows a message if the model is empty.'''
 
     def __init__(self, message, parent=None):
         super(self.__class__, self).__init__(parent)
@@ -54,9 +57,6 @@ class EmptyMessageListView(QtGui.QListView):
 class MainWindow(QtGui.QMainWindow):
     '''The main window of the application.'''
 
-    # Number of seconds that the plots display.
-    BACKLOG = 30
-
     # Update interval of the plots in milliseconds.
     UPDATEINTERVAL = 100
 
@@ -70,6 +70,9 @@ class MainWindow(QtGui.QMainWindow):
         self.context = context
         self.drivers = drivers
 
+        self.logModel = QtGui.QStringListModel(self)
+        self.context.set_log_callback(self._log_callback)
+
         self.delegate = datamodel.MultimeterDelegate(self, self.font())
         self.model = datamodel.MeasurementDataModel(self)
 
@@ -84,6 +87,8 @@ class MainWindow(QtGui.QMainWindow):
         self._plot_update_timer.setInterval(MainWindow.UPDATEINTERVAL)
         self._plot_update_timer.timeout.connect(self._updatePlots)
 
+        settings.graph.backlog.changed.connect(self.on_setting_graph_backlog_changed)
+
         QtCore.QTimer.singleShot(0, self._start_acquisition)
 
     def _start_acquisition(self):
@@ -101,6 +106,31 @@ class MainWindow(QtGui.QMainWindow):
 
         self.start_stop_acquisition()
 
+    def _log_callback(self, level, message):
+        if level.id > settings.logging.level.value().id:
+            return
+
+        t = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')
+        message = '[{}] sr: {}'.format(t, message)
+
+        sys.stderr.write(message + '\n')
+
+        scrollBar = self.logView.verticalScrollBar()
+        bottom = scrollBar.value() == scrollBar.maximum()
+
+        rows = self.logModel.rowCount()
+        maxrows = settings.logging.lines.value()
+        while rows > maxrows:
+            self.logModel.removeRows(0, 1)
+            rows -= 1
+
+        if self.logModel.insertRow(rows):
+            index = self.logModel.index(rows)
+            self.logModel.setData(index, message, QtCore.Qt.DisplayRole)
+
+            if bottom:
+                self.logView.scrollToBottom()
+
     def _setup_ui(self):
         self.setWindowTitle('sigrok-meter')
         # Resizing the listView below will increase this again.
@@ -153,27 +183,27 @@ class MainWindow(QtGui.QMainWindow):
         #actionAdd.setIcon(icons.add)
         #actionAdd.triggered.connect(self.showAddDevicePage)
 
-        #actionLog = self.sideBar.addAction('Logs')
-        #actionLog.setCheckable(True)
-        #actionLog.setIcon(icons.log)
-        #actionLog.triggered.connect(self.showLogPage)
+        actionLog = self.sideBar.addAction('Logs')
+        actionLog.setCheckable(True)
+        actionLog.setIcon(icons.log)
+        actionLog.triggered.connect(self.showLogPage)
 
-        #actionPreferences = self.sideBar.addAction('Preferences')
-        #actionPreferences.setCheckable(True)
-        #actionPreferences.setIcon(icons.preferences)
-        #actionPreferences.triggered.connect(self.showPreferencesPage)
+        actionPreferences = self.sideBar.addAction('Preferences')
+        actionPreferences.setCheckable(True)
+        actionPreferences.setIcon(icons.preferences)
+        actionPreferences.triggered.connect(self.showPreferencesPage)
 
-        # make the buttons at the top exclusive
+        # Make the buttons at the top exclusive.
         self.actionGroup = QtGui.QActionGroup(self)
         self.actionGroup.addAction(actionGraph)
         #self.actionGroup.addAction(actionAdd)
-        #self.actionGroup.addAction(actionLog)
-        #self.actionGroup.addAction(actionPreferences)
+        self.actionGroup.addAction(actionLog)
+        self.actionGroup.addAction(actionPreferences)
 
-        # show graph at startup
+        # Show graph at startup.
         actionGraph.setChecked(True)
 
-        # fill space between buttons on the top and on the bottom
+        # Fill space between buttons on the top and on the bottom.
         fill = QtGui.QWidget(self)
         fill.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
         self.sideBar.addWidget(fill)
@@ -245,14 +275,68 @@ class MainWindow(QtGui.QMainWindow):
     def _setup_logPage(self):
         self.logPage = QtGui.QWidget(self)
         layout = QtGui.QVBoxLayout(self.logPage)
-        label = QtGui.QLabel('log page')
-        layout.addWidget(label)
+
+        self.logView = QtGui.QListView(self)
+        self.logView.setModel(self.logModel)
+        self.logView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
+        self.logView.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
+        layout.addWidget(self.logView)
+
+        btn = QtGui.QPushButton('Save to file...', self)
+        btn.clicked.connect(self.on_save_log_clicked)
+        layout.addWidget(btn)
 
     def _setup_preferencesPage(self):
         self.preferencesPage = QtGui.QWidget(self)
-        layout = QtGui.QVBoxLayout(self.preferencesPage)
-        label = QtGui.QLabel('preferences page')
-        layout.addWidget(label)
+        layout = QtGui.QGridLayout(self.preferencesPage)
+
+        layout.addWidget(QtGui.QLabel('<b>Graph</b>'), 0, 0)
+        layout.addWidget(QtGui.QLabel('Recording time (seconds):'), 1, 0)
+
+        spin = QtGui.QSpinBox(self)
+        spin.setMinimum(10)
+        spin.setMaximum(3600)
+        spin.setSingleStep(10)
+        spin.setValue(settings.graph.backlog.value())
+        spin.valueChanged[int].connect(settings.graph.backlog.setValue)
+        layout.addWidget(spin, 1, 1)
+
+        layout.addWidget(QtGui.QLabel('<b>Logging</b>'), 2, 0)
+        layout.addWidget(QtGui.QLabel('Log level:'), 3, 0)
+
+        cbox = QtGui.QComboBox()
+        descriptions = [
+            'no messages at all',
+            'error messages',
+            'warnings',
+            'informational messages',
+            'debug messages',
+            'very noisy debug messages'
+        ]
+        for i, desc in enumerate(descriptions):
+            level = sr.LogLevel.get(i)
+            text = '{} ({})'.format(level.name, desc)
+            # The numeric log level corresponds to the index of the text in the
+            # combo box. Should this ever change, we could use the 'userData'
+            # that can also be stored in the item.
+            cbox.addItem(text)
+
+        cbox.setCurrentIndex(settings.logging.level.value().id)
+        cbox.currentIndexChanged[int].connect(
+            (lambda i: settings.logging.level.setValue(sr.LogLevel.get(i))))
+        layout.addWidget(cbox, 3, 1)
+
+        layout.addWidget(QtGui.QLabel('Number of lines to log:'), 4, 0)
+
+        spin = QtGui.QSpinBox(self)
+        spin.setMinimum(100)
+        spin.setMaximum(10 * 1000 * 1000)
+        spin.setSingleStep(100)
+        spin.setValue(settings.logging.lines.value())
+        spin.valueChanged[int].connect(settings.logging.lines.setValue)
+        layout.addWidget(spin, 4, 1)
+
+        layout.setRowStretch(layout.rowCount(), 100)
 
     def showPage(self, page):
         self.stackedWidget.setCurrentIndex(self._pages.index(page))
@@ -273,20 +357,35 @@ class MainWindow(QtGui.QMainWindow):
     def showPreferencesPage(self):
         self.showPage(self.preferencesPage)
 
+    @QtCore.Slot(int)
+    def on_setting_graph_backlog_changed(self, bl):
+        for unit in self._plots:
+            plot = self._plots[unit]
+
+            # Remove the limits first, otherwise the range update would
+            # be ignored.
+            plot.view.setLimits(xMin=None, xMax=None)
+
+            # Now change the range, and then use the calculated limits
+            # (also see the comment in '_getPlot()').
+            plot.view.setXRange(-bl, 0, update=True)
+            r = plot.view.viewRange()
+            plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
+
     def _getPlot(self, unit):
         '''Looks up or creates a new plot for 'unit'.'''
 
         if unit in self._plots:
             return self._plots[unit]
 
-        # create a new plot for the unit
+        # Create a new plot for the unit.
         plot = self.plotwidget.addPlot()
         plot.yaxis.setLabel(util.quantity_from_unit(unit), units=util.format_unit(unit))
-        plot.view.setXRange(-MainWindow.BACKLOG, 0, update=False)
+        plot.view.setXRange(-settings.graph.backlog.value(), 0, update=False)
         plot.view.setYRange(-1, 1)
         plot.view.enableAutoRange(axis=pyqtgraph.ViewBox.YAxis)
-        # lock to the range calculated by the view using additional padding,
-        # looks nicer this way
+        # Lock to the range calculated by the view using additional padding,
+        # looks nicer this way.
         r = plot.view.viewRange()
         plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
 
@@ -300,7 +399,7 @@ class MainWindow(QtGui.QMainWindow):
         if key in self._curves:
             return self._curves[key]
 
-        # create a new curve
+        # Create a new curve.
         curve = pyqtgraph.PlotDataItem(
             antialias=True,
             symbolPen=pyqtgraph.mkPen(QtGui.QColor(QtCore.Qt.black)),
@@ -315,7 +414,7 @@ class MainWindow(QtGui.QMainWindow):
     def _updatePlots(self):
         '''Updates all plots.'''
 
-        # loop over all devices and channels
+        # Loop over all devices and channels.
         for row in range(self.model.rowCount()):
             idx = self.model.index(row, 0)
             deviceID = self.model.data(idx,
@@ -327,8 +426,8 @@ class MainWindow(QtGui.QMainWindow):
             for unit, trace in traces.items():
                 now = time.time()
 
-                # remove old samples
-                l = now - MainWindow.BACKLOG
+                # Remove old samples.
+                l = now - settings.graph.backlog.value()
                 while trace.samples and trace.samples[0][0] < l:
                     trace.samples.pop(0)
 
@@ -354,7 +453,7 @@ class MainWindow(QtGui.QMainWindow):
 
         # Mark all traces of all devices/channels with the same unit as the
         # plot as "old" ('trace.new = False'). As soon as a new sample arrives
-        # on one trace, the plot will be shown again
+        # on one trace, the plot will be shown again.
         for row in range(self.model.rowCount()):
             idx = self.model.index(row, 0)
             traces = self.model.data(idx, datamodel.MeasurementDataModel.tracesRole)
@@ -389,7 +488,7 @@ class MainWindow(QtGui.QMainWindow):
             self.actionStartStop.setText('Start Acquisition')
             self.actionStartStop.setIcon(icons.start)
         else:
-            # before starting (again), remove all old samples and old curves
+            # Before starting (again), remove all old samples and old curves.
             self.model.clear_samples()
 
             for key in self._curves:
@@ -403,6 +502,26 @@ class MainWindow(QtGui.QMainWindow):
             self.actionStartStop.setText('Stop Acquisition')
             self.actionStartStop.setIcon(icons.stop)
 
+    @QtCore.Slot()
+    def on_save_log_clicked(self):
+        filename = QtGui.QFileDialog.getSaveFileName(self,
+                    'Save Log File', settings.logging.filename.value())
+
+        if not filename:
+            # User pressed 'cancel'.
+            return
+
+        try:
+            with open(filename, 'w') as f:
+                for line in self.logModel.stringList():
+                    f.write(line)
+                    f.write('\n')
+        except Exception as e:
+            QtGui.QMessageBox.critical(self, 'Error saving log file',
+               'Unable to save the log messages:\n{}'.format(e))
+
+        settings.logging.filename.setValue(filename)
+
     @QtCore.Slot()
     def show_about(self):
         text = textwrap.dedent('''\