# encoding: utf-8
import woo.config
if 'qt5' in woo.config.features:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5 import QtGui
from minieigen import *
# don't import * from woo, it would be circular import
from woo.core import Object
import re,itertools,math
import woo.core, woo.utils
log=woo.utils.makeLog('ObjectEditor')
import woo.qt
import woo.document
import sys
import os.path, os
from .ExceptionDialog import *
seqObjectShowType=True # show type headings in serializable sequences (takes vertical space, but makes the type hyperlinked)
# BUG: cursor is moved to the beginnign of the input field even if it has focus
#
# checking for focus seems to return True always and cursor is never moved
#
# the 'True or' part effectively disables the condition (so that the cursor is moved always), but it might be fixed in the future somehow
#
# if True or w.hasFocus(): w.home(False)
#
#
colormapIconSize=(50,20)
def _ensureUnicode(s): return s if isinstance(s,str) else s.decode('utf-8')
[docs]def makeColormapIcons():
ret=[]
for cmap in range(len(woo.master.cmaps)):
wd,ht=colormapIconSize
img=QtGui.QImage(wd,ht,QtGui.QImage.Format_RGB32)
for col in range(wd):
c=255*woo.utils.mapColor(col*1./wd,cmap=cmap)
#c=(0,0,0)
cc=QtGui.qRgb(int(c[0]),int(c[1]),int(c[2]))
for row in range(ht):
img.setPixel(col,row,cc)
ret.append(QtGui.QIcon(QtGui.QPixmap.fromImage(img)))
return ret
# construct colormap icons, for later use
# use this proxy function so that makeColormapIcons is not called at import time
# since that leads to crash (qt is not fully initialized yet)
_colormapIcons=[]
[docs]def getColormapIcons():
global _colormapIcons
if not _colormapIcons: _colormapIcons=makeColormapIcons()
return _colormapIcons
# HACK: extend the QLineEdit class
# set text but preserve cursor position
[docs]def QLineEdit_setTextStable(self,text):
c=self.cursorPosition(); self.setText(text); self.setCursorPosition(c)
[docs]def QSpinBox_setValueStable(self,value):
c=self.lineEdit().cursorPosition(); self.setValue(value); self.lineEdit().setCursorPosition(c)
QLineEdit.setTextStable=QLineEdit_setTextStable
QSpinBox.setValueStable=QSpinBox_setValueStable
[docs]class AttrEditor():
"""Abstract base class handing some aspects common to all attribute editors.
Holds exacly one attribute which is updated whenever it changes."""
def __init__(self,getter=None,setter=None):
self.getter,self.setter=getter,setter
self.hot=False
self.multiplier=None
self.widget=None
self.readonly=False
[docs] def refresh(self): pass
[docs] def isHot(self,hot=True):
"Called when the widget gets focus; mark it hot, change colors etc."
if hot==self.hot: return
self.hot=hot
if hot: self.setStyleSheet('QWidget { background: red }')
else: self.setStyleSheet('QWidget { background: none }')
[docs] def sizeHint(self): return QSize(100,12)
[docs] def trySetter(self,val):
if not self.readonly:
try: self.setter(val)
except BaseException as e:
log.warning(f'Error setting value {val}: {type(e)} {e}')
# self.setEnabled(False)
# log.exception(f'Error setting value {val}:')
# self.isHot(False)
[docs] def multiplierChanged(self,convSpec):
raise RuntimeError("This widget %s has no multiplierChanged method defined."%self.__class__.__name__)
[docs] def focusInEvent(self,event):
# log.info(f'AttrEditor.focusInEvent {self}')
self.doFocusIn()
super().focusInEvent(event)
[docs] def focusOutEvent(self,event):
# log.info(f'AttrEditor.focusOutEvent {self}')
self.doFocusOut()
super().focusOutEvent(event)
[docs] def doFocusIn(self):
# log.info(f'AttrEditor.doFocusIn {self}')
self.refresh()
self.isHot(True)
[docs] def doFocusOut(self):
# log.info(f'AttrEditor.doFocusOut {self}')
self.update()
self.isHot(False)
#super().focusOutEvent(event)
[docs]class AttrEditor_Bool(AttrEditor,QFrame):
def __init__(self,parent,getter,setter):
AttrEditor.__init__(self,getter,setter)
QFrame.__init__(self,parent)
self.checkBox=QCheckBox(self)
lay=QVBoxLayout(self); lay.setSpacing(0); lay.setContentsMargins(0,0,0,0); lay.addStretch(1); lay.addWidget(self.checkBox); lay.addStretch(1)
self.checkBox.clicked.connect(self.update)
[docs] def refresh(self):
assert(not self.multiplier)
self.checkBox.setChecked(self.getter())
[docs] def update(self): self.trySetter(self.checkBox.isChecked())
[docs]class AttrEditor_Int(AttrEditor,QSpinBox):
def __init__(self,parent,getter,setter):
AttrEditor.__init__(self,getter,setter)
QSpinBox.__init__(self,parent)
self.setRange(int(-1e9),int(1e9)); self.setSingleStep(1);
self.valueChanged.connect(self.update)
[docs] def refresh(self):
assert(not self.multiplier)
self.setValueStable(self.getter())
[docs] def update(self): self.trySetter(self.value())
[docs]class AttrEditor_Str(AttrEditor,QLineEdit):
def __init__(self,parent,getter,setter):
AttrEditor.__init__(self,getter,setter)
QLineEdit.__init__(self,parent)
# self.textEdited.connect(self.isHot)
#self.selectionChanged.connect(self.isHot)
#self.editingFinished.connect(self.update)
[docs] def refresh(self):
assert(not self.multiplier)
self.setTextStable(self.getter())
[docs] def update(self): self.trySetter(str(self.text()))
[docs]class AttrEditor_Float(AttrEditor,QLineEdit):
def __init__(self,parent,getter,setter):
AttrEditor.__init__(self,getter,setter)
QLineEdit.__init__(self,parent)
#self.textEdited.connect(self.isHot)
#self.selectionChanged.connect(self.isHot)
#self.editingFinished.connect(self.update)
[docs] def refresh(self):
v=self.getter()
if self.multiplier: v*=self.multiplier
self.setTextStable(str(v))
[docs] def update(self):
# when everything is deleted, don't refresh because of float('') raising ValueError
if self.hot and not self.text(): return
try:
v=float(self.text())
if self.multiplier: v/=self.multiplier
self.trySetter(v)
except ValueError: self.refresh()
[docs] def multiplierChanged(self,convSpec):
if isinstance(self.multiplier,tuple): raise RuntimeError("Float cannot have multiple units.")
if self.multiplier: self.setToolTip(u'Converting %s (× %g)'%(convSpec,self.multiplier))
else: self.setToolTip('')
self.refresh()
[docs]class AttrEditor_Quaternion(AttrEditor,QFrame):
def __init__(self,parent,getter,setter):
AttrEditor.__init__(self,getter,setter)
QFrame.__init__(self,parent)
self.grid=QHBoxLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
for i in range(4):
if i==3:
f=QFrame(self); f.setFrameShape(QFrame.VLine); f.setFrameShadow(QFrame.Sunken); f.setFixedWidth(4) # add vertical divider (axis | angle)
self.grid.addWidget(f)
w=QLineEdit('')
self.grid.addWidget(w);
w.textEdited.connect(self.isHot)
w.selectionChanged.connect(self.isHot)
w.editingFinished.connect(self.update)
[docs] def refresh(self):
assert(not self.multiplier)
val=self.getter(); axis,angle=val.toAxisAngle()
for i in (0,1,2,4):
w=self.grid.itemAt(i).widget(); w.setTextStable(str(axis[i] if i<3 else angle));
#if True or not w.hasFocus(): w.home(False)
[docs] def update(self):
try:
x=[float((self.grid.itemAt(i).widget().text())) for i in (0,1,2,4)]
except ValueError: self.refresh()
q=Quaternion(Vector3(x[0],x[1],x[2]),x[3]); q.normalize() # from axis-angle
self.trySetter(q)
[docs] def setFocus(self): self.grid.itemAt(0).widget().setFocus()
[docs]class AttrEditor_IntRange(AttrEditor,QFrame):
def __init__(self,parent,getter,setter):
AttrEditor.__init__(self,getter,setter)
QFrame.__init__(self,parent)
self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
curr,(mn,mx)=getter()
self.slider,self.spin=QSlider(self),QSpinBox(self)
self.grid.addWidget(self.spin,0,0)
self.grid.addWidget(self.slider,0,1,1,2) # rowSpan=1,columnSpan=2
self.slider.setMinimum(mn); self.slider.setMaximum(mx)
self.spin.setMinimum(mn); self.spin.setMaximum(mx)
self.slider.setOrientation(Qt.Horizontal)
self.slider.setFocusPolicy(Qt.ClickFocus)
self.spin.valueChanged.connect(self.updateFromSpin)
self.slider.sliderMoved.connect(self.sliderMoved)
self.slider.sliderReleased.connect(self.updateFromSlider)
[docs] def refresh(self):
assert(not self.multiplier)
curr,(mn,mx)=self.getter()
c=self.spin.lineEdit().cursorPosition()
self.spin.setValueStable(curr); self.spin.setMinimum(mn); self.spin.setMaximum(mx)
self.spin.lineEdit().setCursorPosition(c)
self.slider.setValue(curr)
[docs] def updateFromSpin(self):
self.slider.setValue(self.spin.value())
self.trySetter(self.slider.value())
[docs] def updateFromSlider(self):
self.spin.setValue(self.slider.value())
self.trySetter(self.slider.value())
[docs] def sliderMoved(self,val):
self.isHot(True)
self.spin.setValue(val) # self.slider.value())
[docs] def setFocus(self): self.slider.setFocus()
[docs]class AttrEditor_FloatRange(AttrEditor,QFrame):
sliDiv=500
def __init__(self,parent,getter,setter):
AttrEditor.__init__(self,getter,setter)
QFrame.__init__(self,parent)
curr,(mn,mx)=self.multipliedGetter()
self.slider,self.edit=QSlider(self),QLineEdit(str(curr))
self.grid=QHBoxLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
self.grid.addWidget(self.edit); self.grid.addWidget(self.slider)
self.grid.setStretchFactor(self.edit,1); self.grid.setStretchFactor(self.slider,2)
self.slider.setMinimum(0); self.slider.setMaximum(self.sliDiv)
self.slider.setOrientation(Qt.Horizontal)
self.slider.setFocusPolicy(Qt.ClickFocus)
self.edit.textEdited.connect(self.isHot)
self.edit.selectionChanged.connect(self.isHot)
self.edit.editingFinished.connect(self.updateFromText)
self.slider.sliderMoved.connect(self.sliderMoved) #
self.slider.sliderReleased.connect(self.sliderReleased) #
[docs] def multipliedGetter(self):
curr,(mn,mx)=self.getter()
if self.multiplier:
curr*=self.multiplier; mn*=self.multiplier; mx*=self.multiplier
return curr,(mn,mx)
[docs] def refresh(self):
curr,(mn,mx)=self.multipliedGetter()
self.edit.setTextStable('%g'%curr)
if not math.isnan(curr):
self.slider.setValue(int(self.sliDiv*((1.*curr-mn)/(1.*mx-mn))))
self.slider.setEnabled(True)
else: self.slider.setEnabled(False)
[docs] def updateFromText(self):
curr,(mn,mx)=self.multipliedGetter()
try:
value=float(self.edit.text())
self.slider.setValue(int(self.sliDiv*((1.*value-mn)/(1.*mx-mn))))
self.trySetter(value)
except ValueError: self.refresh()
[docs] def sliderPosToNum(self,sliderValue):
curr,(mn,mx)=self.getter()
return mn+sliderValue*(1./self.sliDiv)*(mx-mn)
[docs] def sliderMoved(self,newSliderPos):
self.isHot(True)
self.edit.setText('%g'%self.sliderPosToNum(newSliderPos))
[docs] def sliderReleased(self):
value=self.sliderPosToNum(self.slider.value())
self.edit.setText('%g'%value)
if self.multiplier: value/=self.multiplier
self.trySetter(value)
[docs] def setFocus(self): self.slider.setFocus()
[docs] def multiplierChanged(self,convSpec):
if isinstance(self.multiplier,tuple): raise RuntimeError("Float range cannot have multiple units.")
log.debug("New multiplier is "+str(self.multiplier))
if self.multiplier: self.setToolTip("Unit-conversion %s: factor %g"%(convSpec,self.multiplier))
else: self.setToolTip('')
self.refresh()
#pass # this is OK
[docs]class AttrEditor_Choice(AttrEditor,QFrame):
def __init__(self,parent,getter,setter,isColormap):
AttrEditor.__init__(self,getter,setter)
QFrame.__init__(self,parent)
curr,choices=getter()
self.choices=choices
if len(choices)<1: raise RuntimeError("There are 0 choices for this attribute?")
self.combo=QComboBox(self)
# if choices are single items, then we choose from those values
# otherwise the first item is code value and second is the displayed value
self.justValues=(choices[0].__class__!=tuple)
self.admitValues=[(c if self.justValues else c[0]) for c in choices]
if choices[0].__class__==tuple and len(choices[0])!=2: raise ValueError("Choice must be either single items or 2-tuples of code-value,display-value")
for c in choices:
self.combo.addItem(str(c if self.justValues else c[1]))
### hack for COLORMAPS!!, only based on length of the choice list
if isColormap:
nCmaps=len(woo.master.cmaps)
assert len(choices) in (nCmaps,nCmaps+1), "Number of colormap choices not equal to the number of known colormaps?" # +1 for default
if len(choices)==nCmaps+1:
for i in range(len(choices)): self.combo.setItemIcon(i,getColormapIcons()[(i-1) if i>0 else woo.master.cmap[0]])
else:
for i in range(len(choices)): self.combo.setItemIcon(i,getColormapIcons()[i])
self.combo.setIconSize(QSize(colormapIconSize[0],colormapIconSize[1]))
self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
self.grid.addWidget(self.combo,0,0)
self.combo.activated.connect(self.update)
[docs] def refresh(self):
curr,choices=self.getter()
# print curr,choices
#if self.justValues:
#else:
if type(curr)==int:
if curr<0 or curr>len(self.admitValues): raise ValueError("Current value out of range?")
if curr!=self.combo.currentIndex(): self.combo.setCurrentIndex(curr)
else:
if curr not in self.admitValues:
raise ValueError("Choice attribute value "+str(curr)+" is not within admitted values "+str(self.admitValues))
if self.admitValues[self.combo.currentIndex()]!=curr: # update the combo
self.combo.setCurrentIndex(self.admitValues.index(curr))
[docs] def update(self):
choice=self.choices[self.combo.currentIndex()]
val=choice if self.justValues else choice[0]
self.trySetter(val)
[docs] def setFocus(self): self.combo.setFocus()
[docs] def multiplierChanged(self,convSpec):
if isinstance(self.multiplier,tuple): raise RuntimeError("Float choice cannot have multiple units.")
else: self.setToolTip()
for i,c in enumerate(self.choices):
val=c if self.justValues else c[1]
if c.__class__!=float: raise RuntimeError("Only float choices can meaningfully support unit multipliers")
if self.multiplier: val*=self.multiplier
self.combo.setItemText(i,str(val))
self.refresh()
[docs]class AttrEditor_Bits(AttrEditor,QFrame):
checkersPerRow=3
def __init__(self,parent,getter,setter):
AttrEditor.__init__(self,getter,setter)
QFrame.__init__(self,parent)
curr,self.bits=getter()
if len(self.bits)<1: raise RuntimeError("There are 0 bits for this attribute?")
self.checkers=[]
self.value=0 # current value of checkboxes
self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
for i,b in enumerate(self.bits):
w=QCheckBox(self)
w.setText(b)
w.clicked.connect(self.update)
self.checkers.append(w)
self.grid.addWidget(w,i//self.checkersPerRow,i%self.checkersPerRow)
[docs] def refresh(self):
curr,bits=self.getter()
if curr==self.value: return
if curr>=2**len(self.checkers): raise RuntimeError("Value %d exceeds maximum value %d (=2^%d-1) representable by the UI bitfield"%(curr,2**len(self.checkers)-1,len(self.checkers)))
self.value=curr
for bit,w in enumerate(self.checkers):
b=curr&(1<<bit)
if b!=w.isChecked(): w.setChecked(b)
[docs] def update(self):
val=0
for bit,w in enumerate(self.checkers):
if w.isChecked(): val|=(1<<bit)
self.trySetter(val)
[docs] def setFocus(self): self.checkers[0].setFocus()
[docs]class AttrEditor_RgbColor(AttrEditor,QFrame):
def __init__(self,parent,getter,setter):
AttrEditor.__init__(self,getter,setter)
QFrame.__init__(self,parent)
self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
self.butt=QPushButton()
self.butt.clicked.connect(self.dialogShow)
self.checkBox=QCheckBox('',self)
self.checkBox.setStyleSheet('padding: 0px;')
self.checkBox.toggled.connect(self.toggled)
self.grid.addWidget(self.checkBox,0,0)
self.grid.addWidget(self.butt,0,1)
self.rgbWidgets=[]
self.prevR=0
self.dialog=None
for i in (0,1,2):
w=QLineEdit('')
self.grid.addWidget(w,0,i+2)
w.textEdited.connect(self.isHot)
w.selectionChanged.connect(self.isHot)
w.editingFinished.connect(self.update)
self.rgbWidgets.append(w)
[docs] def toggled(self,enabled):
rgb=self.getter()
if enabled:
for i in 0,1,2:
if math.isnan(rgb[i]): rgb[i]=self.prevR
else:
rgb[0]=float('nan')
for w in self.rgbWidgets+[self.butt]: w.setEnabled(enabled)
[docs] def to256c(self,f):
return min(255,max(0,int(f*256))) if not math.isnan(f) else 0
[docs] def to256(self,v): return (self.to256c(v[0]),self.to256c(v[1]),self.to256c(v[2]))
[docs] def to256str(self,v,sep=','): return sep.join([str(self.to256c(v[0])),str(self.to256c(v[1])),str(self.to256c(v[2]))])
[docs] def dialogShow(self):
rgb=self.getter()
self.dialog=QColorDialog(self)
self.dialog.setCurrentColor(QColor(*self.to256(rgb)))
self.dialog.setOptions(QColorDialog.NoButtons)
self.dialog.currentColorChanged.connect(self.dialogChanged)
self.dialog.show()
[docs] def dialogChanged(self):
rgba=self.dialog.currentColor().getRgb()
self.setter(Vector3(*rgba[0:3])/256)
self.refresh()
[docs] def refresh(self):
rgb=self.getter()
for i in (0,1,2):
self.rgbWidgets[i].setTextStable(str(rgb[i]))
if not math.isnan(rgb[0]): self.prevR=rgb[0]
self.butt.setStyleSheet('QPushButton { background-color: rgb(%s) }'%(self.to256str(rgb)))
if sum([math.isnan(rgb[i]) for i in (0,1,2)]):
if self.checkBox.isChecked(): self.checkBox.setChecked(False)
else:
if not self.checkBox.isChecked(): self.checkBox.setChecked(True)
[docs] def update(self):
try:
rgb=Vector3([float(self.rgbWidgets[i].text()) for i in (0,1,2)])
except ValueError: self.refresh()
self.trySetter(rgb)
[docs]class AttrEditor_FileDir(AttrEditor,QFrame):
def __init__(self,parent,getter,setter,isDir,isExisting):
AttrEditor.__init__(self,getter,setter)
QFrame.__init__(self,parent)
self.isDir,self.isExisting=isDir,isExisting
self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
w=self.nameEdit=QLineEdit('')
self.grid.addWidget(w,0,0)
w.textEdited.connect(self.isHot)
w.selectionChanged.connect(self.isHot)
w.editingFinished.connect(self.update)
b=self.butt=QPushButton()
style=QApplication.style()
if isDir: b.setIcon(style.standardIcon(QStyle.SP_DirIcon))
else: b.setIcon(style.standardIcon(QStyle.SP_FileIcon))
self.butt.clicked.connect(self.dialogShow)
self.grid.addWidget(self.butt,0,2)
rel=self.rel=QPushButton()
rel.setCheckable(True)
rel.setText('rel')
rel.setToolTip(u'Toggle absolute/relative path.\nPaths are relative to the current directory,\nwhich is now %s.'%(str(os.getcwd())))
rel.toggled.connect(self.relToggled)
rel.setStyleSheet('padding: 0px;')
self.grid.addWidget(rel,0,1)
[docs] def dialogShow(self):
curr=self.nameEdit.text()
if self.isDir: f=QFileDialog.getExistingDirectory(self,'Select directory',curr)
elif self.isExisting: f=QFileDialog.getOpenFileName(self,'Select existing file',curr)
else: f=QFileDialog.getSaveFileName(self,'Select file name',curr,options=QFileDialog.DontConfirmOverwrite)
if isinstance(f,tuple): f=f[0]
if not f: return # cancelled
f=str(f)
if self.rel.isChecked(): f=os.path.relpath(f)
self.setter(f)
[docs] def refresh(self):
f=self.getter()
self.nameEdit.setTextStable(f)
self.rel.setChecked(not os.path.isabs(f))
[docs] def update(self):
f=str(self.nameEdit.text())
self.trySetter(f)
self.rel.setChecked(not os.path.isabs(f))
[docs] def relToggled(self,isRel):
f=str(self.nameEdit.text())
if f=='': return # do nothing for empty path
if isRel: self.trySetter(os.path.relpath(f))
else: self.trySetter(os.path.abspath(f))
[docs]class AttrEditor_Se3(AttrEditor,QFrame):
def __init__(self,parent,getter,setter):
AttrEditor.__init__(self,getter,setter)
QFrame.__init__(self,parent)
self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
for row,col in itertools.product(range(2),range(5)): # one additional column for vertical line in quaternion
if (row,col)==(0,3): continue
if (row,col)==(0,4): self.grid.addWidget(QLabel(u'←<i>pos</i> ↙<i>ori</i>',self),row,col); continue
if (row,col)==(1,3):
f=QFrame(self); f.setFrameShape(QFrame.VLine); f.setFrameShadow(QFrame.Sunken); f.setFixedWidth(4); self.grid.addWidget(f,row,col); continue
w=QLineEdit('')
self.grid.addWidget(w,row,col);
w.textEdited.connect(self.isHot)
w.selectionChanged.connect(self.isHot)
w.editingFinished.connect(self.update)
[docs] def refresh(self):
pos,ori=self.getter(); axis,angle=ori.toAxisAngle()
for i in (0,1,2,4):
w=self.grid.itemAtPosition(1,i).widget(); w.setTextStable(str(axis[i] if i<3 else angle));
#if True or not w.hasFocus(): w.home(False)
for i in (0,1,2):
w=self.grid.itemAtPosition(0,i).widget(); w.setTextStable(str(pos[i]));
#if True or not w.hasFocus(): w.home(False)
[docs] def update(self):
try:
q=[float((self.grid.itemAtPosition(1,i).widget().text())) for i in (0,1,2,4)]
v=[float((self.grid.itemAtPosition(0,i).widget().text())) for i in (0,1,2)]
except ValueError: self.refresh()
qq=Quaternion(Vector3(q[0],q[1],q[2]),q[3]); qq.normalize() # from axis-angle
self.trySetter((v,qq))
[docs] def setFocus(self): self.grid.itemAtPosition(0,0).widget().setFocus()
[docs]class QLineEdit_subattr(QLineEdit):
def __init__(self,parent):
if not isinstance(parent,AttrEditor): raise TypeError(f"Parent of QLineEdit_subattr must be AttrEditor (not {type(parent).__qualname__}).")
super().__init__('',parent=parent)
self._parent=parent
self.returnPressed.connect(parent.update)
[docs] def focusInEvent(self,event):
log.warning('QLineEdit_subattr.focusInEvent')
self._parent.doFocusIn()
QLineEdit.focusInEvent(self,event) # show cursors etc
[docs] def focusOutEvent(self,event):
log.warning('QLineEdit_subattr.focusOutEvent')
self._parent.doFocusOut()
QLineEdit.focusOutEvent(self,event) # hides cursor etc
[docs]class AttrEditor_MatrixX(AttrEditor,QWidget):
def __init__(self,parent,getter,setter,rows,cols,idxConverter):
'idxConverter converts row,col tuple to either (row,col), (col) etc depending on what access is used for []'
AttrEditor.__init__(self,getter,setter)
QWidget.__init__(self,parent)
self.rows,self.cols=rows,cols
self.idxConverter=idxConverter
self.setContentsMargins(0,0,0,0)
val=self.getter()
self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
for row,col in itertools.product(range(self.rows),range(self.cols)):
w=QLineEdit_subattr(parent=self)
self.grid.addWidget(w,row,col)
[docs] def refresh(self):
val=self.getter()
for row,col in itertools.product(range(self.rows),range(self.cols)):
w=self.grid.itemAtPosition(row,col).widget()
v=val[self.idxConverter(row,col)]
mult=(self.multiplier[col] if isinstance(self.multiplier,tuple) else self.multiplier)
if mult!=None: v*=mult
w.setTextStable(str(v))
#if True or not w.hasFocus: w.home(False) # make the left-most part visible, if the text is wider than the widget
[docs] def update(self):
try:
val=self.getter()
for row,col in itertools.product(range(self.rows),range(self.cols)):
w=self.grid.itemAtPosition(row,col).widget()
if w.isModified():
v=float(w.text())
mult=(self.multiplier[col] if isinstance(self.multiplier,tuple) else self.multiplier)
if mult: v/=mult
val[self.idxConverter(row,col)]=v
log.debug('setting'+str(val))
self.trySetter(val)
except ValueError: self.refresh()
[docs] def setFocus(self): self.grid.itemAtPosition(0,0).widget().setFocus()
[docs] def multiplierChanged(self,convSpec):
if self.multiplier: self.setToolTip(convSpec)
else: self.setToolTip('')
log.debug("Multiplier changed to "+str(self.multiplier))
self.refresh()
[docs]class AttrEditor_MatrixXi(AttrEditor,QFrame):
def __init__(self,parent,getter,setter,rows,cols,idxConverter):
'idxConverter converts row,col tuple to either (row,col), (col) etc depending on what access is used for []'
AttrEditor.__init__(self,getter,setter)
QFrame.__init__(self,parent)
self.rows,self.cols=rows,cols
self.idxConverter=idxConverter
self.setContentsMargins(0,0,0,0)
self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
for row,col in itertools.product(range(self.rows),range(self.cols)):
w=QSpinBox()
w.setRange(int(-1e9),int(1e9)); w.setSingleStep(1);
self.grid.addWidget(w,row,col);
self.refresh() # refresh before connecting signals!
for row,col in itertools.product(range(self.rows),range(self.cols)):
self.grid.itemAtPosition(row,col).widget().valueChanged.connect(self.update)
[docs] def refresh(self):
val=self.getter()
for row,col in itertools.product(range(self.rows),range(self.cols)):
w=self.grid.itemAtPosition(row,col).widget().setValueStable(val[self.idxConverter(row,col)])
[docs] def update(self):
val=self.getter(); modified=False
for row,col in itertools.product(range(self.rows),range(self.cols)):
w=self.grid.itemAtPosition(row,col).widget()
if w.value()!=val[self.idxConverter(row,col)]:
modified=True; val[self.idxConverter(row,col)]=w.value()
if not modified: return
log.debug('setting'+str(val))
self.trySetter(val)
[docs] def setFocus(self): self.grid.itemAtPosition(0,0).widget().setFocus()
[docs]class AttrEditor_Vector6i(AttrEditor_MatrixXi):
def __init__(self,parent,getter,setter):
AttrEditor_MatrixXi.__init__(self,parent,getter,setter,1,6,lambda r,c:c)
[docs]class AttrEditor_Vector3i(AttrEditor_MatrixXi):
def __init__(self,parent,getter,setter):
AttrEditor_MatrixXi.__init__(self,parent,getter,setter,1,3,lambda r,c:c)
[docs]class AttrEditor_Vector2i(AttrEditor_MatrixXi):
def __init__(self,parent,getter,setter):
AttrEditor_MatrixXi.__init__(self,parent,getter,setter,1,2,lambda r,c:c)
[docs]class AttrEditor_VectorX(AttrEditor_MatrixX):
def __init__(self,parent,getter,setter):
val=getter()
AttrEditor_MatrixX.__init__(self,parent,getter,setter,1,len(val),lambda r,c:c)
[docs]class AttrEditor_Vector6(AttrEditor_MatrixX):
def __init__(self,parent,getter,setter):
AttrEditor_MatrixX.__init__(self,parent,getter,setter,1,6,lambda r,c:c)
[docs]class AttrEditor_Vector3(AttrEditor_MatrixX):
def __init__(self,parent,getter,setter):
AttrEditor_MatrixX.__init__(self,parent,getter,setter,1,3,lambda r,c:c)
[docs]class AttrEditor_Vector2(AttrEditor_MatrixX):
def __init__(self,parent,getter,setter):
AttrEditor_MatrixX.__init__(self,parent,getter,setter,1,2,lambda r,c:c)
[docs]class AttrEditor_Matrix3(AttrEditor_MatrixX):
def __init__(self,parent,getter,setter):
AttrEditor_MatrixX.__init__(self,parent,getter,setter,3,3,lambda r,c:(r,c))
[docs]class AttrEditor_MatrixXX(AttrEditor_MatrixX):
def __init__(self,parent,getter,setter):
val=getter()
AttrEditor_MatrixX.__init__(self,parent,getter,setter,val.rows(),val.cols(),lambda r,c:(r,c))
[docs]class AttrEditor_AlignedBox3(AttrEditor_MatrixX):
def __init__(self,parent,getter,setter):
AttrEditor_MatrixX.__init__(self,parent,getter,setter,2,3,lambda r,c:(r,c))
[docs]class AttrEditor_AlignedBox2(AttrEditor_MatrixX):
def __init__(self,parent,getter,setter):
AttrEditor_MatrixX.__init__(self,parent,getter,setter,2,2,lambda r,c:(r,c))
_fundamentalEditorMap={bool:AttrEditor_Bool,str:AttrEditor_Str,int:AttrEditor_Int,float:AttrEditor_Float,Quaternion:AttrEditor_Quaternion,Vector2:AttrEditor_Vector2,Vector3:AttrEditor_Vector3,Vector6:AttrEditor_Vector6,Matrix3:AttrEditor_Matrix3,Vector6i:AttrEditor_Vector6i,Vector3i:AttrEditor_Vector3i,Vector2i:AttrEditor_Vector2i,MatrixX:AttrEditor_MatrixXX,VectorX:AttrEditor_VectorX,Se3FakeType:AttrEditor_Se3,AlignedBox3:AttrEditor_AlignedBox3,AlignedBox2:AttrEditor_AlignedBox2}
_fundamentalInitValues={bool:True,str:'',int:0,float:0.0,Quaternion:Quaternion.Identity,Vector3:Vector3.Zero,Matrix3:Matrix3.Zero,Vector6:Vector6.Zero,Vector6i:Vector6i.Zero,Vector3i:Vector3i.Zero,Vector2i:Vector2i.Zero,Vector2:Vector2.Zero,Se3FakeType:(Vector3.Zero,Quaternion.Identity),AlignedBox3:(Vector3.Zero,Vector3.Zero),AlignedBox2:(Vector2.Zero,Vector2.Zero),MatrixX:MatrixX(),VectorX:VectorX()}
_attributeGuessedTypeMap={woo.core.NodeList:(woo.core.Node,),woo.core.ObjectList:(woo.core.Object,) }
[docs]def hasActiveLabel(s):
if not hasattr(s,'label') or not s.label: return False
for t in s._getAllTraits():
if t.activeLabel==True: return True
return False
[docs]class ObjQLabel(QFrame):
def __init__(self,parent,label,tooltip,path,ser=None,elide=False,searchSlot=None):
QFrame.__init__(self,parent)
lay=QHBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.setSpacing(0)
self.qlabel=QLabel(self)
self.path=path
self.ser=ser
self.minWd=-1
self.setTextToolTip(label,tooltip,elide=elide)
self.qlabel.linkActivated.connect(woo.qt.openUrl)
if searchSlot:
le=QLineEdit(self)
le.setTextMargins(0,0,0,0)
le.setMaximumWidth(50)
le.setMaximumHeight(15)
le.textChanged.connect(searchSlot)
self.lineEdit=le
self.setColor(-1)
lay.addWidget(le)
lay.addStretch(1)
lay.addWidget(self.qlabel)
if searchSlot:
lay.addStretch(2)
#def sizeHint(self): return QSize(self.minWd,20)
#def minimumSizeHint(self): return QSize(self.minWd,20)
[docs] def setColor(self,match):
if match==1: color='PaleGreen'
elif match==0: color='OrangeRed'
else: color='gray'
self.lineEdit.setStyleSheet('QLineEdit{ background: %s; }'%color)
[docs] def setTextToolTip(self,label,tooltip,elide=False):
# ignore UnicodeDecodeError (appears sometimes under Windows), perhaps there are non-ascii characters in docstrings somewhere?
try:
if elide:
# label is the text description; elide it at some fixed width
#self.minWd=100
label=self.fontMetrics().elidedText(label,Qt.ElideRight,int(1.5*self.width()))
else:
pass
#self.minWd=60
self.qlabel.setText(label)
#self.adjustSize()
if tooltip or self.path: self.qlabel.setToolTip(('<b>'+self.path+'</b><br>' if self.path else '')+(tooltip if tooltip else ''))
else: self.qlabel.setToolTip(None)
except UnicodeDecodeError:
self.setToolTip(None)
[docs] def mousePressEvent(self,event):
if event.button()!=Qt.MidButton:
event.ignore(); return
if self.ser and (event.modifiers() & Qt.AltModifier or event.modifiers() & Qt.ControlModifier):
# open this object in new window
se=ObjectEditor(self.ser,parent=None,showType=True,labelIsVar=True,showChecks=False,showUnits=False,objManip=True)
se.show()
return
if self.path==None: return # no path set
# middle button clicked, paste pasteText to clipboard
cb=QApplication.clipboard()
cb.setText(self.path,mode=QClipboard.Clipboard)
cb.setText(self.path,mode=QClipboard.Selection) # X11 global selection buffer
event.accept()
def _unicodeUnit(u): return (u if isinstance(u,str) else str(u,'utf-8'))
[docs]def makeLibraryBrowser(parentmenu,callback,types,libName='Library'):
'''Create menu under *parentmenu* named *libName*. *types* are passed to :obj:`woo.objectlibrary.checkout`. *callback* is called with chosen (name,object) as arguments.'''
import woo.objectlibrary
# root=parentmenu.addMenu('libName')
objs=woo.objectlibrary.checkout(types=types)
menus={():parentmenu.addMenu(libName)}
if objs:
for key,val in objs.items():
# create submenus
for i in range(1,len(key)):
if key[:i] not in menus:
menus[key[:i]]=menus[key[:i-1]].addMenu(key[i-1])
# create menu item
item=menus[key[:-1]].addAction(key[-1])
item.triggered.connect(lambda checked, name=key,obj=val: callback(name,obj))
else: menus[()].setEnabled(False) # disable library menu if there are no objects in there
[docs]class ObjectEditor(QFrame):
"Class displaying and modifying attributes of a woo object, or of unrelated attributes of different objects."
import collections
import logging
# each attribute has one entry associated with itself
# maximum nesting level in the UI, to avoid cycles and freezes
maxNest=6
# maximum length of inline lists
maxListLen=40
[docs] class EntryData:
def __init__(self,obj,name,T,doc,groupNo,trait,containingClass,editor,label):
self.obj,self.name,self.T,self.doc,self.trait,self.groupNo,self.containingClass,self.label=obj,name,T,doc,trait,groupNo,containingClass,label
self.widget=None
self.visible=True
self.hidden=False # this overrides the "visible" attribute
self.gridAndRow=(None,-1)
self.widgets={} #{'label':None,'value':None,'unit':None}
self.editor=editor
[docs] def propertyId(self):
try:
return id(getattr(self.containingClass,self.name))
except AttributeError: return None
[docs] def unitChanged(self,ix0=-1,forceBaseUnit=False):
if not self.trait.unit or len(self.trait.altUnits[0])==0: return
c=self.unitLayout
if not self.trait.multiUnit:
#print self.name,self.containingClass.__name__,self.trait.unit
ix=c.itemAt(0).widget().currentIndex() if not forceBaseUnit else 0
w=self.widgets['value']
if ix==0: # base unit:
w.multiplier=None
# print(self.obj,self.containingClass,self.name)
w.multiplierChanged('')
else:
w.multiplier=self.trait.altUnits[0][ix-1][1]
# print 20*'!',self.trait.name,self.editor
w.multiplierChanged(u'%s × %g = %s'%(_unicodeUnit(self.trait.altUnits[0][ix-1][0]) if ix>0 else '',w.multiplier,_unicodeUnit(self.trait.unit[0])))
else: # multiplier is a tuple applied to each column separately
mult,msg=[],[]
for i in range(len(self.trait.unit)):
if self.trait.altUnits[i]: # not empty, there is a combo box
ix=c.itemAt(i).widget().currentIndex() if not forceBaseUnit else 0
mult.append(None if ix==0 else self.trait.altUnits[i][ix-1][1])
if mult[-1]!=None: msg.append(u'%s × %g = %s'%(_unicodeUnit(self.trait.altUnits[i][ix-1][0]) if ix>0 else '-',mult[-1],_unicodeUnit(self.trait.unit[i])))
else: msg.append('')
w=self.widgets['value']
w.multiplier=tuple(mult)
w.multiplierChanged('<br>'.join(msg))
[docs] def toggleChecked(self,checked):
self.widgets['value'].setEnabled(not checked)
self.widgets['label'].setEnabled(not checked)
[docs] def eval_hideIf(self):
return self.trait.hideIf and eval(self.trait.hideIf,globals(),{'self':self.editor.ser})
[docs] def setVisible(self,visible=None):
if visible==None: visible=self.visible
self.visible=visible
# if visible, the entry can be nevertheless hidden due to hideIf
self.hidden=self.eval_hideIf()
if not self.visible or self.hidden:
for w in self.widgets.values():
if w: w.hide()
else:
for w in self.widgets.values():
if 'unit' in self.widgets and w==self.widgets['unit']: w.setVisible(self.editor.showUnits)
elif 'check' in self.widgets and w==self.widgets['check']: w.setVisible(self.editor.showChecks)
else: w.show()
[docs] class EntryGroupData:
def __init__(self,number,name):
self.number=number
self.name=name
self.expander=None
self.entries=[]
style=QCommonStyle()
self.rightArrow=style.standardIcon(QStyle.SP_ArrowRight)
self.downArrow =style.standardIcon(QStyle.SP_ArrowDown)
[docs] def makeExpander(self,expanded):
self.expander=QPushButton(self.name)
self.expander.setCheckable(True)
self.expander.setChecked(expanded)
self.expander.setStyleSheet('text-align: left; padding: 0pt; padding-left: 6px; ')
self.expander.setFocusPolicy(Qt.StrongFocus)
self.expander.toggled.connect(self.toggleExpander)
self.setExpanderIcon()
return self.expander
[docs] def setExpanderIcon(self): self.expander.setIcon(self.downArrow if self.expander.isChecked() else self.rightArrow)
[docs] def toggleExpander(self):
with WidgetUpdatesDisabled(self.expander.parentWidget()):
self.setExpanderIcon()
for e in self.entries:
e.setVisible(self.expander.isChecked())
def __init__(self,ser,parent=None,ignoredAttrs=set(),objAttrLabelList=None,showType=False,path=None,labelIsVar=True,showChecks=False,showUnits=False,objManip=True,nesting=0):
"Construct window, *ser* is the object we want to show."
# print path
QFrame.__init__(self,parent)
self.ser=ser
self.oneObject=ser
self.setSizePolicy(QSizePolicy.Preferred,QSizePolicy.Expanding)
#where do widgets go in the grid
self.gridCols={'check':0,'label':1,'value':2,'unit':3}
# set path or use label, if active (allows 'label' attributes which don't imply automatic python variables of that name)
self.path=('woo.master.scene.lab.'+ser.label if ser!=None and hasActiveLabel(ser) else path)
self.showType=showType
self.nesting=nesting
self.labelIsVar=labelIsVar # show variable name; if false, docstring is used instead
self.showChecks=showChecks
self.showUnits=showUnits
self.objManip=objManip
self.hot=False
self.entries=[]
self.entryGroups=[]
self.ignoredAttrs=ignoredAttrs
self.hasSer=True
self.searchText=None
self.prevSearchText=None
self.objQLabel=None
if nesting>ObjectEditor.maxNest:
self.mkStub()
return
if objAttrLabelList:
self.hasSer=False
# create entries for given attributes
self.addListObjAttrEntries(objAttrLabelList)
else:
# no objAttrLabelList
if self.ser==None:
log.debug('New None Object')
# show None
lay=QGridLayout(self); lay.setContentsMargins(2,2,2,2); lay.setVerticalSpacing(0)
lab=QLabel('<b>None</b>'); lab.setFrameShape(QFrame.Box); lab.setFrameShadow(QFrame.Sunken); lab.setLineWidth(2); lab.setAlignment(Qt.AlignHCenter);
if self.showType: lay.addWidget(lab,0,0,1,-1)
return # no timers, nothing will change at all
log.debug('New Object of type %s'%ser.__class__.__name__)
# create entries for all attributes of this object
if ser:
self.setWindowTitle(str(ser))
self.addSerAttrEntries()
with WidgetUpdatesDisabled(self): self.mkWidgets()
self.refreshTimer=QTimer(self)
self.refreshTimer.timeout.connect(self.refreshEvent)
self.refreshTimer.start(500)
[docs] def addListObjAttrEntries(self,objAttrLabelList):
for obj,attr,label in objAttrLabelList:
if not isinstance(obj,woo.core.Object): logging.error('%s is not a woo.core.Object (attribtue %s requested)'%(str(obj),attr))
ii=[i for i in obj._getAllTraitsWithClasses() if i[0].name==attr]
if len(ii)!=1:
if not ii: logging.error('Object %s has no attribute trait %s.'%(obj.__class__.__name__,attr))
else: logging.error('Object %s has multiple attribute traits named %s??'%(obj.__class__.__name__,attr))
continue
trait,klass=ii[0]
self.addAttrEntry(obj=obj,trait=trait,klass=klass,label=label)
[docs] def addSerAttrEntries(self):
for trait,klass in self.oneObject._getAllTraitsWithClasses():
self.addAttrEntry(obj=self.ser,trait=trait,klass=klass)
[docs] def addAttrEntry(self,obj,trait,klass,label=None):
'Return attribute entry where *obj* is the parent object, *trait* is the attribute trait, *klass* is the class of the attribute (as python type; if 1-tuple, list of those types). *label* is what will be shown in the UI; if omitted, trait.name will be used by default.'
if trait.hidden or trait.noGui: return
attr=trait.name
try:
val=getattr(obj,attr) # get the value using serattt, as it might be different from what the dictionary provides (e.g. Body.blockedDOFs)
except TypeError: # no conversion possible
logging.error("To-python conversion failed for %s.%s!"%(obj.__class__.__name__,trait.name))
return
t=None
doc=trait.doc
# remove sphinx markup from docstrings
doc=re.sub(':[a-zA-Z0-9_-]+:`([^`]*)`',r'<i>\1</i>',doc)
doc=re.sub('``([^`]+)``',r'<tt>\1</tt>',doc)
if attr in self.ignoredAttrs: return
# this attribute starts a new attribute group
if trait.startGroup: self.entryGroups.append(self.EntryGroupData(number=len(self.entryGroups),name=trait.startGroup))
# determine entry type
if isinstance(val,list):
t=woo.document.guessListTypeFromCxxType(obj.__class__,trait,warnFail=True)
if not t and len(val)==1: t=(val[0].__class__,) # 1-tuple is list of the contained type
#if not t: raise RuntimeError('Unable to guess type of '+str(obj)+'.'+attr)
elif val.__class__ in _attributeGuessedTypeMap: t=_attributeGuessedTypeMap[val.__class__]
elif not isinstance(val,woo.core.Object) and val is not None: t=val.__class__
else: # for Woo objects, determine base class if manipulation is allowed; if not, use current instance type (it can't be changed anyway) or Object (it the value is None)
if self.objManip: t=woo.document.guessInstanceTypeFromCxxType(obj.__class__,trait)
elif val!=None: t=val.__class__
else: t=Object
if len(self.entryGroups)==0: self.entryGroups.append(self.EntryGroupData(number=0,name=None))
groupNo=len(self.entryGroups)-1
#if not match: print 'No attr match for docstring of %s.%s'%(obj.__class__.__name__,attr)
#log.debug('Attr %s is of type %s'%(attr,((t[0].__name__,) if isinstance(t,tuple) else t.__name__)))
self.entries.append(self.EntryData(obj=obj,name=attr,T=t,groupNo=groupNo,doc=doc,trait=trait,containingClass=klass,editor=self,label=label))
[docs] def getDocstring(self,attr=None):
"If attr is *None*, return docstring of the Object itself"
if attr==None:
if self.oneObject.__class__.__doc__!=None:
doc=self.oneObject.__class__.__doc__
else:
logging.error('Class %s __doc__ is None?'%self.ser.__class__.__name__)
return None
else:
ee=[e.doc for e in self.entries if e.name==attr]
if not ee:
logging.error('No entry for attribute named %s?'%(attr))
doc='[no documentation found]'
else: doc=e[0].doc
# doc=re.sub(':[a-zA-Z0-9_-]+:`([^`]*)`',r'<i>\1</i>',doc)
import textwrap
wrapper=textwrap.TextWrapper(replace_whitespace=False)
return wrapper.fill(textwrap.dedent(doc))
[docs] def handleRanges(self,getter,setter,entry):
rg=entry.trait.range
# return editor for given attribute; no-op, unless float with associated range attribute
if entry.T==float and rg and rg.__class__==Vector2:
# getter returns tuple value,range
# setter needs just the value itself
return AttrEditor_FloatRange(self,lambda: (getattr(entry.obj,entry.name),rg),lambda x: setattr(entry.obj,entry.name,x))
elif entry.T==int and rg and rg.__class__==Vector2i:
return AttrEditor_IntRange(self,lambda: (getattr(entry.obj,entry.name),rg),lambda x: setattr(entry.obj,entry.name,x))
# range for sequences has the special meaning of minimum and maximum lenth; handled in SeqEditor, not here
elif isinstance(entry.T,(list,tuple)) and len(entry.T)==1: return None
else:
raise RuntimeError("Invalid range object for "+entry.obj.__class__.__name__+"."+entry.name+": type is "+str(entry.T)+", range is "+str(rg)+" (of type "+rg.__class__.__name__+")")
[docs] def handleChoices(self,getter,setter,entry):
# choice for sequences has the special meaning of descriptions of individual items; handled in SeqEditor, not here
if isinstance(entry.T,(list,tuple)) and len(entry.T)==1: return None
choice=entry.trait.choice
return AttrEditor_Choice(self,lambda: (getattr(entry.obj,entry.name),choice),lambda x: setattr(entry.obj,entry.name,x),isColormap=entry.trait.colormap)
[docs] def handleBits(self,getter,setter,entry):
bits=entry.trait.bits
return AttrEditor_Bits(self,lambda: (getattr(entry.obj,entry.name),bits),lambda x: setattr(entry.obj,entry.name,x))
[docs] def handleRgbColor(self,getter,setter,entry):
return AttrEditor_RgbColor(self,getter,setter)
[docs] def handleFileDir(self,getter,setter,entry):
return AttrEditor_FileDir(self,getter,setter,isDir=entry.trait.dirname,isExisting=entry.trait.existingFilename)
#print 'menu popped up at ',widget.mapToGlobal(position),' (local',position,')'
[docs] def saveObject(self,obj):
# print('saveObject',obj,arg2,arg3,arg4)
assert obj is not None
f=QFileDialog.getSaveFileName(self,'Save object: use .json, .expr, .pickle, .html ...','.')
if isinstance(f,tuple): f=f[0]
print('Saving to:',f)
if not f: return
woo._monkey.io.Object_dump(obj,str(f),format='auto',fallbackFormat='expr')
# XXX: deprecated chunk
#def loadObject(self):
# assert self.path
# f=QFileDialog.getOpenFileName(self,msg,'.')
# if not f: return # no file selected
# try:
# if isObj: obj=entry.T.load(f) # be user friendly if garbage is being loaded
# else: obj=woo._monkey.io.Object_load(None,f)
# raise NotImplementedError('Loading objects to path is not yet implemented.')
# # get parent object (or parent sequence!) and use setattr/setitem to assign the object
# # parent=path.split('.').join('.')
# # eval(path)=obj
# except Exception as e:
# import traceback
# traceback.print_exc()
# showExceptionDialog(self,e)
[docs] def toggleLabelIsVar(self,val=None):
self.labelIsVar=(not self.labelIsVar if val==None else val)
for entry in self.entries:
entry.widgets['label'].setTextToolTip(*self.getAttrLabelToolTip(entry),elide=not self.labelIsVar)
if entry.widget.__class__==ObjectEditor:
entry.widget.toggleLabelIsVar(self.labelIsVar)
[docs] def toggleShowChecks(self,val=None):
with WidgetUpdatesDisabled(self):
self.showChecks=(not self.showChecks if val==None else val)
#for g in self.entryGroups: g.showChecks=self.showChecks # propagate down
for entry in self.entries:
if entry.visible and not entry.hidden: entry.widgets['check'].setVisible(self.showChecks)
if not entry.trait.readonly:
if 'value' in entry.widgets: entry.widgets['value'].setEnabled(True)
entry.widgets['label'].setEnabled(True)
if entry.widget.__class__==ObjectEditor:
entry.widget.toggleShowChecks(self.showChecks)
[docs] def toggleShowUnits(self,val=None):
with WidgetUpdatesDisabled(self):
self.showUnits=(not self.showUnits if val==None else val)
#print self.showUnits
for entry in self.entries:
entry.setVisible(None)
entry.unitChanged(forceBaseUnit=(not self.showUnits))
if entry.widget.__class__==ObjectEditor:
entry.widget.toggleShowUnits(self.showUnits)
[docs] def setFromLib(self,entry,name,obj):
# setting library object
if isinstance(obj,woo.core.Object): obj=obj.deepcopy() # prevent lib object modification
setattr(entry.obj,entry.name,obj)
[docs] def doObjManip(self,action,entry,isNone,isObj):
'Handle menu action from objManipLabelMenu'
# FIXME: this is an ugly hack of using woo._monkey.io.Object_{dump,load} directly!!!
import woo, woo.core, woo._monkey.io
#print 'Manipulating Object',entry.obj.__class__.__name__+'.'+entry.name
if action=='del':
assert isObj
setattr(entry.obj,entry.name,None)
elif action=='new' or action=='replace':
assert isObj
types=woo.system.childClasses(entry.T,includeBase=True)
if(len(types)==1): setattr(entry.obj,entry.name,entry.T())
else:
d=NewObjectDialog(self,entry.T)
if not d.exec_(): return # cancelled
setattr(entry.obj,entry.name,d.result())
elif action=='save':
assert not isNone
obj=getattr(entry.obj,entry.name)
f=QFileDialog.getSaveFileName(self,'Save object: use .json, .expr, .pickle, .html ...','.')
if isinstance(f,tuple): f=f[0]
if not f: return
woo._monkey.io.Object_dump(obj,str(f),format='auto',fallbackFormat='expr')
elif action=='load':
msg='Load a %s'%entry.T.__name__ if isObj else 'Load'
f=QFileDialog.getOpenFileName(self,msg,'.')
if isinstance(f,tuple): f=f[0]
if not f: return # no file selected
try:
if isObj: obj=entry.T.load(f) # be user friendly if garbage is being loaded
else: obj=woo._monkey.io.Object_load(None,f)
setattr(entry.obj,entry.name,obj)
except Exception as e:
import traceback
traceback.print_exc()
showExceptionDialog(self,e)
elif action=='default':
if isObj:
val=entry.trait.ini.deepcopy() if entry.trait.ini!=None else None # don't call deepcopy on none, it fails :)
else:
import copy
val=copy.deepcopy(entry.trait.ini)
setattr(entry.obj,entry.name,val)
else: raise RuntimError('Unknown action %s for object manipulation context menu!'%action)
self.refreshEvent()
[docs] def mkStub(self):
lay=QGridLayout(self)
lay.setContentsMargins(2,2,2,2)
lay.setVerticalSpacing(0)
self.setLayout(lay)
lay.addWidget(QLabel("GUI nesting > %d"%(ObjectEditor.maxNest)),1,1)
[docs] def search(self,text):
self.prevSearchText=self.searchText
self.searchText=str(text)
self.refreshEvent()
[docs] def refreshEvent(self):
maxLabelWd=0.
# restore group visibility, since there is no search text anymore
if not self.searchText and self.prevSearchText:
for g in self.entryGroups:
g.toggleExpander() # queries the group expander whether it is toggled or not
self.prevSearchText=''
self.objQLabel.setColor(-1)
searchMatches=0
for e in self.entries:
if self.hasSer: assert self.ser==e.obj
if e.widget and not e.widget.hot:
maxLabelWd=max(maxLabelWd,e.widgets['label'].width())
# if there is a new instance of Object, we need to make new widget and replace the old one completely
if type(e.widget)==ObjectEditor and e.widget.hasSer and e.widget.ser!=getattr(e.obj,e.name):
# print 'New ObjectEditor (%s): '%e.name,e.widget.ser,'->',getattr(e.obj,e.name)
# print e.widget.ser._cxxAddr,getattr(e.obj,e.name)._cxxAddr,getattr(e.obj,e.name)
assert e.widget.ser==None or getattr(e.obj,e.name)==None or e.widget.ser._cxxAddr!=getattr(e.obj,e.name)._cxxAddr or (isinstance(getattr(e.obj,e.name),woo.pyderived.PyWooObject) and id(getattr(e.obj,e.name))!=id(e.widget.ser))
e.widget.hide()
e.widget=e.widgets['value']=self.mkWidget(e)
grid,row=e.gridAndRow
colSpan=(2 if 'unit' not in e.widgets else 1)
grid.addWidget(e.widget,row,self.gridCols['value'],1,colSpan)
## propagate the search to nested widgets; disabled for now, since the widget itself will be very likely disabled anyway, as the search result does not propagate upwards to us
# if type(e.widget)==ObjectEditor: e.searchText=self.searchText
if not self.searchText:
# visibility might change if hideIf is defined
if e.trait.hideIf or not e.visible: e.setVisible(None)
else:
if re.search(self.searchText,e.name,re.IGNORECASE):
searchMatches+=1
e.setVisible(True)
else: e.setVisible(False)
e.widget.refresh()
if self.searchText: self.objQLabel.setColor(1 if searchMatches else 0)
#self.layout().setColumnMinimumWidth(self.gridCols['label'],maxLabelWd)
if self.labelIsVar: self.layout().setColumnStretch(self.gridCols['label'],-1)
else: self.layout().setColumnStretch(self.gridCols['label'],2)
[docs] def refresh(self): pass
[docs]def makeObjectLabel(ser,href=False,addr=True,boldHref=True,num=-1,count=-1):
ret=u''
if num>=0:
if count>=0: ret+=u'%d/%d. '%(num,count)
else: ret+=u'%d. '%num
if href: ret+=(u' <b>' if boldHref else u' ')+woo.document.makeObjectHref(ser)+(u'</b> ' if boldHref else u' ')
else: ret+=ser.__class__.__name__+' '
if hasActiveLabel(ser): ret+=u' “'+str(ser.label)+u'”'
# do not show address if there is a label already
elif addr and ser!=None:
ret+='0x%x'%ser._cxxAddr
return ret
[docs]class SeqObjectComboBox(QFrame):
maxShowLen=500
[docs] def getItemType(self): return self.trait.pyType[0]
def __init__(self,parent,getter,setter,T,trait,path=None,shrink=False,nesting=0):
QFrame.__init__(self,parent)
self.getter,self.setter,T,self.trait,self.path,self.shrink=getter,setter,T,trait,path,shrink
#if not hasattr(self.trait,'pyType'): self.trait.pyType=(T,) # this is for compat with C++ (?)
if self.trait.pyType is None: self.trait.pyType=(T,)
self.hot=None # API compat with ObjectEditor
ll=len(self.getter())
if ll>self.maxShowLen:
lab=QLabel('<i>Not showing sequence with %d (>%d) items.</i>'%(ll,self.maxShowLen))
lay=QVBoxLayout(self)
lay.addWidget(lab)
self.setLayout(lay)
return
self.layout=QVBoxLayout(self)
self.nesting=nesting
topLineFrame=QFrame(self)
topLineLayout=QHBoxLayout(topLineFrame);
topLineFrame.setLayout(topLineLayout)
for l in self.layout, topLineLayout: l.setSpacing(0); l.setContentsMargins(0,0,0,0)
labels=(u'+',u'−',u'↑',u'↓')
tooltips=('Add','Delete','Move up','Move down')
buttons=(self.newButton,self.killButton,self.upButton,self.downButton)=[QPushButton(label,self) for label in labels]
buttonSlots=(None,self.killSlot,self.upSlot,self.downSlot) # same order as buttons
for i,b in enumerate(buttons): b.setStyleSheet('QPushButton { font-size: 15pt; font-weight: bold; }'); b.setFixedWidth(25); b.setFixedHeight(30); b.setToolTip(tooltips[i])
self.combo=QComboBox(self)
self.combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
for w in buttons[:2]+[self.combo,]+buttons[2:]: topLineLayout.addWidget(w)
self.layout.addWidget(topLineFrame) # nested layout
self.scroll=QScrollArea(self); self.scroll.setWidgetResizable(True)
self.layout.addWidget(self.scroll)
self.seqEdit=None # currently edited serializable
self.setLayout(self.layout)
self.setFrameShape(QFrame.Box); self.setFrameShadow(QFrame.Raised); self.setLineWidth(1)
self.newDialog=None # is set when new dialog is created, and destroyed when it returns
self.comboItemCount=0 # we use some inactive items with ranges instead of objects, so keep track of valid items separately
# create menu for the "new" button
newMenu=QMenu();
newMenu.addAction(u'☘ New',self.newSlot)
self.cloneAction=newMenu.addAction(u'≡ Clone',self.cloneSlot)
newMenu.addAction(u'↥ Load',self.loadSlot)
lib=makeLibraryBrowser(newMenu,lambda name,obj: self.libLoadSlot(name,obj),self.getItemType(),u'⇈ Library')
self.newButton.setMenu(newMenu)
# signals
for b,slot in zip(buttons[1:],buttonSlots[1:]): b.clicked.connect(slot)
self.combo.currentIndexChanged.connect(self.comboIndexSlot)
self.refreshEvent()
# periodic refresh
self.refreshTimer=QTimer(self)
self.refreshTimer.timeout.connect(self.refreshEvent)
self.refreshTimer.start(500) # 1s should be enough
#print 'SeqObject path is',self.path
[docs] def comboIndexSlot(self,ix): # different seq item selected
currSeq=self.getter();
if ix>=len(currSeq): # this can happen with fake items which get activated when real item is deleted
self.combo.setCurrentIndex(len(currSeq)-1)
return
if len(currSeq)==0: ix=-1
log.debug('%s comboIndexSlot len=%d, ix=%d'%(self.getItemType().__name__,len(currSeq),ix))
self.downButton.setEnabled(ix<len(currSeq)-1)
self.upButton.setEnabled(ix>0)
self.combo.setEnabled(ix>=0)
if ix>=0:
ser=currSeq[ix]
self.seqEdit=ObjectEditor(ser,parent=self,showType=seqObjectShowType,path=(self.path+'['+str(ix)+']') if self.path else None,nesting=self.nesting+1)
self.scroll.setWidget(self.seqEdit)
if self.shrink:
self.sizeHint=lambda: QSize(100,1000)
self.scroll.sizeHint=lambda: QSize(100,1000)
self.sizePolicy().setVerticalPolicy(QSizePolicy.Expanding)
self.scroll.sizePolicy().setVerticalPolicy(QSizePolicy.Expanding)
self.setMinimumHeight(min(300,self.seqEdit.height()+self.combo.height()+10))
self.setMaximumHeight(100000)
self.scroll.setMaximumHeight(100000)
else:
self.scroll.setWidget(QFrame())
if self.shrink:
self.setMaximumHeight(self.combo.height()+10);
self.scroll.setMaximumHeight(0)
# XXX: deprecated chunk
# def serLabel(self,ser,i=-1):
# return ('' if i<0 else str(i)+'. ')+str(ser)[1:-1].replace('instance at ','')
[docs] def refreshEvent(self,forceIx=-1):
try:
currSeq=self.getter()
comboEnabled=self.combo.isEnabled()
if comboEnabled and len(currSeq)==0: self.comboIndexSlot(-1) # force refresh, otherwise would not happen from the initially empty state
ix,cnt=self.combo.currentIndex(),self.comboItemCount # self.combo.count()
# serializable currently being edited (which can be absent) or the one of which index is forced
ser=(self.seqEdit.ser if self.seqEdit else None) if forceIx<0 else currSeq[forceIx]
if comboEnabled and len(currSeq)==cnt and (ix<0 or (ix<len(currSeq) and ser==currSeq[ix])): return
if not comboEnabled and len(currSeq)==0: return
log.debug(self.getItemType().__name__+' rebuilding list from scratch')
self.combo.clear()
if len(currSeq)>0:
prevIx=-1
for i,s in enumerate(currSeq):
extra=[]
if self.trait.choice and i<len(self.trait.choice): extra.append(self.trait.choice[i])
if self.trait.range and i>=self.trait.range[0]: extra.append('optional')
extra=' (%s)'%('; '.join(extra)) if extra else ''
self.combo.addItem(makeObjectLabel(s,num=i,count=len(currSeq),addr=False)+extra)
if s==ser: prevIx=i
self.comboItemCount=self.combo.count()
# add extra (inactive, unselectable) items
# when using range and descriptions
if self.trait.range or self.trait.choice:
for i in range(len(currSeq),max(self.trait.range[1],len(self.trait.choice))):
extra=[]
if self.trait.choice and i<len(self.trait.choice): extra.append(self.trait.choice[i])
if self.trait.range and i>=self.trait.range[0]: extra.append('optional')
extra=' (%s)'%('; '.join(extra)) if extra else ''
self.combo.addItem(u'%d. −'%i+extra)
# use the hack from http://theworldwideinternet.blogspot.cz/2011/01/disabling-qcombobox-items.html to deactivate items
self.combo.setItemData(i,0,Qt.UserRole-1)
if forceIx>=0: newIx=forceIx # force the index (used from newSlot to make the new element active)
elif prevIx>=0: newIx=prevIx # if found what was active before, use it
elif ix>=0: newIx=ix # otherwise use the previous index (e.g. after deletion)
else: newIx=0 # fallback to 0
log.debug('%s setting index %d'%(self.getItemType().__name__,newIx))
self.combo.setCurrentIndex(newIx)
else:
log.debug('%s EMPTY, setting index 0'%(self.getItemType().__name__))
self.combo.setCurrentIndex(-1)
enableKill=(not self.trait.noGuiResize and len(currSeq)>(self.trait.range[0] if self.trait.range else 0))
enableNew=(not self.trait.noGuiResize and (not self.trait.range or len(currSeq)<self.trait.range[1]))
enableClone=enableNew and len(currSeq)>0
self.killButton.setEnabled(enableKill)
self.newButton.setEnabled(enableNew)
self.cloneAction.setEnabled(enableClone)
except RuntimeError as e:
print('Error refreshing sequence (path %s), ignored.'%self.path)
[docs] def newSlot(self):
# print 'newSlot called'
dialog=NewObjectDialog(self,self.getItemType())
if not dialog.exec_(): return # cancelled
ser=dialog.result()
ix=self.combo.currentIndex()
currSeq=list(self.getter()); currSeq.insert(ix,ser); self.setter(currSeq)
log.warning('%s new item created at index %d'%(self.getItemType().__name__,ix))
self.refreshEvent(forceIx=ix)
[docs] def loadSlot(self):
f=QFileDialog.getOpenFileName(self,msg,'.')
if isinstance(f,tuple): f=f[0]
if not f: return # no file selected
T=self.getItemType()
try:
obj=T.load(f) # be user friendly if garbage is being loaded
# else: obj=woo._monkey.io.Object_load(None,f)
currSeq=list(self.getter()); currSeq.insert(ix+1,obj); self.setter(currSeq)
except Exception as e:
import traceback
traceback.print_exc()
showExceptionDialog(self,e)
[docs] def libLoadSlot(self,name,obj):
ix=self.combo.currentIndex()
currSeq=list(self.getter()); currSeq.insert(ix+1,obj); self.setter(currSeq)
self.refreshEvent(forceIx=ix+1)
[docs] def cloneSlot(self):
ix=self.combo.currentIndex()
currSeq=list(self.getter()); currSeq.insert(ix+1,currSeq[ix].deepcopy()); self.setter(currSeq)
self.refreshEvent(forceIx=ix+1)
[docs] def killSlot(self):
ix=self.combo.currentIndex()
currSeq=self.getter(); del currSeq[ix]; self.setter(currSeq)
self.refreshEvent()
[docs] def upSlot(self):
i=self.combo.currentIndex()
assert(i>0)
currSeq=self.getter();
prev,curr=currSeq[i-1:i+1]; currSeq[i-1],currSeq[i]=curr,prev; self.setter(currSeq)
self.refreshEvent(forceIx=i-1)
[docs] def downSlot(self):
i=self.combo.currentIndex()
currSeq=self.getter(); assert(i<len(currSeq)-1);
curr,nxt=currSeq[i:i+2]; currSeq[i],currSeq[i+1]=nxt,curr; self.setter(currSeq)
self.refreshEvent(forceIx=i+1)
[docs] def refresh(self): pass # API compat with ObjectEditor
SeqObject=SeqObjectComboBox
[docs]class NewFundamentalDialog(QDialog):
def __init__(self,parent,attrName,typeObj,typeStr):
QDialog.__init__(self,parent)
self.setWindowTitle('%s (type %s)'%(attrName,typeStr))
self.layout=QVBoxLayout(self)
self.scroll=QScrollArea(self)
self.scroll.setWidgetResizable(True)
self.buttons=QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel);
self.buttons.accepted.connect(self.accept)
self.buttons.rejected.connect(self.reject)
self.layout.addWidget(self.scroll)
self.layout.addWidget(self.buttons)
self.setWindowModality(Qt.WindowModal)
class FakeObjClass: pass
self.fakeObj=FakeObjClass()
self.attrName=attrName
Klass=_fundamentalEditorMap.get(typeObj,None)
initValue=_fundamentalInitValues.get(typeObj,typeObj())
setattr(self.fakeObj,attrName,initValue)
if Klass:
self.widget=Klass(None,self.fakeObj,attrName)
self.scroll.setWidget(self.widget)
self.scroll.show()
self.widget.refresh()
else: raise RuntimeError("Unable to construct new dialog for type %s"%(typeStr))
[docs] def result(self):
self.widget.update()
return getattr(self.fakeObj,self.attrName)
[docs]class NewObjectDialog(QDialog):
def __init__(self,parent,baseClass,includeBase=True):
import woo.system
QDialog.__init__(self,parent)
self.setWindowTitle('Create new object of type %s'%baseClass.__name__)
self.layout=QVBoxLayout(self)
self.combo=QComboBox(self)
self.classes=list(woo.system.childClasses(baseClass,includeBase=False)); self.classes.sort()
if includeBase:
self.classes=[baseClass,None]+self.classes # None is for the separator, so that indices are the same
self.combo.addItem(baseClass.__name__)
self.combo.insertSeparator(1000)
self.combo.addItems([c.__name__ for c in self.classes[(2 if includeBase else 0):]])
self.combo.currentIndexChanged.connect(self.comboSlot)
self.scroll=QScrollArea(self)
self.scroll.setWidgetResizable(True)
self.buttons=QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel);
self.buttons.accepted.connect(self.accept)
self.buttons.rejected.connect(self.reject)
self.layout.addWidget(self.combo)
self.layout.addWidget(self.scroll)
self.layout.addWidget(self.buttons)
self.ser=None
self.combo.setCurrentIndex(0); self.comboSlot(0)
self.setWindowModality(Qt.WindowModal)
[docs] def comboSlot(self,index):
self.ser=self.classes[index]() # instantiate the class
self.scroll.setWidget(ObjectEditor(self.ser,self.scroll,showType=True))
self.scroll.show()
[docs] def result(self): return self.ser
[docs] def sizeHint(self): return QSize(180,400)
[docs]class SeqFundamentalEditor(QFrame):
# maximum length of the sequence; show ellipsis in the middle if longer
# this should avoid freezes in the UI due to unexpectedly long data
maxShowLen=60
#def focusInEvent(self,event):
# log.warning('SeqFundamentalEditor: focusInEvent')
def __init__(self,parent,getter,setter,itemType):
QFrame.__init__(self,parent)
self.getter,self.setter,self.itemType=getter,setter,itemType
self.layout=QVBoxLayout()
topLineFrame=QFrame(self); topLineLayout=QHBoxLayout(topLineFrame)
self.form=QFormLayout()
self.form.setContentsMargins(0,0,0,0)
self.form.setVerticalSpacing(0)
self.form.setLabelAlignment(Qt.AlignLeft)
self.formFrame=QFrame(self); self.formFrame.setLayout(self.form)
self.layout.addWidget(self.formFrame)
self.setLayout(self.layout)
self.emptySeq=False
self.convSpec=None # cache value of unit conversions, for rows being added
self.multiplier=None
# ObjectEditor API compat
self.hot=False
self.rebuild()
# periodic refresh
self.refreshTimer=QTimer(self)
self.refreshTimer.timeout.connect(self.refreshEvent)
self.refreshTimer.start(500) # 1s should be enough
# this does not work...?!
#menu.destroyed.connect(lambda: field.setStyleSheet('QWidget { background : none }'))
[docs] def localPositionToIndex(self,pos,isGlobal=False):
gp=self.mapToGlobal(pos) if not isGlobal else pos
for row in range(self.form.rowCount()):
w,i=self.form.itemAt(row,QFormLayout.FieldRole),self.form.itemAt(row,QFormLayout.LabelRole)
for wi in w.widget(),i.widget():
x0,y0,x1,y1=wi.geometry().getCoords(); globG=QRect(self.mapToGlobal(QPoint(x0,y0)),self.mapToGlobal(QPoint(x1,y1)))
if globG.contains(gp):
return self.mapList2Seq(row)
return -1
[docs] def keyFocusIndex(self):
w=QApplication.focusWidget()
globPos=w.mapToGlobal(QPoint(.5*w.width(),.5*w.height()))
return self.localPositionToIndex(globPos,isGlobal=True)
[docs] def keyPressEvent(self,ev):
isModified=ev.modifiers()&Qt.AltModifier
if not isModified:
if not QApplication.focusWidget(): return
if ev.key()==Qt.Key_Return: QApplication.focusWidget().focusNextPrevChild(True)
if ev.key()==Qt.Key_Up: QApplication.focusWidget().focusNextPrevChild(False)
if ev.key()==Qt.Key_Down: QApplication.focusWidget().focusNextPrevChild(True)
return
if ev.key()==Qt.Key_Up:
self.upSlot(self.keyFocusIndex()); ev.accept()
elif ev.key()==Qt.Key_Down: self.downSlot(self.keyFocusIndex())
elif ev.key()==Qt.Key_Delete or ev.key()==Qt.Key_Minus:
self.killSlot(self.keyFocusIndex())
ev.accept()
elif ev.key()==Qt.Key_Backspace:
self.killSlot(self.keyFocusIndex()-1)
ev.accept()
elif ev.key()==Qt.Key_Enter or ev.key()==Qt.Key_Return or ev.key()==Qt.Key_Plus:
# insert after the current row
self.newSlot(self.keyFocusIndex())
ev.accept();
[docs] def newSlot(self,i): # i is the index AFTER which the new row is inserted
seq=self.getter();
seq.insert(i+1,_fundamentalInitValues.get(self.itemType,self.itemType()))
try: self.setter(seq)
except BaseException as e: log.warning(f'Error setting new value {seq}: {e}')
self.rebuild()
if len(seq)>0:
item=self.form.itemAt(i+1,QFormLayout.FieldRole)
if item: item.widget().setFocus()
[docs] def killSlot(self,i):
seq=self.getter();
if i<0: return
assert(i<len(seq)); del seq[i]; self.setter(seq)
self.refreshEvent()
if len(seq)>0: self.form.itemAt(max(0,i if i<len(seq)-1 else i-1),QFormLayout.FieldRole).widget().setFocus()
[docs] def upSlot(self,i):
if i==0: return
seq=self.getter(); assert(i<len(seq));
prev,curr=seq[i-1:i+1];
seq[i-1],seq[i]=curr,prev;
self.setter(seq)
self.refreshEvent(forceIx=i-1)
[docs] def downSlot(self,i):
seq=self.getter();
if i==len(seq)-1: return
assert(i<len(seq)-1);
curr,nxt=seq[i:i+2]; seq[i],seq[i+1]=nxt,curr; self.setter(seq)
self.refreshEvent(forceIx=i+1)
[docs] def fromClipSlot(self,i):
try:
importables={Vector3:(float,3),Vector2:(float,2),Vector6:(float,6),Vector2i:(int,2),Vector2i:(int,3),Vector6i:(int,6),Matrix3:(float,9),float:(float,1),int:(int,1)}
if self.itemType not in importables: raise NotImplementedError("Type %s is not text-importable"%(self.itemType.__name__))
elementType,lineLen=importables[self.itemType]
print('Will import lines with %d item(s) of type %s'%(lineLen,elementType.__name__))
# get txt from clipboard
cb=QApplication.clipboard()
txt=str(cb.text())
print('Got %d lines from clipboard:\n'%(len(txt.split('\n'))),txt)
# handle unit conversions here
if self.multiplier:
if isinstance(self.multiplier,tuple): mult=[1./m for m in self.multiplier]
else: mult=[1./self.multiplier]*lineLen
print('Input will be scaled by',mult,' to match selected units')
else: mult=[1.]*lineLen
#
seq=[]
for i,ll in enumerate(txt.split('\n')):
l=ll.split()
if len(l)==0: continue # skip empty lines
if len(l)!=lineLen: raise ValueError("Line %d has %d elements (should have %d)"%(i,len(l),lineLen))
print('Line tuple is',tuple([val for val in l]))
lineItems=[elementType(eval(val))*mult[i] for i,val in enumerate(l)]
if lineLen>1: seq.append(tuple(lineItems))
else: seq.append(lineItems[0]) # sequences of floats/ints are imported as sequence, not as sequence of tuples
print('Imported sequence',seq)
except Exception as e:
import traceback
traceback.print_exc()
showExceptionDialog(self,e)
return
self.setter(seq)
self.refreshEvent(forceIx=0)
[docs] def mapList2Seq(self,l):
'return sequence (object) index from given list (widget) index.'
if not self.split: return l
if l>self.split[0]: return l+self.split[1]-1 # 1 for the separator widget
[docs] def mapSeq2List(self,s):
'return list (widget) index from given sequence (object) index. Return -1 if the item is not shown in the UI due to splitting.'
if not self.split: return s
if s<self.split[0]: return s
if s<self.split[1]: return -1
if s>=self.split[1]: return s+(self.split[1]-self.split[0])+1 # 1 for the separator widget
[docs] def recomputeSplit(self,currLen):
l=currLen; ml=SeqFundamentalEditor.maxShowLen
if l>ml:
self.split=(ml/2,l-ml/2)
self.splitLen=l
else:
self.split=None
self.splitLen=0
[docs] def renumber(self,currLen):
if not self.split: return
self.recomputeSplit()
for row in range(self.form.rowCount()):
l=self.form.itemAt(row,QFormLayout.LabelRole)
l.widget().setText('%d. '%self.mapList2Seq(row))
self.splitLen=currLen
[docs] def rebuild(self):
log.debug('rebuild...')
currSeq=self.getter()
# clear everything
for row in range(self.form.rowCount()):
log.debug(f'counts {self.form.rowCount()} {self.form.count()}')
for wi in self.form.itemAt(row,QFormLayout.FieldRole),self.form.itemAt(row,QFormLayout.LabelRole):
self.form.removeItem(wi)
if not wi or not wi.widget(): continue
wi.focusOutEvent=None
log.trace('deleting widget {wi.widget()}')
# for some reason, deleting does not make the thing disappear visually; hiding does, however
# FIXME: this might be the reason why ever-resizing sequences eat up RAM!!
widget=wi.widget(); widget.hide(); del widget
log.trace(f'counts after {self.form.rowCount()} {self.form.count()}')
while self.form.rowCount()>0: self.form.removeRow(0)
#for row in range(self.form.rowCount()): row.
log.debug('cleared')
# add everything
Klass=_fundamentalEditorMap.get(self.itemType,None)
if not Klass:
errMsg=QTextEdit(self)
errMsg.setReadOnly(True); errMsg.setText("Sorry, editing sequences of %s's is not (yet?) implemented."%(self.itemType.__name__))
self.form.insertRow(0,'<b>Error</b>',errMsg)
return
class ItemGetter():
def __init__(self,getter,index): self.getter,self.index=getter,index
def __call__(self):
try:
return self.getter()[self.index]
except IndexError: return None
class ItemSetter():
def __init__(self,getter,setter,index): self.getter,self.setter,self.index=getter,setter,index
def __call__(self,val):
try:
seq=self.getter(); seq[self.index]=val; self.setter(seq)
except IndexError: pass
self.recomputeSplit(len(currSeq))
for i,item in enumerate(currSeq):
if self.split and self.split[0]==i:
# show separator
l=QLabel(u'<b>⋮</b>'); l.font().setPointSize(32)
self.form.insertRow(i,u'<b>⋮</b>',l)
continue
li=self.mapSeq2List(i)
# print 'item #%d mapped to field %d'%(i,li)
if li<0: continue # in the split, do not show anything
widget=Klass(self,ItemGetter(self.getter,i),ItemSetter(self.getter,self.setter,i))
# self.hijackWidgetEvents(widget)
self.form.insertRow(i,'%d. '%li,widget)
log.debug('added item %d %s'%(i,str(widget)))
# set units correctly
if self.multiplier:
widget.multiplier=self.multiplier
widget.multiplierChanged(self.convSpec)
if len(currSeq)==0:
self.emptySeq=True
self.form.insertRow(0,'<i>empty</i>',QLabel('<i>(right-click for menu)</i>'))
else: self.emptySeq=False
log.debug('rebuilt, will refresh now')
self.refreshEvent(dontRebuild=True) # avoid infinite recursion it the length would change meanwhile
[docs] def refreshEvent(self,dontRebuild=False,forceIx=-1):
# log.warning('refreshEvent...')
currSeq=self.getter()
#print 'bbb',len(currSeq)
if not self.split and (len(currSeq)!=(self.form.rowCount() if not self.emptySeq else 0)):
if dontRebuild: return # length changed behind our back, just pretend nothing happened and update next time instead
log.warning(f'Sequence length not matching (data: {len(currSeq)}, form: {self.form.rowCount()}), rebuilding')
self.rebuild()
currSeq=self.getter()
elif self.split and len(currSeq)!=self.splitLen: self.renumber(len(currSeq))
for i in range(len(currSeq)):
item=self.form.itemAt(i,QFormLayout.FieldRole)
if not item: continue # some error condition, oh well
widget=item.widget()
log.trace('got item #%d %s'%(i,str(widget)))
if hasattr(widget,'hot') and not widget.hot: # it can be a QLabel as well
widget.refresh()
if forceIx>=0 and forceIx==i: widget.setFocus()
[docs] def refresh(self): pass # ObjectEditor API
# propagate multiplier change to children
[docs] def multiplierChanged(self,convSpec):
self.convSpec=convSpec # cache value should new rows be created
self.setToolTip(convSpec)
for row in range(self.form.count()//2):
w=self.form.itemAt(row,QFormLayout.FieldRole).widget()
if isinstance(w,QLabel): continue # label that the sequence is empty
w.multiplier=self.multiplier
w.multiplierChanged(convSpec)