2 ## This file is part of the sigrok-meter project.
4 ## Copyright (C) 2013 Uwe Hermann <uwe@hermann-uwe.de>
5 ## Copyright (C) 2014 Jens Steinhauser <jens.steinhauser@gmail.com>
7 ## This program is free software; you can redistribute it and/or modify
8 ## it under the terms of the GNU General Public License as published by
9 ## the Free Software Foundation; either version 2 of the License, or
10 ## (at your option) any later version.
12 ## This program is distributed in the hope that it will be useful,
13 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 ## GNU General Public License for more details.
17 ## You should have received a copy of the GNU General Public License
18 ## along with this program; if not, write to the Free Software
19 ## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23 import multiplotwidget
31 QtCore
= qtcompat
.QtCore
32 QtGui
= qtcompat
.QtGui
33 pyqtgraph
= qtcompat
.pyqtgraph
35 class EmptyMessageListView(QtGui
.QListView
):
36 '''List view that shows a message if the model im empty.'''
38 def __init__(self
, message
, parent
=None):
39 super(self
.__class
__, self
).__init
__(parent
)
41 self
._message
= message
43 def paintEvent(self
, event
):
45 if m
and m
.rowCount():
46 super(self
.__class
__, self
).paintEvent(event
)
49 painter
= QtGui
.QPainter(self
.viewport())
50 painter
.drawText(self
.rect(), QtCore
.Qt
.AlignCenter
, self
._message
)
52 class MainWindow(QtGui
.QMainWindow
):
53 '''The main window of the application.'''
55 # Number of seconds that the plots display.
58 # Update interval of the plots in milliseconds.
61 def __init__(self
, context
, drivers
):
62 super(self
.__class
__, self
).__init
__()
64 self
.context
= context
66 self
.delegate
= datamodel
.MultimeterDelegate(self
, self
.font())
67 self
.model
= datamodel
.MeasurementDataModel(self
)
68 self
.model
.rowsInserted
.connect(self
.modelRowsInserted
)
72 self
.thread
= samplingthread
.SamplingThread(self
.context
, drivers
)
73 self
.thread
.measured
.connect(self
.model
.update
)
74 self
.thread
.error
.connect(self
.error
)
78 self
.setWindowTitle('sigrok-meter')
79 # Resizing the listView below will increase this again.
82 p
= os
.path
.abspath(os
.path
.dirname(__file__
))
83 p
= os
.path
.join(p
, 'sigrok-logo-notext.png')
84 self
.setWindowIcon(QtGui
.QIcon(p
))
86 actionQuit
= QtGui
.QAction(self
)
87 actionQuit
.setText('&Quit')
88 actionQuit
.setIcon(QtGui
.QIcon
.fromTheme('application-exit'))
89 actionQuit
.setShortcut('Ctrl+Q')
90 actionQuit
.triggered
.connect(self
.close
)
92 actionAbout
= QtGui
.QAction(self
)
93 actionAbout
.setText('&About')
94 actionAbout
.setIcon(QtGui
.QIcon
.fromTheme('help-about'))
95 actionAbout
.triggered
.connect(self
.show_about
)
97 menubar
= self
.menuBar()
98 menuFile
= menubar
.addMenu('&File')
99 menuFile
.addAction(actionQuit
)
100 menuHelp
= menubar
.addMenu('&Help')
101 menuHelp
.addAction(actionAbout
)
103 self
.listView
= EmptyMessageListView('waiting for data...')
104 self
.listView
.setFrameShape(QtGui
.QFrame
.NoFrame
)
105 self
.listView
.viewport().setBackgroundRole(QtGui
.QPalette
.Window
)
106 self
.listView
.viewport().setAutoFillBackground(True)
107 self
.listView
.setMinimumWidth(260)
108 self
.listView
.setSelectionMode(QtGui
.QAbstractItemView
.NoSelection
)
109 self
.listView
.setEditTriggers(QtGui
.QAbstractItemView
.NoEditTriggers
)
110 self
.listView
.setVerticalScrollMode(QtGui
.QAbstractItemView
.ScrollPerPixel
)
111 self
.listView
.setItemDelegate(self
.delegate
)
112 self
.listView
.setModel(self
.model
)
113 self
.listView
.setUniformItemSizes(True)
114 self
.listView
.setMinimumSize(self
.delegate
.sizeHint())
116 self
.plotwidget
= multiplotwidget
.MultiPlotWidget(self
)
117 self
.plotwidget
.plotHidden
.connect(self
._on
_plotHidden
)
119 # Maps from 'unit' to the corresponding plot.
121 # Maps from '(plot, device)' to the corresponding curve.
124 self
.splitter
= QtGui
.QSplitter(QtCore
.Qt
.Horizontal
);
125 self
.splitter
.addWidget(self
.listView
)
126 self
.splitter
.addWidget(self
.plotwidget
)
127 self
.splitter
.setStretchFactor(0, 0)
128 self
.splitter
.setStretchFactor(1, 1)
130 self
.setCentralWidget(self
.splitter
)
131 self
.centralWidget().setContentsMargins(0, 0, 0, 0)
132 self
.resize(800, 500)
134 self
.startTimer(MainWindow
.UPDATEINTERVAL
)
136 def _getPlot(self
, unit
):
137 '''Looks up or creates a new plot for 'unit'.'''
139 if unit
in self
._plots
:
140 return self
._plots
[unit
]
142 # create a new plot for the unit
143 plot
= self
.plotwidget
.addPlot()
144 plot
.yaxis
.setLabel(util
.quantity_from_unit(unit
), units
=util
.format_unit(unit
))
145 plot
.view
.setXRange(-MainWindow
.BACKLOG
, 0, update
=False)
146 plot
.view
.setYRange(-1, 1)
147 plot
.view
.enableAutoRange(axis
=pyqtgraph
.ViewBox
.YAxis
)
148 # lock to the range calculated by the view using additional padding,
149 # looks nicer this way
150 r
= plot
.view
.viewRange()
151 plot
.view
.setLimits(xMin
=r
[0][0], xMax
=r
[0][1])
153 self
._plots
[unit
] = plot
156 def _getCurve(self
, plot
, deviceID
):
157 '''Looks up or creates a new curve for '(plot, deviceID)'.'''
159 key
= (id(plot
), deviceID
)
160 if key
in self
._curves
:
161 return self
._curves
[key
]
164 curve
= pyqtgraph
.PlotDataItem(
166 symbolPen
=pyqtgraph
.mkPen(QtGui
.QColor(QtCore
.Qt
.black
)),
167 symbolBrush
=pyqtgraph
.mkBrush(QtGui
.QColor(QtCore
.Qt
.black
)),
170 plot
.view
.addItem(curve
)
172 self
._curves
[key
] = curve
175 def timerEvent(self
, event
):
176 '''Periodically updates all graphs.'''
180 def _updatePlots(self
):
181 '''Updates all plots.'''
183 # loop over all devices and channels
184 for row
in range(self
.model
.rowCount()):
185 idx
= self
.model
.index(row
, 0)
186 deviceID
= self
.model
.data(idx
,
187 datamodel
.MeasurementDataModel
.idRole
)
188 traces
= self
.model
.data(idx
,
189 datamodel
.MeasurementDataModel
.tracesRole
)
191 for unit
, trace
in traces
.items():
195 l
= now
- MainWindow
.BACKLOG
196 while trace
.samples
and trace
.samples
[0][0] < l
:
199 plot
= self
._getPlot
(unit
)
202 self
.plotwidget
.showPlot(plot
)
205 xdata
= [s
[0] - now
for s
in trace
.samples
]
206 ydata
= [s
[1] for s
in trace
.samples
]
208 color
= self
.model
.data(idx
,
209 datamodel
.MeasurementDataModel
.colorRole
)
211 curve
= self
._getCurve
(plot
, deviceID
)
212 curve
.setPen(pyqtgraph
.mkPen(color
=color
))
213 curve
.setData(xdata
, ydata
)
215 @QtCore.Slot(multiplotwidget
.Plot
)
216 def _on_plotHidden(self
, plot
):
217 plotunit
= [u
for u
, p
in self
._plots
.items() if p
== plot
][0]
219 # Mark all traces of all devices/channels with the same unit as the
220 # plot as "old" ('trace.new = False'). As soon as a new sample arrives
221 # on one trace, the plot will be shown again
222 for row
in range(self
.model
.rowCount()):
223 idx
= self
.model
.index(row
, 0)
224 traces
= self
.model
.data(idx
, datamodel
.MeasurementDataModel
.tracesRole
)
226 for traceunit
, trace
in traces
.items():
227 if traceunit
== plotunit
:
230 def closeEvent(self
, event
):
235 def show_about(self
):
236 text
= textwrap
.dedent('''\
238 <b>sigrok-meter 0.1.0</b><br/><br/>
239 Using libsigrok {} (lib version {}).<br/><br/>
240 <a href='http://www.sigrok.org'>
241 http://www.sigrok.org</a><br/>
243 License: GNU GPL, version 3 or later<br/>
245 This program comes with ABSOLUTELY NO WARRANTY;<br/>
247 <a href='http://www.gnu.org/licenses/gpl.html'>
248 http://www.gnu.org/licenses/gpl.html</a>
250 '''.format(self
.context
.package_version
, self
.context
.lib_version
))
252 QtGui
.QMessageBox
.about(self
, 'About sigrok-meter', text
)
255 def error(self
, msg
):
256 '''Error handler for the sampling thread.'''
257 QtGui
.QMessageBox
.critical(self
, 'Error', msg
)
260 @QtCore.Slot(object, int, int)
261 def modelRowsInserted(self
, parent
, start
, end
):
262 '''Resize the list view to the size of the content.'''
263 rows
= self
.model
.rowCount()
264 dh
= self
.delegate
.sizeHint().height()
265 self
.listView
.setMinimumHeight(dh
* rows
)