# ----------------------------------------------------------------------------
# This file is part of qarbon (http://qarbon.rtfd.org/)
#
# Copyright (c) 2013 European Synchrotron Radiation Facility, Grenoble, France
#
# Distributed under the terms of the GNU Lesser General Public License,
# either version 3 of the License, or (at your option) any later version.
# See LICENSE.txt for more info.
# ----------------------------------------------------------------------------
"""Multiple axis (axes) widget."""
__all__ = ["Axis", "AxesWidget"]
import weakref
from qarbon.core import State
from qarbon.color import getCSSColorFromState
from qarbon.external.enum import Enum
from qarbon.external.qt import QtCore, QtGui
from qarbon.qt.gui.icon import Icon
from qarbon.qt.gui.groupbox import GroupBox
# TODO: implement ?1?!
def getStatusBar():
return
class Column(Enum):
Label, Position, Icon, Steps, StepLeft, StepRight, Stop = range(7)
class PositionColumn(Enum):
Read, Write, ReadWrite = range(3)
__height_hint = None
def get_height_hint():
"""Little trick to get a uniform height hint between all widgets"""
global __height_hint
if __height_hint is None:
h1 = QtGui.QComboBox().sizeHint().height()
h2 = QtGui.QLineEdit().sizeHint().height()
h3 = QtGui.QDoubleSpinBox().sizeHint().height()
h4 = QtGui.QPushButton().sizeHint().height()
__height_hint = min(h1, h2, h3, h4)
return __height_hint
__minimum_height_hint = None
def get_minimum_height_hint():
"""Little trick to get a uniform height hint between all widgets"""
global __minimum_height_hint
if __minimum_height_hint is None:
h1 = QtGui.QComboBox().minimumSizeHint().height()
h2 = QtGui.QLineEdit().minimumSizeHint().height()
h3 = QtGui.QDoubleSpinBox().minimumSizeHint().height()
h4 = QtGui.QPushButton().minimumSizeHint().height()
__minimum_height_hint = min(h1, h2, h3, h4)
return __minimum_height_hint
class DisplayLabel(QtGui.QLabel):
StyleT = "DisplayLabel {border-width:1px; border-radius: 4px; %s}"
UnmodifiedStyle = StyleT % "border-style:transparent;"
ModifiedStyle = StyleT % "border-style:solid; border-color: blue;"
def __init__(self, axis, parent=None):
super(DisplayLabel, self).__init__(parent)
self.setStyleSheet(self.UnmodifiedStyle)
self.axis = axis
def setValue(self, value):
if value is None:
value = ""
else:
value += ":"
self.setText(value)
def setModified(self, yesno):
if yesno:
s = "font-weight: bold; text-decoration: underline"
s = self.ModifiedStyle
else:
s = ""
s = self.UnmodifiedStyle
self.setStyleSheet(s)
def contextMenuEvent(self, event):
menu = QtGui.QMenu(self)
refreshAction = QtGui.QAction(Icon("view-refresh"), "Refresh", self)
refreshAction.triggered.connect(self.axis.refresh)
menu.addAction(refreshAction)
menu.popup(event.globalPos())
class ValueLabel(QtGui.QLabel):
def __init__(self, axis, parent=None):
super(ValueLabel, self).__init__(parent)
self.axis = axis
def setValue(self, value):
if value is None:
value = ""
self.setText(value)
class ValueSpinBox(QtGui.QDoubleSpinBox):
SpinStyleT = 'ValueSpinBox {font-family: "Monospace"; %s}'
# : value applied signal
# :
# : emitted when the spinbox enter/return key is pressed
valueApplied = QtCore.Signal()
def __init__(self, axis, parent=None):
super(ValueSpinBox, self).__init__(parent)
self.axis = axis
self.setAccelerated(True)
# self.setButtonSymbols(self.PlusMinus)
# self.setFrame(False)
self.setDecimals(3)
self.setMinimum(float("-inf"))
self.setMaximum(float("+inf"))
self.setAlignment(QtCore.Qt.AlignLeft)
self.setStyleSheet(self.SpinStyleT % "")
def setValue(self, value, emit=True):
if value is None:
value = float('nan')
if emit:
return super(ValueSpinBox, self).setValue(value)
blocked = self.signalsBlocked()
try:
self.blockSignals(True)
result = super(ValueSpinBox, self).setValue(value)
finally:
self.blockSignals(blocked)
return result
def setUnit(self, unit):
if unit is None or not len(unit):
unit = ""
elif not unit.startswith(" "):
unit = " " + unit
self.setSuffix(unit)
def setState(self, state):
if state is None:
state = State._Invalid
styleSheet = getCSSColorFromState(state)
self.setStyleSheet(self.SpinStyleT % styleSheet)
def setSingleStep(self, value):
if value is None:
return
return super(ValueSpinBox, self).setSingleStep(value)
def setMinimum(self, value):
if value is None:
value = float('-inf')
return super(ValueSpinBox, self).setMinimum(value)
def setMaximum(self, value):
if value is None:
value = float('+inf')
return super(ValueSpinBox, self).setMaximum(value)
def sizeHint(self):
size = super(ValueSpinBox, self).sizeHint()
size.setHeight(get_height_hint())
return size
def minimumSizeHint(self):
size = super(ValueSpinBox, self).minimumSizeHint()
size = QtCore.QSize(size.width(), get_minimum_height_hint())
return size
def keyPressEvent(self, event):
key = event.key()
if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self.valueApplied.emit()
elif key == QtCore.Qt.Key_Escape:
self.axis.refresh()
elif key == QtCore.Qt.Key_F5:
self.axis.refresh()
else:
return super(ValueSpinBox, self).keyPressEvent(event)
class StepSize(QtGui.QComboBox):
def __init__(self, axis, parent=None):
super(StepSize, self).__init__(parent)
self.axis = axis
def addSteps(self, steps):
icon = Icon(":/controls/step2.png")
for step_label, step_value in steps:
self.addItem(icon, step_label, step_value)
def setSteps(self, steps):
self.clear()
if steps is None:
steps = []
self.addSteps(steps)
def setCurrentStep(self, step):
index = self.findData(step, QtCore.Qt.UserRole)
self.setCurrentIndex(index)
def sizeHint(self):
size = super(StepSize, self).sizeHint()
size = QtCore.QSize(size.width(), get_height_hint())
return size
def minimumSizeHint(self):
size = super(StepSize, self).minimumSizeHint()
size = QtCore.QSize(size.width(), get_minimum_height_hint())
return size
class IconButton(QtGui.QPushButton):
def __init__(self, axis, icon=None, parent=None):
super(IconButton, self).__init__(parent)
self.axis = axis
if icon is not None:
self.setIcon(icon)
self.setIconSize(QtCore.QSize(16, 16))
class SquareButton(IconButton):
def __init__(self, axis, icon=None, parent=None):
super(SquareButton, self).__init__(axis, icon=icon, parent=parent)
self.axis = axis
def sizeHint(self):
h = get_height_hint()
return QtCore.QSize(h, h)
def minimumSizeHint(self):
h = get_minimum_height_hint()
return QtCore.QSize(h, h)
class StepLeftButton(SquareButton):
def __init__(self, axis, parent=None):
# media-skip-backward
# media-seek-backward
# go-previous
# edit-undo
# fwk4:/backward.png
# fwk4:/1leftarrow.png
icon = Icon("edit-undo")
super(StepLeftButton, self).__init__(axis, icon=icon,
parent=parent)
class StepRightButton(SquareButton):
def __init__(self, axis, parent=None):
# media-skip-forward
# media-seek-forward
# go-next
# edit-redo
# fwk4:/forward.png
# fwk4:/1rightarrow.png
icon = Icon("edit-redo")
super(StepRightButton, self).__init__(axis, icon=icon,
parent=parent)
class StopButton(SquareButton):
def __init__(self, axis, parent=None):
# media-playback-stop
# process-stop
icon = Icon("process-stop")
super(StopButton, self).__init__(axis, icon=icon, parent=parent)
[docs]class Axis(QtCore.QObject):
# : position changed signal
# :
# : emitted when the axis position has changed
# : the signal is emitted with the axis name and position value
positionChanged = QtCore.Signal(str, float)
# : position changed signal
# :
# : emitted when the axis minimum allowed position has changed
# : the signal is emitted with the axis name and minimum position value
limitsChanged = QtCore.Signal(str, list)
# : step sizes changed signal
# :
# : emitted when the allowed axis step sizes has changed
# : the signal is emitted with the axis name and step sizes
stepsChanged = QtCore.Signal(str, object)
# : current step changed signal
# :
# : emitted when the current step size has changed
# : the signal is emitted with the axis name and current step size
currentStepChanged = QtCore.Signal(str, float)
# : state changed signal
# :
# : emitted when the axis state has changed
# : the signal is emitted with the axis name, old state and new state
stateChanged = QtCore.Signal(str, object, object)
# : label changed signal
# :
# : emitted when the axis label has changed
# : the signal is emitted with the axis name and label value
labelChanged = QtCore.Signal(str, str)
# : units changed signal
# :
# : emitted when the axis units has changed
# : the signal is emitted with the axis name and units value
unitChanged = QtCore.Signal(str, str)
def __init__(self, axis_info, axes, parent=None):
super(Axis, self).__init__(parent)
self._axes = weakref.ref(axes)
self.name = axis_info['name']
self.index = axis_info['index']
self.role = axis_info.get('role', str(self.index))
self._label = axis_info.get('username', self.name)
self._position = None # float('nan')
self._limits = None # float('-inf'), float('+inf')
self._state = None
self._steps = None
self._current_step = None
self._unit = None
@property
[docs] def axes(self):
return self._axes()
[docs] def refresh(self):
self.state = self.getState(cache=False)
self.limits = self.getLimits(cache=False)
self.position = self.getPosition(cache=False)
[docs] def getPosition(self, cache=True):
if cache and self._position is not None:
result = self._position
else:
self._position = result = self.axes.position(self.name)
return result
[docs] def setPosition(self, position, emit=True):
self._position = position
if emit:
self.positionChanged.emit(self.name, position)
#: This property contains the axis position
#:
#: **Access functions:**
#:
#: * :meth:`Axis.getPosition`
#: * :meth:`Axis.setPosition`
position = QtCore.Property(str, getPosition, setPosition)
[docs] def getLimits(self, cache=True):
if cache and self._limits is not None:
result = self._limits
else:
self._limits = result = list(self.axes.limits(self.name))
return result
[docs] def setLimits(self, limits, emit=True):
self._limits = list(limits)
if emit:
self.limitsChanged.emit(self.name, limits)
#: This property contains the axis limits
#:
#: **Access functions:**
#:
#: * :meth:`Axis.getLimits`
#: * :meth:`Axis.setLimits`
limits = QtCore.Property(list, getLimits, setLimits)
[docs] def getState(self, cache=True):
if cache and self._state is not None:
result = self._state
else:
self._state = result = self.axes.state(self.name)
return result
[docs] def setState(self, state, emit=True):
old_state = self._state
if state is None:
state = State._Invalid
self._state = state
if emit:
self.stateChanged.emit(self.name, old_state, state)
#: This property contains the axis state
#:
#: **Access functions:**
#:
#: * :meth:`Axis.getState`
#: * :meth:`Axis.setState`
state = QtCore.Property(object, getState, setState)
[docs] def getLabel(self):
return self._label
[docs] def setLabel(self, label, emit=True):
if label is None:
label = ""
self._label = label
if emit:
self.labelChanged.emit(self.name, label)
#: This property contains the axis label
#:
#: **Access functions:**
#:
#: * :meth:`Axis.getLabel`
#: * :meth:`Axis.setLabel`
label = QtCore.Property(str, getLabel, setLabel)
[docs] def getSteps(self):
return self._steps
[docs] def setSteps(self, steps, emit=True):
if steps is None:
steps = []
self._steps = steps
if emit:
self.stepsChanged.emit(self.name, steps)
#: This property contains the axis steps
#:
#: **Access functions:**
#:
#: * :meth:`Axis.getSteps`
#: * :meth:`Axis.setSteps`
steps = QtCore.Property(str, getSteps, setSteps)
[docs] def getCurrentStep(self):
return self._current_step
[docs] def setCurrentStep(self, current_step, emit=True):
self._current_step = current_step
if emit:
self.currentStepChanged.emit(self.name, current_step)
#: This property contains the axis current step size
#:
#: **Access functions:**
#:
#: * :meth:`Axis.getCurrentStep`
#: * :meth:`Axis.setCurrentStep`
currentStep = QtCore.Property(float, getCurrentStep, setCurrentStep)
[docs] def getUnit(self):
return self._unit
[docs] def setUnit(self, unit, emit=True):
if unit is None:
unit = ""
self._unit = unit
if emit:
self.unitChanged.emit(self.name, unit)
#: This property contains the axis unit
#:
#: **Access functions:**
#:
#: * :meth:`Axis.getUnit`
#: * :meth:`Axis.setUnit`
unit = QtCore.Property(float, getUnit, setUnit)
[docs] def move(self, absolute_position):
self.axes.move(self.name, absolute_position)
[docs] def moveRelative(self, relative_position):
self.move(self.position + relative_position)
[docs] def moveUp(self):
self.moveRelative(+self.currentStep)
[docs] def moveDown(self):
self.moveRelative(-self.currentStep)
stepUp = moveUp
stepDown = moveDown
[docs] def stop(self):
self.axes.abort(self.name)
ToolTipTemplate = """<html>axis <u>{axis.label}</u> is in \
<b>{axis.state.name}</b> state, at position <b>{axis.position}</b><br/>
Limits set to <b>[{axis.limits[0]}, {axis.limits[1]}]</b><br/>
(the hardware name for this axis is: <i>{axis.name}</i>)"""
def main():
from qarbon.qt.gui.application import Application
app = Application()
class Axes(object):
axes = {}
def get(self, name):
if not name in self.axes:
self.axes[name] = [State.On, 0, [-10, 10]]
return self.axes[name]
def state(self, name):
return self.get(name)[0]
def position(self, name):
return self.get(name)[1]
def limits(self, name):
return self.get(name)[2]
def move(self, name, v):
self.get(name)[1] = v
def abort(self, name):
pass
p = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
p.setLayout(layout)
axes = Axes()
axes_list = []
for i in range(16):
name = "axis%02d" % i
label = "Axis %02d" % i
info = dict(name=name, label=label, role=str(i), index=i)
axis = Axis(info, axes)
axis.steps = [["1 um", 0.001], ["10 um", 0.01], ["1 mm", 1]]
axis.currentStep = 0.01
axis.unit = "mm"
axis.state = State(i)
axes_list.append(axis)
axis_w1 = AxesWidget(axes=axes_list)
axis_w1.title = "First axes"
layout.addWidget(axis_w1)
axes_list[0].setLabel("Bla")
# simulate motor at 5.4
axes.move("axis00", 5.4)
axes_list[0].setPosition(5.4)
axis_w2 = AxesWidget(axes=axes_list[:5])
axis_w2.title = "Second axes"
layout.addWidget(axis_w2)
layout.addStretch(1)
p.show()
app.exec_()
if __name__ == "__main__":
main()