Source code for woo.qt.ObjectEditor

# 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
[docs]class WidgetUpdatesDisabled(): 'Context manager/decorator for disabling updates of qt widgets temporarily' def __init__(self,widget): self.widget=widget def __enter__(self): self.widget.setUpdatesEnabled(False) def __exit__(self,eType,eValue,eTrace): self.widget.setUpdatesEnabled(True)
# 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 update(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))
[docs]class Se3FakeType: pass
_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)
[docs] def mkWidget(self,entry): if not entry.T: #print 'return None for %s.%s'%(entry.obj.__class__.__name__,entry.name) return None # single fundamental object widget=None # default getter and setter getter,setter=lambda: getattr(entry.obj,entry.name), lambda x: setattr(entry.obj,entry.name,x) # try to find specific widget first based on traits # these functions may return none, indicating that it won't be handled specially if entry.trait.range: widget=self.handleRanges(getter,setter,entry) elif entry.trait.choice: widget=self.handleChoices(getter,setter,entry) elif entry.trait.bits: widget=self.handleBits(getter,setter,entry) elif entry.trait.rgbColor: widget=self.handleRgbColor(getter,setter,entry) elif entry.trait.filename or entry.trait.existingFilename or entry.trait.dirname: widget=self.handleFileDir(getter,setter,entry) # no specific widget found, try one for fundamental types else: Klass=_fundamentalEditorMap.get(entry.T,None) if Klass: widget=Klass(self,getter=getter,setter=setter) if widget: widget.setFocusPolicy(Qt.StrongFocus) if entry.trait.readonly: widget.setEnabled(False) return widget # sequences if entry.T.__class__==tuple: assert len(entry.T)==1 # we don't handle tuples of other lengths # sequence of serializables T=entry.T[0] if (issubclass(T,Object) or T==Object): # HACK!!! # per-instance traits for py-derived objects if hasattr(entry.obj,'_instanceTraits') and entry.trait.name in entry.obj._instanceTraits: entry.trait=entry.obj._instanceTraits[entry.trait.name] widget=SeqObject(self,getter,setter,T=T,trait=entry.trait,path=(self.path+'.'+entry.name if self.path else None),shrink=True,nesting=self.nesting+1) return widget if (T in _fundamentalEditorMap): widget=SeqFundamentalEditor(self,getter,setter,T) return widget print('No widget for (%s,) in %s.%s'%(T.__name__,entry.obj.__class__.__name__,entry.name)) return None # a woo.Object if issubclass(entry.T,Object) or entry.T==Object: obj=getattr(entry.obj,entry.name) # should handle the case of obj==None as well widget=ObjectEditor(getattr(entry.obj,entry.name),parent=self,showType=self.showType,path=(self.path+'.'+entry.name if self.path else None),labelIsVar=self.labelIsVar,showChecks=self.showChecks,showUnits=self.showUnits,objManip=self.objManip,nesting=self.nesting+1) widget.setFrameShape(QFrame.Box); widget.setFrameShadow(QFrame.Raised); widget.setLineWidth(1) return widget print('No widget for %s in %s.%s'%(entry.T.__name__,entry.obj.__class__.__name__,entry.name)) return None
[docs] def serQLabelMenu(self,widget,position): menu=QMenu(self) toggleLabelIsVar=menu.addAction('Variables') toggleLabelIsVar.setCheckable(True); toggleLabelIsVar.setChecked(self.labelIsVar) toggleLabelIsVar.triggered.connect(lambda: self.toggleLabelIsVar(None)) toggleShowChecks=menu.addAction(u'Checks') toggleShowChecks.setCheckable(True); toggleShowChecks.setChecked(self.showChecks) toggleShowChecks.triggered.connect(lambda: self.toggleShowChecks(None)) toggleShowUnits=menu.addAction('Units') toggleShowUnits.setCheckable(True); toggleShowUnits.setChecked(self.showUnits) toggleShowUnits.triggered.connect(lambda: self.toggleShowUnits(None)) if self.hasSer and self.ser is not None: actionSave=menu.addAction(u'⛁ Save') actionSave.triggered.connect(lambda: self.saveObject(self.ser)) # actionSave.triggered.connect(lambda obj=self.ser: self.saveObject(obj)) #if self.path is not None: # actionLoad=menu.addAction(u'↥ Load') # actionLoad.trigger.connect(lambda: self.loadObject) menu.popup(self.mapToGlobal(position))
#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 getAttrLabelToolTip(self,entry): try: ini=str(entry.trait.ini) if (entry.trait.ini and not isinstance(entry.trait.ini,Object)) else '' except TypeError: # boost::python won't convert weak_ptr, catch it here ini='' toolTip=entry.containingClass.__name__+u'.<b><i>'+entry.name+u'</i></b><br>'+_ensureUnicode(entry.doc)+u'<br><small>default: %s</small>'%ini if self.labelIsVar: return woo.document.makeObjectHref(entry.obj,entry.name,text=entry.label),toolTip return entry.doc,toolTip
[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 objManipLabelMenu(self,entry,pos): 'context menu for creating/deleting/loading/saving woo.core.Object from within the editor' menu=QMenu(self) isNone=(getattr(entry.obj,entry.name)==None) # isinstance is false for None, but None is always (?) missing woo.core.Object anyway isObj=isinstance(getattr(entry.obj,entry.name),woo.core.Object) or isNone default=menu.addAction(u'↺ Default') default.triggered.connect(lambda: self.doObjManip('default',entry,isNone,isObj)) if isObj: if isNone: newDel=menu.addAction(u'☘ New') newDel.triggered.connect(lambda: self.doObjManip('new',entry,isNone,isObj)) else: d=menu.addAction(u'☠ Delete') d.triggered.connect(lambda: self.doObjManip('del',entry,isNone,isObj)) replace=menu.addAction(u'⇄ Replace') replace.triggered.connect(lambda: self.doObjManip('replace',entry,isNone,isObj)) if not isNone: save=menu.addAction(u'⛁ Save') save.triggered.connect(lambda: self.doObjManip('save',entry,isNone,isObj)) load=menu.addAction(u'↥ Load') load.triggered.connect(lambda: self.doObjManip('load',entry,isNone,isObj)) lib=makeLibraryBrowser(menu,lambda name,obj: self.setFromLib(entry,name,obj),entry.T,u'⇈ Library') # put this into each varibles menus instead, as there is no the label at the top if not self.showType or not self.hasSer: menu.addSeparator() v=menu.addAction('Variables') v.setCheckable(True); v.setChecked(self.labelIsVar) v.triggered.connect(lambda: self.toggleLabelIsVar(None)) c=menu.addAction(u'Checks') c.setCheckable(True); c.setChecked(self.showChecks) c.triggered.connect(lambda: self.toggleShowChecks(None)) u=menu.addAction('Units') u.setCheckable(True); u.setChecked(self.showUnits) u.triggered.connect(lambda: self.toggleShowUnits(None)) menu.popup(entry.widgets['label'].mapToGlobal(pos))
[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 mkWidgets(self): onlyDefaultGroups=(len(self.entryGroups)==1 and self.entryGroups[0].name==None) if self.showType and self.hasSer: # create type label lab=ObjQLabel(self,makeObjectLabel(self.ser,addr=True,href=True),tooltip=self.getDocstring(),path=self.path,ser=self.ser,searchSlot=self.search) self.objQLabel=lab lab.setFrameShape(QFrame.Box); lab.setFrameShadow(QFrame.Sunken); lab.setLineWidth(2); lab.qlabel.setAlignment(Qt.AlignHCenter); lab.qlabel.linkActivated.connect(woo.qt.openUrl) ## attach context menu to the label lab.setContextMenuPolicy(Qt.CustomContextMenu) lab.customContextMenuRequested.connect(lambda pos: self.serQLabelMenu(lab,pos)) lab.setFocusPolicy(Qt.ClickFocus) lay=QGridLayout(self) lay.setContentsMargins(2,2,2,2) lay.setVerticalSpacing(0) if self.showType: lay.addWidget(lab,0,0,1,-1) lay.setRowStretch(0,-1) self.setLayout(lay) maxLabelWd=0 for entry in self.entries: w=self.mkWidget(entry) if w!=None: entry.widget=entry.widgets['value']=w # else: print 'No value widget for',entry.obj.__class__.__name__+'.'+entry.name #entry.widgets['value']=entry.widget # for code compat #if not entry.widgets['value']: entry.widgets['value']=entry.widget=QFrame() # avoid None widgets objPath=(self.path+'.'+entry.name) if self.path else None labelText,labelTooltip=self.getAttrLabelToolTip(entry) label=ObjQLabel(self,labelText,tooltip=labelTooltip,path=objPath,elide=not self.labelIsVar) entry.widgets['label']=label if self.objManip and ('value' in entry.widgets): label.setContextMenuPolicy(Qt.CustomContextMenu) label.customContextMenuRequested.connect(lambda pos,entry=entry: self.objManipLabelMenu(entry,pos)) label.setFocusPolicy(Qt.ClickFocus) entry.widgets['check']=QCheckBox('',self) ch=entry.widgets['check'] ch.setVisible(self.showChecks) if entry.trait.readonly: ch.setEnabled(False) ch.clicked.connect(entry.toggleChecked) if entry.trait.unit: # frame for all unit-manipulating boxes entry.widgets['unit']=QFrame(self) unitLay=QHBoxLayout(entry.widgets['unit']) entry.unitLayout=unitLay unitLay.setSpacing(0); unitLay.setContentsMargins(0,0,0,0) entry.widgets['unit'].setLayout(unitLay) assert len(entry.trait.unit)==len(entry.trait.altUnits) assert len(entry.trait.unit)==len(entry.trait.prefUnit) unitChoice=False for unit,pref,alt in zip(entry.trait.unit,entry.trait.prefUnit,entry.trait.altUnits): if alt: # there are alternative units, we give choice therefore unitChoice=True w=QComboBox(self) w.addItem(_unicodeUnit(unit)) w.activated.connect(entry.unitChanged) for u,mult in alt: w.addItem(_unicodeUnit(u)) # set preferred unit right away; when units are not shown, always use SI, however if unit!=pref[0]: # this is checked in c++, should never fail # print entry.trait.name,[i for i in range(len(alt)) if alt[i][0]==pref[0]] ii=[i for i in range(len(alt)) if alt[i][0]==pref[0]][0] w.setCurrentIndex(ii+1) else: w=QLabel(_unicodeUnit(unit),self) unitLay.addWidget(w) if unitChoice: entry.unitChanged() # postpone calling this at the very end entry.widgets['unit'].setVisible(self.showUnits) if entry.trait.buttons: bb=entry.trait.buttons[0] for i in range(0,len(bb),3): b=QPushButton(bb[i],self) l=QLabel(self.fontMetrics().elidedText(' '+bb[i+2],Qt.ElideRight,100)) b.setToolTip('<code>'+bb[i+1]+'</code>') b.setStyleSheet('QPushButton {text-align: left; padding-left:5px; }') b.setFocusPolicy(Qt.NoFocus) l.setToolTip(bb[i+2]) def callButton(cmd): exec(cmd, globals(),dict(self=entry.obj,S=woo.master.scene)) # first arg (foo) used by the dispatch # cmd=bb[i+1] binds the current value bb[i+1] b.clicked.connect(lambda foo,cmd=bb[i+1]: callButton(cmd)) entry.widgets['buttons-%d'%(i//3)]=b entry.widgets['buttonLabels-%d'%(i//3)]=l #print 'Buttons',entry.trait.name,entry.trait.buttons,entry.widgets self.entryGroups[entry.groupNo].entries.append(entry) for i,g in enumerate(self.entryGroups): hide=i>0 if not onlyDefaultGroups: ex=g.makeExpander(not hide) # first group expanded, other hidden lay.addWidget(ex,lay.rowCount(),0,1,-1) lay.setRowStretch(lay.rowCount()-1,-1) for i,entry in enumerate(g.entries): try: def addButtonsNow(): bb,ll=sorted([k for k in entry.widgets.keys() if k.startswith('buttons-')]),sorted([k for k in entry.widgets.keys() if k.startswith('buttonLabels-')]) for b,l in zip(bb,ll): row=lay.rowCount() lay.addWidget(entry.widgets[b],row,self.gridCols['value'],1,1) lay.addWidget(entry.widgets[l],row,self.gridCols['label'],1,1) entry.widgets[l].setStyleSheet('background: palette(%s); '%('Base' if row%2 else 'AlternateBase')) # add buttons before if entry.trait.buttons and entry.trait.buttons[1]==True: addButtonsNow() row=lay.rowCount() entry.gridAndRow=lay,row lay.addWidget(entry.widgets['check'],row,self.gridCols['check'],1,1) lay.addWidget(entry.widgets['label'],row,self.gridCols['label'],1,1) entry.widgets['check'].setFocusPolicy(Qt.ClickFocus) # entry.widgets['label'].setFocusPolicy(Qt.NoFocus) # default maxLabelWd=max(maxLabelWd,entry.widgets['label'].width()) if 'value' in entry.widgets: colSpan=(2 if 'unit' not in entry.widgets else 1) # use the unit column if there is no unit lay.addWidget(entry.widgets['value'],row,self.gridCols['value'],1,colSpan) # entry.widgets['value'].setFocusPolicy(Qt.StrongFocus) # default if 'unit' in entry.widgets: lay.addWidget(entry.widgets['unit'],row,self.gridCols['unit']) entry.widgets['unit'].setFocusPolicy(Qt.ClickFocus) # skip when keyboard-navigating lay.setRowStretch(row,2) for w in entry.widgets['label'],: # entry.widgets['value']: if not w or w.__class__==ObjectEditor: continue # nested editor not modified w.setStyleSheet('background: palette(%s); '%('Base' if row%2 else 'AlternateBase')) # add buttons after if entry.trait.buttons and entry.trait.buttons[1]==False: addButtonsNow() except RuntimeError: print('ERROR while creating widget for entry %s (%s)'%(entry.name,objPath)) import traceback traceback.print_exc() # close all groups except the first one for g in self.entryGroups[1:]: g.toggleExpander() lay.setColumnMinimumWidth(self.gridCols['label'],int(maxLabelWd)) lay.setSpacing(0) lay.addWidget(QFrame(self),lay.rowCount(),0,1,-1) # expander at the very end lay.setRowStretch(lay.rowCount()-1,10000) lay.setColumnStretch(self.gridCols['check'],-1) lay.setColumnStretch(self.gridCols['label'],2) lay.setColumnStretch(self.gridCols['value'],10) lay.setColumnStretch(self.gridCols['unit'],-1) self.refreshEvent()
[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 (&gt;%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
[docs] def contextMenuEvent(self, event): # may return -1, which is OK index=self.localPositionToIndex(event.pos()) seq=self.getter() if len(seq)==0: index=-1 item=self.form.itemAt(index,QFormLayout.LabelRole) field=item.widget() if (index>=0 and item) else None menu=QMenu(self) actNew,actKill,actUp,actDown,actFromClip=[menu.addAction(name) for name in (u'☘ New',u'☠ Remove',u'↑ Up',u'↓ Down',u'↵ From clipboard')] if index<0: [a.setEnabled(False) for a in (actKill,actUp,actDown)] if index==len(seq)-1: actDown.setEnabled(False) if index==0: actUp.setEnabled(False) # disable until we figure out how to cancel when the no item in the menu is chosen #if field: field.setStyleSheet('QWidget { background: green }') menu.popup(self.mapToGlobal(event.pos())) actNew.triggered.connect(lambda: self.newSlot(index)) actKill.triggered.connect(lambda: self.killSlot(index)) actUp.triggered.connect(lambda: self.upSlot(index)) actDown.triggered.connect(lambda: self.downSlot(index)) actFromClip.triggered.connect(lambda: self.fromClipSlot(index))
# 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)