class MdiMainWindow(QtWidgets.QMainWindow): '''Base class for MDI applications. :SIGNALS: * :attr:`subWindowClosed` ''' # @TODO: should the subWindowClosed signal be emitted by mdiarea? #: SIGNAL: it is emitted when an MDI subwindow is closed #: #: :C++ signature: `void subWindowClosed()` subWindowClosed = QtCore.Signal() def __init__(self, parent=None, flags=QtCore.Qt.WindowFlags(0), **kwargs): super(MdiMainWindow, self).__init__(parent, flags, **kwargs) #: MDI area instance (QMdiArea) self.mdiarea = QtWidgets.QMdiArea() self.setCentralWidget(self.mdiarea) self.mdiarea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.mdiarea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) #: sub-windows menu self.windowmenu = QtWindowListMenu(self.menuBar()) self.windowmenu.attachToMdiArea(self.mdiarea)
class RubberBandMode(MouseMode): '''Mouse mode for rubber band selection. :SIGNALS: * :attr:`rubberBandSeclection` ''' dragmode = QtWidgets.QGraphicsView.RubberBandDrag cursor = QtCore.Qt.CrossCursor icon = qtsupport.geticon('area.svg', __name__) label = 'Rubber band' name = 'rubberband' #: SIGNAL: it is emitted when a rectangular area is selected #: #: :C++ signature: `void rubberBandSeclection(const QRectF&)` rubberBandSeclection = QtCore.Signal(QtCore.QRectF) def sceneEventFilter(self, obj, event): if event.type() == QtCore.QEvent.GraphicsSceneMouseRelease: p0 = event.buttonDownScenePos(QtCore.Qt.LeftButton) p1 = event.scenePos() rect = QtCore.QRectF(p0, p1).normalized() self.rubberBandSeclection.emit(rect) return True #return obj.eventFilter(obj, event) # @TODO: check #return QtWidgets.QGraphicsScene.eventFilter(self, obj, event) return False def scrollbarEventFilter(self, obj, event): # ignore wheel events if some button is pressed if ((event.type() == QtCore.QEvent.Wheel) and (event.buttons() != QtCore.Qt.NoButton)): return True else: return False
class MouseManager(QtCore.QObject): #: SIGNAL: it is emitted when the mouse mode is changed #: #: :C++ signature: `void modeChanged(const QString&)` modeChanged = QtCore.Signal(str) def __init__(self, parent=None, stdmodes=True, **kwargs): QtCore.QObject.__init__(self, parent, **kwargs) self._moderegistry = [] self.actions = QtWidgets.QActionGroup(self) self.actions.setExclusive(True) if stdmodes: self.registerStandardModes() def registerStandardModes(self): for mode in (PointerMode, ScrollHandMode): # , RubberBandMode): self.addMode(mode) if len(self._moderegistry) and not self.actions.checkedAction(): self.actions.actions()[0].setChecked(True) def _newModeAction(self, mode, parent): if isinstance(mode.icon, string_types): icon = QtGui.QIcon(mode.icon) elif isinstance(mode.icon, QtWidgets.QStyle.StandardPixmap): style = QtWidgets.QApplication.style() icon = style.standardIcon(mode.icon) else: icon = mode.icon action = QtWidgets.QAction( icon, self.tr(mode.label), parent, statusTip=self.tr(mode.label), checkable=True) action.triggered.connect(lambda: self.modeChanged.emit(self.mode)) return action def _getMode(self): action = self.actions.checkedAction() index = self.actions.actions().index(action) return self._moderegistry[index].name def _setMode(self, name): names = self.modes() index = names.index(name) action = self.actions.actions()[index] action.setChecked(True) def _delMode(self, name): names = [m.name for m in self._moderegistry] index = names.index(name) action = self.actions.actions()[index] self.actions.removeAction(action) del self._moderegistry[index] #~ if actin.checked() and self._moderegistry: #~ self.actions.actions()[0].setChecked(True) mode = property(_getMode, _setMode, _delMode, 'mouse mode name') def modes(self): return tuple(m.name for m in self._moderegistry) def addMode(self, mode): if isinstance(mode, type): mode = mode(self) action = self._newModeAction(mode, self.actions) self.actions.addAction(action) self._moderegistry.append(mode) def getModeDescriptor(self, name=None): '''Return the mouse mode object''' try: if name is None: action = self.actions.checkedAction() index = self.actions.actions().index(action) else: names = self.modes() index = names.index(name) return self._moderegistry[index] except IndexError: # @TODO: check #raise ValueError('invalid mde nema: "%s"' % mode) return None def eventFilter(self, obj, event): '''Events dispatcher''' return self.getModeDescriptor().eventFilter(obj, event) def register(self, obj): '''Register a Qt graphics object to be monitored by the mouse manager. QGraphicsScene and QGrapgicsViews (and descending classes) objects can be registered to be monitored by the mouse manager. Scene objects associated to views (passes as argument) are automatically registered. ''' obj.installEventFilter(self) try: obj.verticalScrollBar().installEventFilter(self) except AttributeError: # it is a QGraphicsScene scene = obj else: scene = obj.scene() # Avoid event filter duplication scene.removeEventFilter(self) scene.installEventFilter(self) def unregister(self, obj): '''Unregister monitored objects. If the object passed as argument is not a registered object nothing happens. .. note:: this method never tries to unregister scene objects associated to the view passed as argument. ''' obj.removeEventFilter(self)
class QtOutputPane(QtWidgets.QTextEdit): #: SIGNAL: emits a hide request. #: #: :C++ signature: `void paneHideRequest()` paneHideRequest = QtCore.Signal() def __init__(self, parent=None, **kwargs): super(QtOutputPane, self).__init__(parent, **kwargs) self._setupActions() self.banner = None def _setupActions(self): qstype = QtWidgets.QApplication.style() # Setup actions self.actions = QtWidgets.QActionGroup(self) # Save As icon = qstype.standardIcon(QtWidgets.QStyle.SP_DialogSaveButton) self.actionSaveAs = QtWidgets.QAction( icon, self.tr('&Save As'), self, shortcut=self.tr('Ctrl+S'), statusTip=self.tr('Save text to file'), triggered=self.save) self.actions.addAction(self.actionSaveAs) # Clear icon = QtGui.QIcon(':/trolltech/styles/commonstyle/images/' 'standardbutton-clear-32.png') self.actionClear = QtWidgets.QAction( icon, self.tr('&Clear'), self, shortcut=self.tr('Shift+F5'), statusTip=self.tr('Clear the text'), triggered=self.clear) self.actions.addAction(self.actionClear) # Close icon = qstype.standardIcon(QtWidgets.QStyle.SP_DialogCloseButton) self.actionHide = QtWidgets.QAction( icon, self.tr('&Hide'), self, shortcut=self.tr('Ctrl+W'), statusTip=self.tr('Hide the text pane'), triggered=self.paneHideRequest) self.actions.addAction(self.actionHide) def contextMenuEvent(self, event): menu = QtWidgets.QTextEdit.createStandardContextMenu(self) menu.addSeparator() menu.addActions(self.actions.actions()) menu.exec_(event.globalPos()) def _report(self): if callable(self.banner): header = self.banner() elif self.banner is not None: header = self.banner else: header = '# Output log generated on %s' % time.asctime() text = self.toPlainText() return '%s\n\n%s' % (header, text) # def clear(self): # it is a standard QtWidgets.QTextEdit method def save(self): '''Save a file.''' filter_ = self.tr('Text files (*.txt)') filename, _ = QtWidgets.QFileDialog.getSaveFileName( self, '', '', filter_) if filename: text = self._report() logfile = open(filename, 'w') logfile.write(text) logfile.close()
class QtToolController(QtCore.QObject, BaseToolController): '''Qt tool controller. :SIGNALS: * :attr:`finished` :SLOTS: * :meth:`stop_tool` * :meth:`finalize_run` * :meth:`handle_stdout` * :meth:`handle_stderr` * :meth:`handle_error` ''' _delay_after_stop = 200 # ms #: SIGNAL: it is emitted when the processing is finished. #: #: :param int exitcode: #: the external proces exit code #: #: :C++ signature: `void finished(int exitCode)` finished = QtCore.Signal(int) def __init__(self, logger=None, parent=None, **kwargs): QtCore.QObject.__init__(self, parent, **kwargs) BaseToolController.__init__(self, logger) self.subprocess = QtCore.QProcess(parent) self.subprocess.setProcessChannelMode(QtCore.QProcess.MergedChannels) # connect process handlers and I/O handlers self.subprocess.readyReadStandardOutput.connect(self.handle_stdout) self.subprocess.readyReadStandardError.connect(self.handle_stderr) self.subprocess.error.connect(self.handle_error) self.subprocess.finished.connect(self.finalize_run) @property def isbusy(self): '''If True then the controller is already running a subprocess.''' return self.subprocess.state() != self.subprocess.NotRunning @QtCore.Slot(int, QtCore.QProcess.ExitStatus) def finalize_run(self, exitCode=None, exitStatus=None): '''Perform finalization actions. This method is called when the controlled process terminates to perform finalization actions like: * read and handle residual data in buffers, * flush and close output handlers, * close subprocess file descriptors * run the "finalize_run_hook" method * reset the controller instance If one just needs to perfor some additional finalization action it should be better to use a custom "finalize_run_hook" instead of overriging "finalize_run". :C++ signature: `finalize_run(int, QProcess::ExitStatus)` ''' if not self._tool: return out_encoding = self._tool.output_encoding try: # retrieve residual data # @TODO: check if it is actually needed if self._tool.stdout_handler: byteArray = self.subprocess.readAllStandardOutput() data = byteArray.data().decode(out_encoding) self._tool.stdout_handler.feed(data) if self._tool.stderr_handler: byteArray = self.subprocess.readAllStandardError() data = byteArray.data().decode(out_encoding) self._tool.stderr_handler.feed(data) # close the pipe and wait for the subprocess termination self.subprocess.close() if self._tool.stdout_handler: self._tool.stdout_handler.close() if self._tool.stderr_handler: self._tool.stderr_handler.close() if self._userstop: self.logger.info('Execution stopped by the user.') elif exitCode != EX_OK: msg = ('Process (PID=%d) exited with return code %d.' % (self.subprocess.pid(), self.subprocess.exitCode())) self.logger.warning(msg) # Call finalize hook if available self.finalize_run_hook() finally: # @TODO: check # Protect for unexpected errors in the feed and close methods of # the stdout_handler self._reset() self.finished.emit(exitCode) def _reset(self): '''Internal reset.''' if self.subprocess.state() != self.subprocess.NotRunning: self._stop(force=True) self.subprocess.waitForFinished() stopped = self.subprocess.state() == self.subprocess.NotRunning if not stopped: self.logger.warning('reset running process (PID=%d)' % self.subprocess.pid()) assert self.subprocess.state() == self.subprocess.NotRunning, \ 'the process is still running' self.subprocess.setProcessState(self.subprocess.NotRunning) # @TODO: check self.subprocess.closeReadChannel(QtCore.QProcess.StandardOutput) self.subprocess.closeReadChannel(QtCore.QProcess.StandardError) self.subprocess.closeWriteChannel() super(QtToolController, self)._reset() self.subprocess.close() @QtCore.Slot() def handle_stdout(self): '''Handle standard output. :C++ signature: `void handle_stdout()` ''' byteArray = self.subprocess.readAllStandardOutput() if not byteArray.isEmpty(): data = byteArray.data().decode(self._tool.output_encoding) self._tool.stdout_handler.feed(data) @QtCore.Slot() def handle_stderr(self): '''Handle standard error. :C++ signature: `void handle_stderr()` ''' byteArray = self.subprocess.readAllStandardError() if not byteArray.isEmpty(): data = byteArray.data().decode(self._tool.output_encoding) self._tool.stderr_handler.feed(data) @QtCore.Slot(QtCore.QProcess.ProcessError) def handle_error(self, error): '''Handle a error in process execution. Can be handle different types of errors: * starting failed * crashing after starts successfully * timeout elapsed * write error * read error * unknow error :C++ signature: `void handle_error(QProcess::ProcessError)` ''' msg = '' level = logging.DEBUG if self.subprocess.state() == self.subprocess.NotRunning: logging.debug('NotRunning') exit_code = self.subprocess.exitCode() else: exit_code = 0 if error == QtCore.QProcess.FailedToStart: msg = ('The process failed to start. Either the invoked program ' 'is missing, or you may have insufficient permissions to ' 'invoke the program.') level = logging.ERROR # @TODO: check #self._reset() elif error == QtCore.QProcess.Crashed: if not self._userstop and self.subprocess.exitCode() == EX_OK: msg = ('The process crashed some time after starting ' 'successfully.') level = logging.ERROR elif error == QtCore.QProcess.Timedout: msg = ('The last waitFor...() function timed out. The state of ' 'QProcess is unchanged, and you can try calling ' 'waitFor...() again.') level = logging.DEBUG elif error == QtCore.QProcess.WriteError: msg = ('An error occurred when attempting to write to the process.' ' For example, the process may not be running, or it may ' 'have closed its input channel.') #level = logging.ERROR # @TODO: check elif error == QtCore.QProcess.ReadError: msg = ('An error occurred when attempting to read from the ' 'process. For example, the process may not be running.') #level = logging.ERROR # @TODO: check elif error == QtCore.QProcess.UnknownError: msg = ('An unknown error occurred. This is the default return ' 'value of error().') #level = logging.ERROR # @TODO: check if msg: self.logger.log(level, msg) self.finished.emit(exit_code) #QtCore.Slot() # @TODO: check how to handle varargs def run_tool(self, tool, *args, **kwargs): '''Run an external tool in controlled way. The output of the child process is handled by the controller and, optionally, notifications can be achieved at sub-process termination. ''' assert self.subprocess.state() == self.subprocess.NotRunning self.reset() self._tool = tool if self._tool.stdout_handler: self._tool.stdout_handler.reset() if self._tool.stderr_handler: self._tool.stderr_handler.reset() cmd = self._tool.cmdline(*args, **kwargs) self.prerun_hook(cmd) cmd = ' '.join(cmd) if self._tool.env: qenv = QtCore.QProcessEnvironment() for key, val in self._tool.env.items(): qenv.insert(key, str(val)) self.subprocess.setProcessEnvironment(qenv) if self._tool.cwd: self.subprocess.setWorkingDirectory(self._tool.cwd) self.logger.debug('"shell" flag set to %s.' % self._tool.shell) self.logger.debug('Starting: %s' % cmd) self.subprocess.start(cmd) self.subprocess.closeWriteChannel() def _stop(self, force=True): if self.subprocess.state() == self.subprocess.NotRunning: return self.subprocess.terminate() self.subprocess.waitForFinished(self._delay_after_stop) stopped = self.subprocess.state() == self.subprocess.NotRunning if not stopped and force: self.logger.info('Force process termination (PID=%d).' % self.subprocess.pid()) self.subprocess.kill() @QtCore.Slot() @QtCore.Slot(bool) def stop_tool(self, force=True): '''Stop the execution of controlled subprocess. When this method is invoked the controller instance is always reset even if the controller is unable to stop the subprocess. When possible the controller try to kill the subprocess in a polite way. If this fails it also tryes brute killing by default (force=True). This behaviour can be controlled using the `force` parameter. :C++ signature: `void stop_tool(bool)` ''' if self._userstop: return if self.subprocess.state() != self.subprocess.NotRunning: self.logger.debug('Execution stopped by the user.') self._userstop = True self._stop(force) self.subprocess.waitForFinished() stopped = self.subprocess.state() == self.subprocess.NotRunning if not stopped: msg = ('Unable to stop the sub-process (PID=%d).' % self.subprocess.pid()) self.logger.warning(msg)
class QtOutputHandler(QtCore.QObject, BaseOutputHandler): '''Qt Output Handler. :SIGNALS: * :attr:`pulse` * :attr:`percentageChanged` ''' _statusbar_timeout = 2000 # ms #: SIGNAL: it is emitted to signal some kind of activity of the external #: process #: #: :param str text: #: an optional text describing the kind activity of the external #: process #: #: :C++ signature: `void pulse(QString)` pulse = QtCore.Signal([], [str]) #: SIGNAL: it is emitted when the progress percentage changes #: #: :param float percentage: #: the new completion percentage [0, 100] #: #: :C++ signature: `void percentageChanged(float)` percentageChanged = QtCore.Signal([int], []) def __init__(self, logger=None, statusbar=None, progressbar=None, blinker=None, parent=None, **kwargs): QtCore.QObject.__init__(self, parent, **kwargs) BaseOutputHandler.__init__(self, logger) self.statusbar = statusbar if self.statusbar: if blinker is None: blinker = QtBlinker() statusbar.addPermanentWidget(blinker) blinker.hide() self.pulse.connect(blinker.show) self.pulse.connect(blinker.pulse) self.pulse[str].connect(lambda text: statusbar.showMessage( text, self._statusbar_timeout)) if progressbar is None: progressbar = QtWidgets.QProgressBar(self.statusbar) progressbar.setTextVisible(True) statusbar.addPermanentWidget(progressbar) # , 1) # stretch=1 progressbar.hide() self.progressbar = progressbar #self.percentageChanged[()].connect(progressbar.show) self.percentageChanged.connect(progressbar.show) self.percentageChanged.connect(progressbar.setValue) self.progressbar = progressbar self.blinker = blinker def feed(self, data): '''Feed some data to the parser. It is processed insofar as it consists of complete elements; incomplete data is buffered until more data is fed or close() is called. ''' if self.blinker: self.blinker.show() super(QtOutputHandler, self).feed(data) def close(self): '''Reset the instance.''' if self.statusbar: self.statusbar.clearMessage() super(QtOutputHandler, self).close() def reset(self): '''Reset the handler instance. Loses all unprocessed data. This is called implicitly at instantiation time. ''' super(QtOutputHandler, self).reset() if self.progressbar: self.progressbar.setRange(0, 100) self.progressbar.reset() self.progressbar.hide() if self.blinker: self.blinker.reset() self.blinker.hide() def handle_progress(self, data): '''Handle progress data. :param data: a list containing an item for each named group in the "progress" regular expression: (pulse, percentage, text) for the default implementation. Each item can be None. ''' #pulse = data.get('pulse') percentage = data.get('percentage') text = data.get('text') self.pulse.emit() if text: self.pulse[str].emit(text) if percentage is not None: self.percentageChanged.emit(int(percentage))
class StretchDialog(QtWidgets.QDialog, StretchDialogBase): '''Stretch dialog. :SIGNALS: * :attr:`valueChanged` ''' #: SIGNAL: it is emitted when the stretch value changes #: #: :C++ signature: `void valueChanged()` valueChanged = QtCore.Signal() def __init__(self, parent=None, flags=QtCore.Qt.WindowFlags(0), floatmode=True, **kwargs): super(StretchDialog, self).__init__(parent, flags, **kwargs) self.setupUi(self) self.stretchwidget = StretchWidget(self, floatmode=floatmode) self.mainLayout.insertWidget(0, self.stretchwidget) if not self.checkBox.isChecked(): self.setAdvanced(False) self.state = None self.saveState() self.checkBox.toggled.connect(self.setAdvanced) self.buttonBox.button( QtWidgets.QDialogButtonBox.Reset).clicked.connect(self.reset) self.stretchwidget.valueChanged.connect(self.valueChanged) #~ self.stretchwidget.lowSpinBox.valueChanged.connect( #~ self.valueChanged) #~ self.stretchwidget.highSpinBox.valueChanged.connect( #~ self.valueChanged) def advanced(self): return self.stretchwidget.lowSpinBox.isVisible() @QtCore.Slot() @QtCore.Slot(bool) def setAdvanced(self, advmode=True): self.stretchwidget.lowSpinBox.setVisible(advmode) self.stretchwidget.lowSlider.setVisible(advmode) @QtCore.Property(bool) def floatmode(self): return self.stretchwidget.floatmode @floatmode.setter def floatmode(self, mode): self.stretchwidget.floatmode = mode def saveState(self): self.state = self.stretchwidget.state() @QtCore.Slot() def reset(self, d=None): if d is None: d = self.state if d is None: return try: self.stretchwidget.setState(d) except KeyError as e: _log.info('unable to set state: %s', e) def values(self): return self.stretchwidget.values()
class StretchWidget(QtWidgets.QWidget, StretchWidgetBase): '''Stretch widget. :SIGNALS: * :attr:`valueChanged` ''' #: SIGNAL: it is emitted when the stretch value changes #: #: :C++ signature: `void valueChanged()` valueChanged = QtCore.Signal() #: SIGNAL: it is emitted when the stretch range changes #: #: :C++ signature: `void rangeChanged(int, int)` #rangeChanged = QtCore.Signal(int, int) def __init__(self, parent=None, flags=QtCore.Qt.WindowFlags(0), floatmode=True, **kwargs): super(StretchWidget, self).__init__(parent, flags, **kwargs) self.setupUi(self) self._floatmode = False self._kslider = self._computeKSlider() self._connectSignals() self.floatmode = floatmode def _connectSignals(self): self.lowSlider.valueChanged.connect(self._onLowSliderChanged) self.highSlider.valueChanged.connect(self._onHighSliderChanged) #self.rangeChanged.connect(self._onRangeChanged) self.minSpinBox.valueChanged[float].connect(self._onMinimumChanged) self.maxSpinBox.valueChanged[float].connect(self._onMaximumChanged) self.lowSpinBox.valueChanged[float].connect(self._onLowSpinBoxChanged) self.highSpinBox.valueChanged[float].connect(self._onHighSpinBoxChanged) self.lowSpinBox.valueChanged.connect(self.valueChanged) self.highSpinBox.valueChanged.connect(self.valueChanged) def _disconnectSignals(self): self.lowSlider.valueChanged.disconnect(self._onLowSliderChanged) self.highSlider.valueChanged.disconnect(self._onHighSliderChanged) #self.rangeChanged.disconnect(self._onRangeChanged) self.minSpinBox.valueChanged[float].disconnect(self._onMinimumChanged) self.maxSpinBox.valueChanged[float].disconnect(self._onMaximumChanged) self.lowSpinBox.valueChanged[float].disconnect(self._onLowSpinBoxChanged) self.highSpinBox.valueChanged[float].disconnect(self._onHighSpinBoxChanged) self.lowSpinBox.valueChanged.disconnect(self.valueChanged) self.highSpinBox.valueChanged.disconnect(self.valueChanged) @contextlib.contextmanager def _disconnectedSignals(self): self._disconnectSignals() yield self._connectSignals() @QtCore.Property(bool) def floatmode(self): return self._floatmode @floatmode.setter def floatmode(self, floatmode=True): '''Set the stretch widget in floating point mode.''' floatmode = bool(floatmode) if floatmode == self._floatmode: return vmin = self.minSpinBox.value() vmax = self.maxSpinBox.value() self._floatmode = floatmode if self._floatmode: self.lowSlider.setRange(0, 1000) self.highSlider.setRange(0, 1000) else: self.lowSlider.setRange(vmin, vmax) self.highSlider.setRange(vmin, vmax) self.setRange(vmin, vmax) def setRange(self, vmin, vmax): if vmin >= vmax: raise ValueError('vmin (%f) >= vmax (%f)' % (vmin, vmax)) low = self.lowSpinBox.value() if low < vmin: low = vmin elif low > vmax: low = vmax high = self.highSpinBox.value() if high < vmin: high = vmin elif high > vmax: high = vmax with self._disconnectedSignals(): self.minSpinBox.setValue(vmin) self.maxSpinBox.setValue(vmax) self._kslider = self._computeKSlider(vmin, vmax) if not self._floatmode: self.lowSlider.setRange(vmin, vmax) self.highSlider.setRange(vmin, vmax) self.lowSlider.setValue(low) self.highSlider.setValue(high) else: self.lowSlider.setValue(self._pos(low)) self.highSlider.setValue(self._pos(high)) self._updateSteps(self.maximum(), self.minimum()) def _computeSteps(self, vmin, vmax): vmax = max(abs(vmax), abs(vmin)) if vmax == 0: # @TODO: check return 1, 10 if not self._floatmode and vmax < 32: return 1, min(5, vmax) kmax = np.log10(vmax) singleStep = 10**(np.round(kmax) - 2) pageStep = 10 * singleStep return singleStep, pageStep def _updateSteps(self, vmin, vmax): singleStep, pageStep = self._computeSteps(vmin, vmax) self.lowSpinBox.setSingleStep(singleStep) self.highSpinBox.setSingleStep(singleStep) if self._floatmode: decimals = 7 self.lowSpinBox.setDecimals(decimals) self.highSpinBox.setDecimals(decimals) step = np.round(singleStep * self._kslider) self.lowSlider.setSingleStep(step) self.highSlider.setSingleStep(step) step = np.round(pageStep * self._kslider) self.lowSlider.setSingleStep(step) self.highSlider.setSingleStep(step) else: self.lowSpinBox.setDecimals(0) self.highSpinBox.setDecimals(0) self.lowSlider.setSingleStep(singleStep) self.highSlider.setSingleStep(singleStep) self.lowSlider.setSingleStep(pageStep) self.highSlider.setSingleStep(pageStep) # @TODO: update all steps self.maxSpinBox.setSingleStep(pageStep) if self.minimum() != 0: step = 10**(np.round(np.log10(abs(self.minimum())))) else: step = singleStep self.minSpinBox.setSingleStep(step) def _computeKSlider(self, vmin=None, vmax=None): if not self._floatmode: return 1 if vmin is None: vmin = self.lowSlider.minimum() if vmax is None: vmax = self.highSlider.maximum() vrange = float(self.maxSpinBox.value() - self.minSpinBox.value()) srange = float(vmax - vmin) if srange == 0: return 0 else: return np.round(vrange / srange) def _pos(self, value): if self._kslider == 0: return self.minSpinBox.value() return (value - self.minSpinBox.value()) / self._kslider def _value(self, pos): return self.minSpinBox.value() + self._kslider * pos def low(self): return self.lowSpinBox.value() @QtCore.Slot(float) def setLow(self, value): self.lowSpinBox.setValue(value) if self.lowSpinBox.value() > self.highSpinBox.value(): self.highSpinBox.setValue(self.lowSpinBox.value()) def high(self): return self.highSpinBox.value() @QtCore.Slot(float) def setHigh(self, value): self.highSpinBox.setValue(value) if self.lowSpinBox.value() > self.highSpinBox.value(): self.lowSpinBox.setValue(self.highSpinBox.value()) @QtCore.Slot(float) def _onLowSpinBoxChanged(self, value): if self.floatmode: pos = self._pos(value) else: pos = value self.lowSlider.setValue(pos) @QtCore.Slot(float) def _onHighSpinBoxChanged(self, value): if self.floatmode: pos = self._pos(value) else: pos = value self.highSlider.setValue(pos) @QtCore.Slot(int) def _onLowSliderChanged(self, value): if value > self.highSlider.value(): self.highSlider.setValue(value) if self.floatmode: value = self._value(value) self.lowSpinBox.setValue(value) @QtCore.Slot(int) def _onHighSliderChanged(self, value): if value < self.lowSlider.value(): self.lowSlider.setValue(value) if self.floatmode: value = self._value(value) self.highSpinBox.setValue(value) def values(self): return self.low(), self.high() def singleStep(self): #assert self.lowSlider.singleStep() == self.highSlider.singleStep() #assert self.lowSlider.singleStep() == self.lowSpinBox.singleStep() #assert self.lowSlider.singleStep() == self.highSpinBox.singleStep() return self.highSpinBox.singleStep() def setSingleStep(self, step): k = self._kslider if self._kslider else 1 self.lowSlider.setSingleStep(step / k) self.highSlider.setSingleStep(step / k) self.lowSpinBox.setSingleStep(step) self.highSpinBox.setSingleStep(step) def pageStep(self): #assert self.lowSlider.pageStep() == self.highSlider.pageStep() return self.highSlider.pageStep() * self._kslider def setPageStep(self, step): k = self._kslider if self._kslider else 1 self.lowSlider.setPageStep(step / k) self.highSlider.setPageStep(step / k) def minimum(self): return self.minSpinBox.value() def setMinimum(self, value): if value >= self.maximum(): raise ValueError("can't set a minimum value greater that maximum") self.minSpinBox.setValue(value) @QtCore.Slot(float) def _onMinimumChanged(self, value): if value >= self.maxSpinBox.value(): value = self.maxSpinBox.value() - self.singleStep() self.minSpinBox.setValue(value) return stretch_changed = False with self._disconnectedSignals(): self.maxSpinBox.setMinimum(value) self.lowSpinBox.setMinimum(value) self.highSpinBox.setMinimum(value) if self.floatmode: self._kslider = self._computeKSlider() vmin = self._pos(self.lowSpinBox.value()) vmax = self._pos(self.highSpinBox.value()) self.lowSlider.setValue(vmin) self.highSlider.setValue(vmax) else: self.lowSlider.setMinimum(value) self.highSlider.setMinimum(value) if self.lowSpinBox.value() < self.minSpinBox.value(): self.lowSpinBox.setValue(self.minSpinBox.value()) stretch_changed = True if self.highSpinBox.value() < self.minSpinBox.value(): high = self.minSpinBox.value() + self.lowSpinBox.singleStep() high = min(high, self.maxSpinBox.value()) self.highSpinBox.setValue(high) stretch_changed = True self._updateSteps(self.minimum(), self.maximum()) if stretch_changed: self.valueChanged.emit() def maximum(self): return self.maxSpinBox.value() def setMaximum(self, value): if value <= self.minimum(): raise ValueError("can't set a maximum value smaller that minimum") self.maxSpinBox.setValue(value) @QtCore.Slot(float) def _onMaximumChanged(self, value): if value <= self.minSpinBox.value(): value = self.minSpinBox.value() + self.singleStep() self.maxSpinBox.setValue(value) return stretch_changed = False with self._disconnectedSignals(): self.minSpinBox.setMaximum(value) self.lowSpinBox.setMaximum(value) self.highSpinBox.setMaximum(value) if self.floatmode: self._kslider = self._computeKSlider() vmin = self._pos(self.lowSpinBox.value()) vmax = self._pos(self.highSpinBox.value()) self.lowSlider.setValue(vmin) self.highSlider.setValue(vmax) else: self.lowSlider.setMaximum(value) self.highSlider.setMaximum(value) if self.lowSpinBox.value() > self.maxSpinBox.value(): low = self.maxSpinBox.value() + self.highSpinBox.singleStep() low = max(low, self.minSpinBox.value()) self.lowSpinBox.setValue(low) stretch_changed = True if self.highSpinBox.value() > self.maxSpinBox.value(): self.highSpinBox.setValue(self.maxSpinBox.value()) stretch_changed = True self._updateSteps(self.minimum(), self.maximum()) if stretch_changed: self.valueChanged.emit() def setState(self, d): self.minSpinBox.setMinimum(d['minSpinBox.minimum']) self.minSpinBox.setMaximum(d['minSpinBox.maximum']) self.minSpinBox.setSingleStep(d['minSpinBox.singleStep']) self.maxSpinBox.setMinimum(d['maxSpinBox.minimum']) self.maxSpinBox.setMaximum(d['maxSpinBox.maximum']) self.maxSpinBox.setSingleStep(d['maxSpinBox.singleStep']) self.floatmode = d['floatmode'] self.setMinimum(d['minimum']) self.setMaximum(d['maximum']) self.setLow(d['low']) self.setHigh(d['high']) self.setSingleStep(d['singleStep']) self.setPageStep(d['pageStep']) def state(self, d=None): if d is None: d = dict() d['floatmode'] = self.floatmode d['minimum'] = self.minimum() d['maximum'] = self.maximum() d['low'] = self.low() d['high'] = self.high() d['singleStep'] = self.singleStep() d['pageStep'] = self.pageStep() d['minSpinBox.minimum'] = self.minSpinBox.minimum() d['minSpinBox.maximum'] = self.minSpinBox.maximum() d['minSpinBox.singleStep'] = self.minSpinBox.singleStep() d['maxSpinBox.minimum'] = self.maxSpinBox.minimum() d['maxSpinBox.maximum'] = self.maxSpinBox.maximum() d['maxSpinBox.singleStep'] = self.maxSpinBox.singleStep() return d
class NavigationGraphicsView(QtWidgets.QGraphicsView): '''Graphics view for dataset navigation. The view usually displays an auto-scalled low resolution overview of the scene with a red box indicating the area currently displayed in the high resolution view. :SIGNALS: * :attr:`mousePressed` * :attr:`mouseMoved` ''' BOXCOLOR = QtGui.QColor(QtCore.Qt.red) BOXWIDTH = 50 #: SIGNAL: it is emitted when a mouse button is presses on the view #: #: :param point: #: the scene position #: :param mousebutton: #: the ID of the pressed button #: :param dragmode: #: current darg mode #: #: :C++ signature: `void mousePressed(QPointF, Qt::MouseButtons, #: QGraphicsView::DragMode)` mousePressed = QtCore.Signal(QtCore.QPointF, QtCore.Qt.MouseButtons, QtWidgets.QGraphicsView.DragMode) #: SIGNAL: it is emitted when the mouse is moved on the view #: #: :param point: #: the scene position #: :param mousebutton: #: the ID of the pressed button #: :param dragmode: #: current darg mode #: #: :C++ signature: `void mouseMoved(QPointF, Qt::MouseButtons, #: QGraphicsView::DragMode)` mouseMoved = QtCore.Signal(QtCore.QPointF, QtCore.Qt.MouseButtons, QtWidgets.QGraphicsView.DragMode) def __init__(self, parent=None, **kwargs): super(NavigationGraphicsView, self).__init__(parent, **kwargs) self._viewbox = None self._autoscale = True self.setMouseTracking(True) # default pen self._pen = QtGui.QPen() self._pen.setColor(self.BOXCOLOR) self._pen.setWidth(self.BOXWIDTH) @property def viewbox(self): '''Viewport box in scene coordinates''' return self._viewbox @viewbox.setter def viewbox(self, box): '''Set the viewport box in scene coordinates''' assert isinstance(box, (QtCore.QRect, QtCore.QRectF)) self._viewbox = box if self.isVisible(): # @WARNING: calling "update" on the scene causes a repaint of # *all* attached views and for each view the entire # exposedRect is updated. # Using QGraphicsView.invalidateScene with the # QtWidgets.QGraphicsScene.ForegroundLayer parameter # should be faster and repaint only one layer of the # current view. # @TODO: check #self.invalidateScene(self.sceneRect(), # QtWidgets.QGraphicsScene.ForegroundLayer) self.scene().update() def drawForeground(self, painter, rect): if not self.viewbox: return pen = painter.pen() try: box = self.viewbox.intersected(self.sceneRect()) painter.setPen(self._pen) painter.drawRect(box) #painter.drawConvexPolygon(self.viewbox) #@TODO: check finally: painter.setPen(pen) def fitInView(self, rect=None, aspectRatioMode=QtCore.Qt.KeepAspectRatio): if not rect: scene = self.scene() if scene: rect = scene.sceneRect() else: return QtWidgets.QGraphicsView.fitInView(self, rect, aspectRatioMode) @property def autoscale(self): return self._autoscale @autoscale.setter def autoscale(self, flag): self._autoscale = bool(flag) if self._autoscale: self.fitInView() else: self.setTransform(QtGui.QTransform()) self.update() def resizeEvent(self, event): if self.autoscale: self.fitInView() return QtWidgets.QGraphicsView.resizeEvent(self, event) # @TODO: use event filters def mousePressEvent(self, event): pos = self.mapToScene(event.pos()) self.mousePressed.emit(pos, event.buttons(), self.dragMode()) return QtWidgets.QGraphicsView.mousePressEvent(self, event) def mouseMoveEvent(self, event): pos = self.mapToScene(event.pos()) self.mouseMoved.emit(pos, event.buttons(), self.dragMode()) return QtWidgets.QGraphicsView.mouseMoveEvent(self, event)
class PreferencesDialog(QtWidgets.QDialog, PreferencesDialogBase): '''Extendible preferences dialogg for GSDView. :SIGNALS: * :attr:`apply` ''' #: SIGNAL: it is emitted when modifications are applied #: #: :C++ signature: `void apply()` apply = QtCore.Signal() # @TODO: also look at # .../python-qt4-doc/examples/tools/settingseditor/settingseditor.py def __init__(self, parent=None, flags=QtCore.Qt.WindowFlags(0), **kwargs): super(PreferencesDialog, self).__init__(parent, flags, **kwargs) self.setupUi(self) self.setWindowIcon(qtsupport.geticon('preferences.svg', __name__)) # remove empty page page = self.stackedWidget.widget(0) self.stackedWidget.removeWidget(page) # app pages icon = qtsupport.geticon('preferences.svg', __name__) self.addPage(GeneralPreferencesPage(), icon, self.tr('General')) #~ icon = qt4support.geticon('harddisk.svg', __name__) #~ self.addPage(CachePreferencesPage(), icon, self.tr('Cache')) assert self.listWidget.count() == self.stackedWidget.count() self.listWidget.currentItemChanged.connect(self.changePage) applybutton = self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply) applybutton.clicked.connect(self.apply) # @TODO: check #@QtCore.Slot(QtWidgets.QListWidgetItem, QtWidgets.QListWidgetItem) def changePage(self, current, previous): if not current: current = previous self.stackedWidget.setCurrentIndex(self.listWidget.row(current)) def addPage(self, page, icon, label=None): if not (hasattr(page, 'load') and hasattr(page, 'save')): raise TypeError('preference pages must have both "load" and ' '"save" methods') index = self.stackedWidget.addWidget(page) item = QtWidgets.QListWidgetItem(icon, label) self.listWidget.addItem(item) assert self.listWidget.row(item) == index def removePageIndex(self, index): if 0 <= index < self.stackedWidget.count(): page = self.stackedWidget.widget(index) self.stackedWidget.removeWidget(page) self.listWidget.model().removeRow(index) def removePage(self, page): index = self.stackedWidget.indexOf(page) if 0 <= index < self.stackedWidget.count(): self.stackedWidget.removeWidget(page) self.listWidget.model().removeRow(index) def load(self, settings): for index in range(self.stackedWidget.count()): page = self.stackedWidget.widget(index) page.load(settings) def save(self, settings): for index in range(self.stackedWidget.count()): page = self.stackedWidget.widget(index) page.save(settings)
class GraphicsViewMonitor(QtCore.QObject): '''Emit signals when a registered graphics view changes status. :SIGNALS: * :attr:`leave` * :attr:`scrolled` * :attr:`resized` * :attr:`viewportResized` * :attr:`mouseMoved` ''' ##: SIGNAL: it is emitted when the mouse pointer enterss the scene ##: ##: :C++ signature: `void enter(QGraphicsView*)` ###enter = QtCore.Signal(QtWidgets.QGraphicsScene) ##enter = QtCore.Signal(QtCore.QObject) # @TODO: check #: SIGNAL: it is emitted when the mouse pointer leaves the scene #: #: :C++ signature: `void leave(QGraphicsView*)` leave = QtCore.Signal(QtWidgets.QGraphicsScene) #: SIGNAL: it is emitted when a graphics view is scrolled #: #: :C++ signature: `void scrolled(QGraphicsView*)` scrolled = QtCore.Signal(QtWidgets.QGraphicsView) #: SIGNAL: it is emitted when the graphicsview window is resized #: #: :C++ signature: `void resized(QGraphicsView*, QSize)` resized = QtCore.Signal(QtWidgets.QGraphicsView, QtCore.QSize) # @TODO: explain difference with previous #: SIGNAL: #: #: :C++ signature: `void viewportResized(QGraphicsView*)` viewportResized = QtCore.Signal(QtWidgets.QGraphicsView) #: SIGNAL: it is emitted when the mouse pointer is moved on the scene #: #: :C++ signature: `void mouseMoved(QtWidgets.QGraphicsScene, #: QtCore.QPointF, #: QtCore.Qt.MuseButtons)` mouseMoved = QtCore.Signal(QtWidgets.QGraphicsScene, QtCore.QPointF, QtCore.Qt.MouseButtons) ##: SIGNAL: ##: ##: :C++ signature: `newPos(QtCore.QObject, QPoint)` ###newPos = QtCore.Signal(QtWidgets.QGraphicsView, QtCore.QPoint) ##newPos = QtCore.Signal(QtCore.QObject, QtCore.QPoint) # @TODO: check # @TODO: use signal mappers #~ def __init__(self, parent=None, **kwargs): #~ super(GraphicsViewMonitor, self).__init__(parent, **kwargs) #~ self.mappers = {} #~ self.mappers['scroll'] = QtCore.QSignalMapper(self, #~ mapped=self.scrolled) #~ #self.mappers['scroll'].mapped.connect(self.scrolled) #~ self.mappers['scale'] = QtCore.QSignalMapper( #~ self, mapped=self.scaled) #~ #self.mappers['scale'].mapped.connect(self.scaled) #~ def register(self, graphicsview): #~ graphicsview.horizontalScrollBar().valueChanged.connect( #~ self.mappers['scroll'].map) #~ graphicsview.verticalScrollBar().valueChanged.connect( #~ self.mappers['scroll'].map) #~ self.mappers['scroll'].setMapping(graphicsview, graphicsview) #~ graphicsview.viewportResized.connect(self.mappers['scale'].map) #~ self.mappers['scale'].setMapping(graphicsview, graphicsview) def register(self, graphicsview): graphicsview.horizontalScrollBar().valueChanged.connect( lambda: self.scrolled.emit(graphicsview)) graphicsview.verticalScrollBar().valueChanged.connect( lambda: self.scrolled.emit(graphicsview)) graphicsview.horizontalScrollBar().rangeChanged.connect( lambda: self.viewportResized.emit(graphicsview)) graphicsview.verticalScrollBar().rangeChanged.connect( lambda: self.viewportResized.emit(graphicsview)) graphicsview.installEventFilter(self) # Many views can refer to the same scene so before installing a new # event filter old ones are removed scene = graphicsview.scene() if scene: scene.removeEventFilter(self) scene.installEventFilter(self) def eventFilter(self, obj, event): # @TODO: use an event map (??) if event.type() == QtCore.QEvent.Resize: assert isinstance(obj, QtWidgets.QGraphicsView) self.resized.emit(obj, event.size()) elif event.type() == QtCore.QEvent.GraphicsSceneMouseMove: assert isinstance(obj, QtWidgets.QGraphicsScene) self.mouseMoved.emit(obj, event.scenePos(), event.buttons()) elif event.type() == QtCore.QEvent.Leave: # Discard events from graphicsviews if isinstance(obj, QtWidgets.QGraphicsScene): self.leave.emit(obj) return obj.eventFilter(obj, event)