from .. import QtCore, QtWidgets, Signal, utils
from ..limiter import as_limiter, LimitError, NumberLimit
from ..formatter import as_formatter, str_to_float, order_to_pos, pos_to_order
[docs]class TextEdit(QtWidgets.QLineEdit):
"""
Expanded text edit.
Maintains internally stored consistent value (which can be, e.g., accessed from different threads).
"""
def __init__(self, parent, value=None):
super().__init__(parent)
self.returnPressed.connect(self._on_enter)
self.editingFinished.connect(self._on_edit_done)
self._value=None
if value is not None:
self.set_value(value)
self.textChanged.connect(self._on_change_text)
self._expand_border=None
self._exp_parameters=None
self._exp_placeholder=None
self._exp_frame=None
self._expanded=False
def _on_edit_done(self):
self.set_value(self.text())
self.value_entered.emit(self._value)
def _on_enter(self):
self._on_edit_done()
self.clearFocus()
def _on_change_text(self, text):
if not self.isModified():
self.set_value(text)
[docs] def keyPressEvent(self, event):
if event.key()==QtCore.Qt.Key_Escape:
self.clearFocus()
self.show_value()
else:
super().keyPressEvent(event)
[docs] def set_expandable(self, left=0, right=0, top=0, bottom=0):
"""
Make text edit expandable.
If it is expandable, the edit size is expanded by the given size into the corresponding directions.
If all are zero, the widget behaves as normal.
"""
if any([left,right,top,bottom]):
self._expand_border=(left,right,top,bottom)
else:
self._expand_border=None
def _make_exp_frame(self, pos, size, border):
frame=QtWidgets.QFrame(utils.get_top_parent(self))
frame.setLayout(QtWidgets.QVBoxLayout())
pad=4
frame.layout().setContentsMargins(pad,pad,pad,pad)
pos=frame.parentWidget().mapFromGlobal(pos)
frame.move(pos.x()-border[0]-pad,pos.y()-border[2]-pad)
frame.setFixedWidth(size.width()+border[0]+border[1]+pad*2)
frame.setFixedHeight(size.height()+border[2]+border[3]+pad*2)
return frame
[docs] def focusInEvent(self, evt):
if self._expand_border is not None and self._exp_placeholder is None:
loc=utils.get_widget_location(self)
size_rng=self.minimumWidth(),self.maximumWidth(),self.minimumHeight(),self.maximumHeight()
self._exp_parameters=loc,size_rng
parent=self.parentWidget()
pos,size=parent.mapToGlobal(self.pos()),self.size()
parent.setUpdatesEnabled(False)
self._exp_placeholder=QtWidgets.QWidget(parent)
self._exp_placeholder.setFixedWidth(self.width())
self._exp_placeholder.setFixedHeight(self.height())
if loc is not None:
utils.place_widget_at_location(self._exp_placeholder,loc)
loc.layout.removeWidget(self)
self._exp_frame=self._make_exp_frame(pos,size,self._expand_border)
self._exp_frame.layout().addWidget(self)
self._exp_frame.setVisible(True)
self.setFocus()
parent.setUpdatesEnabled(True)
self._expanded=True
return super().focusInEvent(evt)
[docs] def focusOutEvent(self, evt):
if self._expanded:
self._expanded=False
loc,size_rng=self._exp_parameters
self.setMinimumWidth(size_rng[0])
self.setMaximumWidth(size_rng[1])
self.setMinimumHeight(size_rng[2])
self.setMaximumHeight(size_rng[3])
self._exp_frame.layout().removeWidget(self)
utils.place_widget_at_location(self,loc)
self._exp_frame.hide()
utils.delete_widget(self._exp_placeholder)
utils.delete_widget(self._exp_frame)
self._exp_parameters=None
self._exp_placeholder=None
self._exp_frame=None
return super().focusOutEvent(evt)
value_entered=Signal(object)
"""Signal emitted when value is entered (regardless of whether it stayed the same)"""
value_changed=Signal(object)
"""Signal emitted when value is changed"""
[docs] def get_value(self):
"""Get current text value"""
return self._value
[docs] def show_value(self, interrupt_edit=False):
"""
Display currently stored text value
If ``interrupt_edit==True`` and the edit is currently being modified by the user, don't update the display.
"""
if (not self.hasFocus()) or interrupt_edit:
self.setText(self._value)
[docs] def set_value(self, value, notify_value_change=True, interrupt_edit=False):
"""
Set current text value.
If ``notify_value_change==True``, emit the `value_changed` signal; otherwise, change value silently.
If ``interrupt_edit==True`` and the edit is currently being modified by the user, don't update the display (but still update the internally stored value).
"""
value_changed=False
value=str(value)
if self._value!=value:
self._value=value
if notify_value_change:
self.value_changed.emit(self._value)
value_changed=True
self.show_value(interrupt_edit=interrupt_edit)
return value_changed
class MultilineTextEdit(QtWidgets.QPlainTextEdit):
"""
Multi-line text edit.
Maintains internally stored consistent value (which can be, e.g., accessed from different threads).
If ``continuous_update==True``, update signals are sent any time the content is edited;
otherwise, they are sent only when the edit is done (i.e., focus is lost).
"""
def __init__(self, parent, value=None, continuous_update=False):
super().__init__(parent)
self._value=None
self._continuous_update=continuous_update
if value is not None:
self.set_value(value)
self.textChanged.connect(self._on_change_text)
def _on_text_update(self):
text=self.toPlainText()
if text!=self._value:
self._value=text
self.value_changed.emit(self._value)
self.value_entered.emit(self._value)
def _on_change_text(self):
if self._continuous_update:
self._on_text_update()
def focusOutEvent(self, evt):
if not self._continuous_update:
self._on_text_update()
super().focusOutEvent(evt)
value_entered=Signal(object)
"""Signal emitted when value is entered (regardless of whether it stayed the same)"""
value_changed=Signal(object)
"""Signal emitted when value is changed"""
def get_value(self):
"""Get current text value"""
return self._value
def show_value(self, interrupt_edit=False):
"""
Display currently stored text value
If ``interrupt_edit==True`` and the edit is currently being modified by the user, don't update the display.
"""
if (not self.hasFocus()) or interrupt_edit:
self.setPlainText(self._value)
def set_value(self, value, notify_value_change=True, interrupt_edit=False):
"""
Set current text value.
If ``notify_value_change==True``, emit the `value_changed` signal; otherwise, change value silently.
If ``interrupt_edit==True`` and the edit is currently being modified by the user, don't update the display (but still update the internally stored value).
"""
value_changed=False
value=str(value)
if self._value!=value:
self._value=value
if notify_value_change:
self.value_changed.emit(self._value)
value_changed=True
self.show_value(interrupt_edit=interrupt_edit)
return value_changed
[docs]class NumEdit(QtWidgets.QLineEdit):
"""
Labview-style numerical edit.
Maintains internally stored consistent value (which can be, e.g., accessed from different threads).
Supports different number representations, metric prefixes (in input or output), keyboard shortcuts (up/down for changing number, escape for cancelling).
Args:
parent: parent widget
value: initial value (``None`` means no value is set)
limiter: number limiter (for details, see :meth:`set_limiter`)
formatter: number formatter (for details, see :meth:`set_formatter`)
custom_steps: if not ``None``, can specify custom fixed value steps when up/down keys are pressed with a modifier key (Control, Alt, or Shift)
specifies a dictionary ``{'ctrl':ctrl_step, 'alt':alt_step, 'shift':shift_step}`` with the corresponding steps (missing elements mean that the modifier key is ignored)
"""
def __init__(self, parent, value=None, limiter=None, formatter=None, custom_steps=None):
super().__init__(parent)
self.limiter=as_limiter(limiter)
self.formatter=as_formatter(formatter)
self.custom_steps=custom_steps or {}
self.returnPressed.connect(self._on_enter)
self.editingFinished.connect(self._on_edit_done)
self._value=None
if value is not None:
self.set_value(value)
else:
self.set_value(0)
if self._value is None:
raise ValueError("can't assign a safe default value")
self.textChanged.connect(self._on_change_text)
def _on_edit_done(self):
self.set_value(self._read_input())
self.value_entered.emit(self._value)
def _on_enter(self):
self._on_edit_done()
self.clearFocus()
def _on_change_text(self, text): # pylint: disable=unused-argument
if not self.isModified():
try:
value=str_to_float(str(self.text()))
self.set_value(value)
except ValueError:
pass
[docs] def keyPressEvent(self, event):
k=event.key()
m=event.modifiers()
if k==QtCore.Qt.Key_Escape:
self.show_value(interrupt_edit=True)
self.clearFocus()
elif k in [QtCore.Qt.Key_Up,QtCore.Qt.Key_Down]:
try:
step=None
if m==QtCore.Qt.ControlModifier and "ctrl" in self.custom_steps:
step=self.custom_steps["ctrl"]
elif m==QtCore.Qt.ShiftModifier and "shift" in self.custom_steps:
step=self.custom_steps["shift"]
elif m==QtCore.Qt.AltModifier and "alt" in self.custom_steps:
step=self.custom_steps["alt"]
else:
cursor_order=self.get_cursor_order()
if cursor_order!=None:
step=10**(cursor_order)
if step is not None:
str_value=str(self.text())
num_value=str_to_float(str_value)
if k==QtCore.Qt.Key_Up:
self.set_value(num_value+step,interrupt_edit=True)
else:
self.set_value(num_value-step,interrupt_edit=True)
except ValueError:
self.show_value(interrupt_edit=True)
else:
super().keyPressEvent(event)
def _read_input(self):
try:
return str_to_float(str(self.text()))
except ValueError:
return self._value
[docs] def set_limiter(self, limiter, new_value=None):
"""
Change current numerical limiter.
Limiter can be a callable object which takes a single value and either returns a limited value, or raises :exc:`.limiter.LimitError` if it should be ignored;
or it can be a tuple ``(lower, upper, action, value_type)``, where ``lower`` and ``upper`` are the limits (``None`` means no limits),
``action`` defines out-of-limit action (either ``"ignore"`` to ignore entered value, or ``"coerce"`` to truncate to the nearest limit),
and ``value_type`` can be ``None`` (keep value as is), ``"float"`` (cast value to float), ``"int"`` (cast value to int).
If the tuple is shorter, the missing parts are filled by default values ``(None, None, "ignore", None)``.
"""
self.limiter=as_limiter(limiter)
if new_value is None:
new_value=self._value
new_value=self._coerce_value(new_value,coerce_on_limit=True)
if new_value!=self._value:
self.set_value(new_value)
[docs] def set_custom_steps(self, custom_steps=None):
"""
Specify custom fixed value steps when up/down keys are pressed with a modifier key (Control, Alt, or Shift).
`custom_steps` is a dictionary ``{'ctrl':ctrl_step, 'alt':alt_step, 'shift':shift_step}``
with the corresponding steps (missing elements mean that the modifier key is ignored).
"""
self.custom_steps=custom_steps or {}
[docs] def get_cursor_order(self):
"""Get a decimal order of the text cursor"""
str_value=str(self.text())
cursor_pos=self.cursorPosition()
return pos_to_order(str_value,cursor_pos)
[docs] def set_cursor_order(self, order):
"""Move text cursor to a given decimal order"""
if order is not None:
new_cursor_pos=order_to_pos(str(self.text()),order)
self.setCursorPosition(new_cursor_pos)
def _coerce_value(self, value, coerce_on_limit=False):
for _ in range(10):
str_value=self.formatter(value)
num_value=str_to_float(str_value)
try:
new_value=self.limiter(num_value)
except LimitError:
if coerce_on_limit and isinstance(self.limiter,NumberLimit):
if self.limiter.range[0] is not None and num_value<self.limiter.range[0]:
new_value=self.limiter.range[0]
else:
new_value=self.limiter.range[1]
else:
raise
if new_value==value:
return new_value
value=new_value
raise ValueError("couldn't coerce the new value")
[docs] def repr_value(self, value):
"""Return representation of `value` according to the current numerical format"""
return self.formatter(value)
value_entered=Signal(object)
"""Signal emitted when value is entered (regardless of whether it stayed the same)"""
value_changed=Signal(object)
"""Signal emitted when value is changed"""
[docs] def get_value(self):
"""Get current numerical value"""
return self._value
[docs] def show_value(self, interrupt_edit=False, preserve_cursor_order=True):
"""
Display currently stored numerical value
If ``interrupt_edit==False`` and the edit is currently being modified by the user, don't update the display.
If ``preserve_cursor_order==True`` and the display value is being edited, keep the decimal order of the cursor position after change.
"""
if (not self.hasFocus()) or interrupt_edit:
if preserve_cursor_order and self.hasFocus():
cursor_order=self.get_cursor_order()
self.setText(self.formatter(self._value))
if cursor_order is not None:
self.set_cursor_order(cursor_order)
else:
self.setText(self.formatter(self._value))
[docs] def set_value(self, value, notify_value_change=True, interrupt_edit=False, preserve_cursor_order=True):
"""
Set and display current numerical value.
If ``notify_value_change==True``, emit the ``value_changed`` signal; otherwise, change value silently.
If ``interrupt_edit==False`` and the edit is currently being modified by the user, don't update the display (but still update the internally stored value).
If ``preserve_cursor_order==True`` and the display value is being edited, keep the decimal order of the cursor position after change.
"""
value_changed=False
try:
value=self._coerce_value(value)
if self._value!=value:
self._value=value
if notify_value_change:
self.value_changed.emit(self._value)
value_changed=True
except LimitError:
pass
self.show_value(interrupt_edit=interrupt_edit,preserve_cursor_order=preserve_cursor_order)
return value_changed