class NonExclusivePushButtons2(gpi.NonExclusivePushButtons): valueChanged = gpi.Signal() def __init__(self, title, parent=None): super(NonExclusivePushButtons2, self).__init__(title, parent) # overwrite set_val def set_val(self, value): if type(value) is not list: value = [value] # remove any values that don't have a corresponding button names = [button.text() for button in self.buttons] temp = [] for i in range(len(value)): if value[i] in names: temp.append(value[i]) value = temp self._value = value for button_ind in range(len(self.buttons)): name = self.buttons[button_ind].text() if name in value: self.buttons[button_ind].setChecked(True) else: self.buttons[button_ind].setChecked(False) # overwrite findValue def findValue(self, value): valarr = [] for button in self.buttons: if button.isChecked(): valarr.append(button.text()) self._value = valarr return
class GPITabBar(QtWidgets.QTabBar): currentChanged = gpi.Signal() def __init__(self, parent=None): super().__init__(parent) self._labels_at_press = None def clearTabs(self): while self.count() > 0: self.removeTab(0) def addTabsFromList(self, names): for i in range(len(names)): self.addTab(str(names[i])) def labels(self): return [str(self.tabText(i)) for i in range(self.count())] def mousePressEvent(self, event): self._labels_at_press = self.labels() super().mousePressEvent(event) def mouseReleaseEvent(self, event): super().mouseReleaseEvent(event) if self.labels() != self._labels_at_press: self.currentChanged.emit()
class OrderButtons(gpi.GenericWidgetGroup): """A set of reorderable tabs.""" valueChanged = gpi.Signal() def __init__(self, title, parent=None): super().__init__(title, parent) # at least one button wdgLayout = QtWidgets.QGridLayout() self.wdg = GPITabBar() self.wdg.addTab('0') self.wdg.setMovable(True) self.wdg.currentChanged.connect(self.valueChanged.emit) # layout wdgLayout.addWidget(self.wdg) self.setLayout(wdgLayout) # setters def set_val(self, names): """list(str,str,...) | A list of labels (e.g. ['b1', 'b2',...]).""" self.wdg.clearTabs() self.wdg.addTabsFromList(names) # getters def get_val(self): return self.wdg.labels()
class GPIState(QtCore.QObject): """A single FSM state to be used with the GPI_FSM class. It manages its transitions, entry and exit functions. """ entered = gpi.Signal() exited = gpi.Signal() def __init__(self, name, func, machine=None, efunc=None): super(GPIState, self).__init__() if not isinstance(name, str): msg = "expecting str arg: GPIState.__init__(>str<,func,GPI_FSM)" log.critical(msg) if machine: if not isinstance(machine, GPI_FSM): msg = "expecting GPI_FSM arg: GPIState.__init__(str,func,>GPI_FSM<)" log.critical(msg) self._name = str(name) self._transitions = {} self._func = func # on entry self._efunc = efunc # on exit if machine: machine.addState(self) def addTransition(self, sig, state): if not isinstance(sig, str): msg = "expecting str arg: GPIState.addTransition(>str<,GPIState)" log.critical(msg) self._transitions[sig] = state @property def name(self): return self._name def transitions(self): return self._transitions def onEntry(self, sig): self.entered.emit() if self._func: self._func(sig) def onExit(self, sig): self.exited.emit() if self._efunc: self._efunc(sig)
class ATask(QtCore.QObject): '''App-Loop or Main-Loop executed task. This will block GUI updates. Data is communicated directly. NOTE: The apploop-type blocks until finished, obviating the need for signals or timer checks ''' finished = gpi.Signal() terminated = gpi.Signal() def __init__(self, func, title, label, proxy): super(ATask, self).__init__() self._func = func self._title = title self._label = label self._proxy = proxy self._cnt = 0 def run(self): # This try/except is only good for catching compute() exceptions # not run() terminations. try: self._retcode = self._func() except: log.error('APPLOOP: \'' + str(self._title) + '\':\'' + str(self._label) + '\' compute() failed.\n' + str(traceback.format_exc())) self._retcode = Return.ComputeError def terminate(self): pass # can't happen b/c blocking mainloop def wait(self): pass # can't happen b/c blocking mainloop def isRunning(self): pass # can't happen b/c blocking mainloop def quit(self): pass def start(self): self.run() self.finished.emit()
class StringBoxes(gpi.GenericWidgetGroup): """A set of editable string boxes""" valueChanged = gpi.Signal() def __init__(self, title, parent=None): super(StringBoxes, self).__init__(title, parent) # at least one string box self.boxes = [] self.boxes.append(QtWidgets.QLineEdit()) self.vbox = QtWidgets.QVBoxLayout() self.vbox.setSpacing(0) for box in self.boxes: self.vbox.addWidget(box) box.returnPressed.connect(self.valueChanged) self.setLayout(self.vbox) # setters def set_strings(self, strlist): """str | Set the string (str).""" ind = 0 if len(strlist) < len(self.boxes): strlendiff = len(self.boxes) - len(strlist) strlist.extend(strlendiff * ['']) log.warn("StringBoxes: list of strings shorter than number of \ boxes. Filling rest with empty strings.") if len(strlist) > len(self.boxes): log.warn("StringBoxes: list of strings longer than number of \ boxes. Only using first portion of strings.") for box in self.boxes: string = strlist[ind] box.setText(string) ind = ind + 1 def set_length(self, length): # add specified number of string boxes while length > len(self.boxes): newbox = QtWidgets.QLineEdit() self.boxes.append(newbox) newbox.returnPressed.connect(self.valueChanged) self.vbox.addWidget(newbox) while length < len(self.boxes): oldbox = self.boxes.pop() oldbox.setParent(None) if length != len(self.boxes): log.critical("StringBoxes: length not properly set!") def get_strings(self): strings = [] for box in self.boxes: strings.append(box.text()) return strings
class MainWin_close(QtWidgets.QMainWindow): window_closed = gpi.Signal() def __init__(self): super().__init__() self._isActive = True def closeEvent(self, event): super().closeEvent(event) self.window_closed.emit() self._isActive = False def isActive(self): return self._isActive
class Tee(QtCore.QObject): '''(Mostly Unused) An attempt at providing console output to a GPI internal console window. This is part of an unfinished feature that is meant to give the user easy access to logging information. ''' newStreamTxt = gpi.Signal(str) def __init__(self, stdIO=None, parent=None): """Redirect to a pyqtSignal and stdIO stream. stdIO = alternate stream ( can be the original sys.stdout ) """ super(Tee, self).__init__(parent) self._stdIO = stdIO self._fromProc = False def setMultiProc(self, val=True): self._fromProc = val def isMultiProc(self): return self._fromProc def write(self, m): #if self._color and self._edit: # tc = self._edit.textColor() # self._edit.setTextColor(self._color) # # if self._edit: # self._edit.moveCursor(QtGui.QTextCursor.End) # self._edit.insertPlainText( m ) # # if self._color and self._edit: # self._edit.setTextColor(tc) # if not self.isMultiProc(): self.newStreamTxt.emit(m) if self._stdIO: self._stdIO.write(m) def flush(self): self._stdIO.flush()
class PTask(multiprocessing_context.Process, QtCore.QObject): '''A forked process node task. Memmaps are used to communicate data. NOTE: The process-type has to be checked periodically to see if its alive, from the spawning process. ''' finished = gpi.Signal() terminated = gpi.Signal() def __init__(self, func, title, label, proxy): multiprocessing_context.Process.__init__(self) QtCore.QObject.__init__(self) self._func = func self._title = title self._label = label self._proxy = proxy self._cnt = 0 # Since we don't know when the process finishes # probe at regular intervals. # -it would be nicer to have the process check-in with the GPI # main proc when its done. self._timer = QtCore.QTimer() self._timer.timeout.connect(self.checkProcess) self._timer.start(10) # 10msec update def run(self): # This try/except is only good for catching compute() exceptions # not run() terminations. try: self._proxy.append(['retcode', self._func()]) except: log.error('PROCESS: \'' + str(self._title) + '\':\'' + str(self._label) + '\' compute() failed.\n' + str(traceback.format_exc())) self._proxy.append(['retcode', Return.ComputeError]) def terminate(self): self._timer.stop() super(PTask, self).terminate() def wait(self): self.join() def isRunning(self): return self.is_alive() def retcodeExists(self): for o in self._proxy: if o[0] == 'retcode': return True return False def checkProcess(self): if self.is_alive(): return # else if its not alive: self._timer.stop() if self.retcodeExists(): # we assume its termination was deliberate. self.finished.emit() else: self.terminated.emit()
class ComboBox_GROUP(gpi.GenericWidgetGroup): """A combination of two comboBoxes for image quality measure of two images next to each other. """ valueChanged = gpi.Signal() def __init__(self, title, parent=None): super(ComboBox_GROUP, self).__init__(title, parent) self._val = {} self._val['image_quality_left'] = 0 self._val['image_quality_right'] = 0 #image_quality_left self.iql = gpi.ComboBox('image_quality_left') self.iql.set_items(['choose..', 'excellent', 'good', 'diagnostic', 'non-diagnostic']) #self.iql.set_val('choose..') self.iql.set_index(0) #image_quality_right self.iqr = gpi.ComboBox('image_quality_right') self.iqr.set_items(['choose..', 'excellent', 'good', 'diagnostic', 'non-diagnostic']) #self.iqr.set_val('choose..') self.iqr.set_index(0) self.iql.valueChanged.connect(self.iqlChange) self.iqr.valueChanged.connect(self.iqrChange) vbox = QtGui.QHBoxLayout() vbox.addWidget(self.iql) vbox.addWidget(self.iqr) vbox.setStretch(0, 0) vbox.setStretch(1, 0) vbox.setContentsMargins(0, 0, 0, 0) # we don't need margins here vbox.setSpacing(0) self.setLayout(vbox) # setters def set_val(self, val): """A python-dict containing: image_quality_left and image_quality_right parms. """ if 'image_quality_left' in val: # check if identical so this does not change every time compute is called (according to code in FFTW) if self._val['image_quality_left'] != val['image_quality_left']: self._val['image_quality_left'] = val['image_quality_left'] self.iql.blockSignals(True) self.iql.set_index(val['image_quality_left']) self.iql.blockSignals(False) if 'image_quality_right' in val: if self._val['image_quality_right'] != val['image_quality_right']: self._val['image_quality_right'] = val['image_quality_right'] self.iqr.blockSignals(True) self.iqr.set_index(val['image_quality_right']) self.iqr.blockSignals(False) # getters def get_val(self): return self._val # support def iqlChange(self, val): self._val['image_quality_left'] = val self.valueChanged.emit() def iqrChange(self, val): self._val['image_quality_right'] = val self.valueChanged.emit()
class DICOM_HDR_GROUP(gpi.GenericWidgetGroup): """A combination of Textboxes and StringBoxes to allow for easy display of DICOM header info and editing of the values""" valueChanged = gpi.Signal() def __init__(self, title, parent=None): super(DICOM_HDR_GROUP, self).__init__(title, parent) self._val = {} self._val['tags'] = '' self._val['desc'] = '' self._val['VRs'] = '' self._val['values'] = '' self.tb1 = TextBoxes('Tags:') self.tb1.set_length(1) self.tb2 = TextBoxes('Descriptions:') self.tb2.set_length(1) self.tb3 = TextBoxes('VRs:') self.tb3.set_length(1) self.sb = StringBoxes('Values:') self.sb.set_length(1) self.sb.valueChanged.connect(self.valueChanged) vbox = QtWidgets.QHBoxLayout() vbox.addWidget(self.tb1) vbox.addWidget(self.tb2) vbox.addWidget(self.tb3) vbox.addWidget(self.sb) vbox.setStretch(0, 0) vbox.setStretch(1, 0) vbox.setStretch(2, 0) vbox.setStretch(3, 0) vbox.setContentsMargins(0, 0, 0, 0) # we don't need margins here vbox.setSpacing(0) self.setLayout(vbox) # setters def set_info(self, val): """A python-dict containing: tags, desc, and VRs parms. """ if 'tags' in val: self._val['tags'] = val['tags'] self.setTagsQuietly(val['tags']) if 'desc' in val: self._val['desc'] = val['desc'] self.setDescriptionsQuietly(val['desc']) if 'VRs' in val: self._val['VRs'] = val['VRs'] self.setVRsQuietly(val['VRs']) if 'values' in val: self._val['values'] = val['values'] self.setValuesQuietly(val['values']) def set_length(self, length): self.tb1.set_length(length) self.tb2.set_length(length) self.tb3.set_length(length) self.sb.set_length(length) # getters def get_val(self): return self.sb.get_strings() # support def setTagsQuietly(self, val): self.tb1.blockSignals(True) self.tb1.set_strings(val) self.tb1.blockSignals(False) def setDescriptionsQuietly(self, val): self.tb2.blockSignals(True) self.tb2.set_strings(val) self.tb2.blockSignals(False) def setVRsQuietly(self, val): self.tb3.blockSignals(True) self.tb3.set_strings(val) self.tb3.blockSignals(False) def setValuesQuietly(self, val): self.sb.blockSignals(True) self.sb.set_strings(val) self.sb.blockSignals(False)
class RESHAPE_GROUP(gpi.GenericWidgetGroup): """Widget for entering a new shape via spin boxes""" valueChanged = gpi.Signal() def __init__(self, title, parent=None): super().__init__(title, parent) # buttons for adding/removing a box self.sub = gpi.BasicPushButton() self.sub.set_toggle(False) self.sub.set_button_title(u'\u2212') self.sub.valueChanged.connect(self.removeBox) self.add = gpi.BasicPushButton() self.add.set_toggle(False) self.add.set_button_title(u'\uFF0B') self.add.valueChanged.connect(self.addBox) self.hbox = QtWidgets.QHBoxLayout() self.hbox.setSpacing(0) self.hbox.addWidget(self.sub) self.hbox.addWidget(self.add) # at least one spin box self.boxes = [] self.boxes.append(gpi.BasicSpinBox()) self.vbox = QtWidgets.QVBoxLayout() self.vbox.setSpacing(0) self.vbox.addLayout(self.hbox) for box in self.boxes: self.vbox.addWidget(box) box.valueChanged.connect(self.valueChanged) self.setLayout(self.vbox) # setters def set_vals(self, inlist): while len(inlist) < len(self.boxes): oldbox = self.boxes.pop(0) self.vbox.removeWidget(oldbox) oldbox.deleteLater() oldbox.setParent(None) oldbox = None while len(inlist) > len(self.boxes): newbox = gpi.BasicSpinBox() self.boxes.insert(0, newbox) newbox.valueChanged.connect(self.valueChanged) newbox.set_min(1) newbox.set_val(1) newbox.set_max(gpi.GPI_INT_MAX) self.vbox.addWidget(newbox) if len(inlist) != len(self.boxes): self.log.critical("RESHAPE_GROUP: number of boxes not correct!") ind = 0 for box in self.boxes: inval = inlist[ind] box.set_val(inval) ind = ind + 1 # support def removeBox(self): if len(self.boxes) > 1: oldbox = self.boxes.pop(0) self.vbox.removeWidget(oldbox) oldbox.deleteLater() oldbox.setParent(None) oldbox = None self.valueChanged.emit() def addBox(self): newbox = gpi.BasicSpinBox() self.boxes.insert(0, newbox) newbox.valueChanged.connect(self.valueChanged) newbox.set_min(1) newbox.set_val(1) newbox.set_max(gpi.GPI_INT_MAX) self.vbox.addWidget(newbox) self.valueChanged.emit() def get_vals(self): vals = [] for box in self.boxes: vals.append(box.get_val()) return vals
class DataSliders(gpi.GenericWidgetGroup): """Combines the BasicCWFCSliders with ExclusivePushButtons for a unique widget element useful for reduce dimensions. """ valueChanged = gpi.Signal() def __init__(self, title, parent=None): super().__init__(title, parent) self.sl = gpi.BasicCWFCSliders() self.sl.valueChanged.connect(self.valueChanged) # at least one button self.button_names = ['C/W'] self.buttons = [] cnt = 0 wdgLayout = QtWidgets.QGridLayout() for name in self.button_names: newbutton = QtWidgets.QPushButton(name) newbutton.setCheckable(True) newbutton.setAutoExclusive(True) self.buttons.append(newbutton) newbutton.clicked.connect(self.findValue) newbutton.clicked.connect(self.valueChanged) wdgLayout.addWidget(newbutton, 0, cnt, 1, 1) cnt += 1 # layout wdgLayout.addWidget(self.sl, 1, 0, 1, 4) wdgLayout.setVerticalSpacing(0) wdgLayout.setSpacing(0) self.setLayout(wdgLayout) # default self.set_min(1) self._selection = 0 # pass is default #self.buttons[self._selection].setChecked(True) self.sl.set_allvisible(False) # setters def set_val(self, val): """A python-dict containing keys: center, width, floor, ceiling, and selection. """ self.sl.set_center(val['center']) self.sl.set_width(val['width']) self.sl.set_floor(val['floor']) self.sl.set_ceiling(val['ceiling']) self._selection = val['selection'] self.buttons[self._selection].blockSignals(True) self.buttons[self._selection].setChecked(True) self.buttons[self._selection].blockSignals(False) self.findValue(None) def set_min(self, val): """A min value for center, width, floor and ceiling (int).""" self.sl.set_min(val) def set_max(self, val): """A max value for center, width, floor and ceiling (int).""" self.sl.set_max(val) # getters def get_val(self): val = {} val['selection'] = self._selection val['center'] = self.sl.get_center() val['width'] = self.sl.get_width() val['floor'] = self.sl.get_floor() val['ceiling'] = self.sl.get_ceiling() return val def get_min(self): return self.sl.get_min() def get_max(self): return self.sl.get_max() # support def setCropBounds(self): self.sl.set_min_width(1) def setSliceBounds(self): self.sl.set_min_width(0) self.sl.set_width(1) def setPassBounds(self): self.sl.set_min_width(1) self.sl.set_width(self.get_max()) # JGP For Pass, reset cwfc self.sl.set_center((self.get_max() + 1) / 2) self.sl.set_width(self.get_max()) self.sl.set_floor(1) self.sl.set_ceiling(self.get_max()) def findValue(self, value): cnt = 0 for button in self.buttons: if button.isChecked(): self._selection = cnt cnt += 1 # hide appropriate sliders if self._selection == 0: # C/W self.sl.set_cwvisible(True) self.setCropBounds()
class WindowLevel(gpi.GenericWidgetGroup): """Provides an interface to the BasicCWFCSliders.""" valueChanged = gpi.Signal() def __init__(self, title, parent=None): super().__init__(title, parent) self.sl = gpi.BasicCWFCSliders() self.sl.valueChanged.connect(self.valueChanged) self.pb = gpi.BasicPushButton() self.pb.set_button_title('reset') # layout wdgLayout = QtWidgets.QVBoxLayout() wdgLayout.addWidget(self.sl) wdgLayout.addWidget(self.pb) self.setLayout(wdgLayout) # default self.set_min(0) self.set_max(100) self.sl.set_allvisible(True) self.reset_sliders() self.pb.valueChanged.connect(self.reset_sliders) # setters def set_val(self, val): """Set multiple values with a python-dict with keys: level, window, floor and ceiling. -Requires integer values for each key. """ self.sl.set_center(val['level']) self.sl.set_width(val['window']) self.sl.set_floor(val['floor']) self.sl.set_ceiling(val['ceiling']) def set_min(self, val): """Set min for level, window, floor and ceiling (int).""" self.sl.set_min(val) def set_max(self, val): """Set max for level, window, floor and ceiling (int).""" self.sl.set_max(val) # getters def get_val(self): val = {} val['level'] = self.sl.get_center() val['window'] = self.sl.get_width() val['floor'] = self.sl.get_floor() val['ceiling'] = self.sl.get_ceiling() return val def get_min(self): return self.sl.get_min() def get_max(self): return self.sl.get_max() def reset_sliders(self): val = {} val['window'] = 100 val['level'] = 50 val['floor'] = 0 val['ceiling'] = 100 self.set_val(val)
class NodeAPI(QtWidgets.QWidget): """ Base class for all external nodes. External nodes implement the extensible methods of this class: i.e. compute(), validate(), execType(), etc... to create a unique Node module. """ GPIExtNodeType = ExternalNodeType # ensures the subclass is of THIS class modifyWdg = gpi.Signal(str, dict) def __init__(self, node): super(NodeAPI, self).__init__() #self.setToolTip("Double Click to Show/Hide each Widget") self.node = node self.label = '' self._detailLabel = '' self._docText = None self.parmList = [] # deprecated, since dicts have direct name lookup self.parmDict = {} # mirror parmList for now self.parmSettings = {} # for buffering wdg parms before copying to a PROCESS self.shdmDict = {} # for storing base addresses # grid for module widgets self.layout = QtWidgets.QGridLayout() # this must exist before user-widgets are added so that they can get # node label updates self.wdglabel = QtWidgets.QLineEdit(self.label) # allow logger to be used in initUI() self.log = manager.getLogger(node.getModuleName()) try: self._initUI_ret = self.initUI() except: log.error('initUI() failed. '+str(node.item.fullpath)+'\n'+str(traceback.format_exc())) self._initUI_ret = -1 # error # make a label box with the unique id labelGroup = HidableGroupBox("Node Label") labelLayout = QtWidgets.QGridLayout() self.wdglabel.textChanged.connect(self.setLabel) labelLayout.addWidget(self.wdglabel, 0, 1) labelGroup.setLayout(labelLayout) self.layout.addWidget(labelGroup, len(self.parmList) + 1, 0) labelGroup.set_collapsed(True) labelGroup.setToolTip("Displays the Label on the Canvas (Double Click)") # make an about box with the unique id self.aboutGroup = HidableGroupBox("About") aboutLayout = QtWidgets.QGridLayout() self.about_button = QtWidgets.QPushButton("Open Node &Documentation") self.about_button.clicked.connect(self.openNodeDocumentation) aboutLayout.addWidget(self.about_button, 0, 1) self.aboutGroup.setLayout(aboutLayout) self.layout.addWidget(self.aboutGroup, len(self.parmList) + 2, 0) self.aboutGroup.set_collapsed(True) self.aboutGroup.setToolTip("Node Documentation (docstring + autodocs, Double Click)") # window (just a QTextEdit) that will show documentation text self.doc_text_win = QtWidgets.QTextEdit() self.doc_text_win.setPlainText(self.generateHelpText()) self.doc_text_win.setReadOnly(True) doc_text_font = QtGui.QFont("Monospace", 14) self.doc_text_win.setFont(doc_text_font) self.doc_text_win.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) self.doc_text_win.setWindowTitle(node.getModuleName() + " Documentation") hbox = QtWidgets.QHBoxLayout() self._statusbar_sys = QtWidgets.QLabel('') self._statusbar_usr = QtWidgets.QLabel('') hbox.addWidget(self._statusbar_sys) hbox.addWidget(self._statusbar_usr, 0, (QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)) # window resize grip self._grip = QtWidgets.QSizeGrip(self) hbox.addWidget(self._grip) self.layout.addLayout(hbox, len(self.parmList) + 3, 0) self.layout.setRowStretch(len(self.parmList) + 3, 0) # uid display # uid = QtWidgets.QLabel("uid: "+str(self.node.getID())) # uid.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) # self.layout.addWidget(uid,len(self.parmList)+2,0) # instantiate the layout self.setLayout(self.layout) # run through all widget titles since each widget parent is now set. for parm in self.parmList: parm.setDispTitle() # instantiate the layout # self.setGeometry(50, 50, 300, 40) self.setTitle(node.getModuleName()) self._starttime = 0 self._startline = 0 def initUI_return(self): return self._initUI_ret def getWidgets(self): # return a list of widgets return self.parmList def getWidgetNames(self): return list(self.parmDict.keys()) def starttime(self): """Begin the timer for the node `Wall Time` calculation. Nodes store their own runtime, which is displayed in a tooltip when hovering over the node on the canvas (see :doc:`../ui`). Normally, each node reports the complete time it takes to run its :py:meth:`compute` function. However, a dev can use this method along with :py:meth:`endtime` to set the portion of :py:meth:`compute` to be used to calculate the `Wall Time`. """ self._startline = inspect.currentframe().f_back.f_lineno self._starttime = time.time() def endtime(self, msg=''): """Begin the timer for the node `Wall Time` calculation. Nodes store their own runtime, which is displayed in a tooltip when hovering over the node on the canvas (see :doc:`../ui`). Normally, each node reports the complete time it takes to run its :py:meth:`compute` function. However, a dev can use this method along with :py:meth:`starttime` to set the portion of :py:meth:`compute` to be used to calculate the `Wall Time`. This method also prints the `Wall Time` to stdout, along with an optional additional message, using :py:meth:`gpi.logger.PrintLogger.node`). Args: msg (string): a message to be sent to stdout (using :py:meth:`gpi.logger.PrintLogger.node`) along with the `Wall Time` """ ttime = time.time() - self._starttime eline = inspect.currentframe().f_back.f_lineno log.node(self.node.getName()+' - '+str(ttime)+'sec, between lines:'+str(self._startline)+'-'+str(eline)+'. '+msg) def stringifyExecType(self): if self.execType() is GPI_PROCESS: return " [Process]" elif self.execType() is GPI_THREAD: return " [Thread]" else: return " [App-Loop]" def setStatus_sys(self, msg): msg += self.stringifyExecType() self._statusbar_sys.setText(msg) def setStatus(self, msg): self._statusbar_usr.setText(msg) def execType(self): # default executable type # return GPI_THREAD return GPI_PROCESS # this is the safest # return GPI_APPLOOP def setReQueue(self, val=False): # NODEAPI # At the end of a nodeQueue, these tasked are checked for # more events. if self.node.inDisabledState(): return if self.node.nodeCompute_thread.execType() == GPI_PROCESS: self.node.nodeCompute_thread.addToQueue(['setReQueue', val]) else: self.node._requeue = val def reQueueIsSet(self): return self.node._requeue def getLabel(self): return self.label def moduleExists(self, name): """Give the user a simple module checker for the node validate function. """ try: imp.find_module(name) except ImportError: return False else: return True def moduleValidated(self, name): """Provide a stock error message in the event a c/ext module cannot be found. """ if not self.moduleExists(name): log.error("The \'" + name + "\' module cannot be found, compute() aborted.") return 1 return 0 def setLabelWidget(self, newlabel=''): self.wdglabel.setText(newlabel) def setLabel(self, newlabel=''): self.label = str(newlabel) self.updateTitle() self.node.updateOutportPosition() self.node.graph.scene().update(self.node.boundingRect()) self.node.update() def openNodeDocumentation(self): self.doc_text_win.show() # setting the size only works if we have shown the widget # set the width based on some ideal width (max at 800px) docwidth = self.doc_text_win.document().idealWidth() lmargin = self.doc_text_win.contentsMargins().left() rmargin = self.doc_text_win.contentsMargins().right() scrollbar_width = 20 # estimate, scrollbar overlaps content otherwise total_width = min(lmargin + docwidth + rmargin + scrollbar_width, 800) self.doc_text_win.setFixedWidth(total_width) # set the height based on the content size docheight= self.doc_text_win.document().size().height() self.doc_text_win.setMinimumHeight(min(docheight, 200)) self.doc_text_win.setMaximumHeight(docheight) def setDetailLabel(self, newDetailLabel='', elideMode='middle'): """Set an additional label for the node. This offers a way to programmatically set an additional label for a node (referred to as the `detail label`), which shows up underneath the normal node tile and label (as set wihtin the node menu). This is used by the `core` library to show file paths for the file reader/writer nodes, for example. Args: newDetailLabel (string): The detail label for the node (e.g. file path, operator, ...) elideMode ({'middle', 'left', 'right', 'none'}, optional): Method to use when truncating the detail label with an ellipsis. """ self._detailLabel = str(newDetailLabel) self._detailElideMode = elideMode self.node.updateOutportPosition() def getDetailLabel(self): """Get the current node detail label. Returns: string: The node detail label, as set by :py:meth:`setDetailLabel` """ return self._detailLabel def getDetailLabelElideMode(self): """How the detail label should be elided if it's too long:""" mode = self._detailElideMode qt_mode = QtCore.Qt.ElideMiddle if mode == 'left': qt_mode = QtCore.Qt.ElideLeft elif mode == 'right': qt_mode = QtCore.Qt.ElideRight elif mode == 'none': qt_mode = QtCore.Qt.ElideNone else: # default, mode == 'middle' qt_mode = QtCore.Qt.ElideMiddle return qt_mode # to be subclassed and reimplemented. def initUI(self): """Initialize the node UI (Node Menu). This method is intended to be reimplemented by the external node developer. This is where :py:class:`Port` and :py:class:`Widget` objects are added and initialized. This method is called whenever a node is added to the canvas. See :ref:`adding-widgets` and :ref:`adding-ports` for more detail. """ # window #self.setWindowTitle(self.node.name) # IO Ports #self.addInPort('in1', str, obligation=REQUIRED) #self.addOutPort('out2', int) pass def windowActivationChange(self, test): self.node.graph.scene().makeOnlyTheseNodesSelected([self.node]) def getSettings(self): # NODEAPI """Wrap up all settings from each widget.""" s = {} s['label'] = self.label s['parms'] = [] for parm in self.parmList: s['parms'].append(copy.deepcopy(parm.getSettings())) return s def loadSettings(self, s): self.setLabelWidget(s['label']) # modify node widgets for parm in s['parms']: # the NodeAPI has instantiated the widget by name, this will # change the wdg-ID, however, this step is only dependend on # unique widget names. if not self.getWidget(parm['name']): log.warn("Trying to load settings; can't find widget with name: \'" + \ stw(parm['name']) + "\', skipping...") continue log.debug('Setting widget: \'' + stw(parm['name']) + '\'') try: self.modifyWidget_direct(parm['name'], **parm['kwargs']) except: log.error('Failed to set widget: \'' + stw(parm['name']) + '\'\n' + str(traceback.format_exc())) if parm['kwargs']['inport']: # widget-inports self.addWidgetInPortByName(parm['name']) if parm['kwargs']['outport']: # widget-outports self.addWidgetOutPortByName(parm['name']) def generateHelpText(self): """Gather the __doc__ string of the ExternalNode derived class, all the set_ methods for each attached widget, and any attached GPI-types from each of the ports. """ if self._docText is not None: return self._docText # NODE DOC node_doc = "NODE: \'" + self.node._moduleName + "\'\n" + " " + \ str(self.__doc__) # WIDGETS DOC # parm_doc = "" # contains parameter info wdg_doc = "\n\nAPPENDIX A: (WIDGETS)\n" # generic widget ref info for parm in self.parmList: # wdg order matters wdg_doc += "\n \'" + parm.getTitle() + "\': " + \ str(parm.__class__) + "\n" numSpaces = 8 set_doc = "\n".join((numSpaces * " ") + i for i in str( parm.__doc__).splitlines()) wdg_doc += set_doc + "\n" # set methods for member in dir(parm): if member.startswith('set_'): wdg_doc += (8 * " ") + member + inspect.formatargspec( *inspect.getargspec(getattr(parm, member))) set_doc = str(inspect.getdoc(getattr(parm, member))) numSpaces = 16 set_doc = "\n".join(( numSpaces * " ") + i for i in set_doc.splitlines()) wdg_doc += "\n" + set_doc + "\n" # PORTS DOC port_doc = "\n\nAPPENDIX B: (PORTS)\n" # get the port type info for port in self.node.getPorts(): typ = port.GPIType() port_doc += "\n \'" + port.portTitle + "\': " + \ str(typ.__class__) + "|" + str(type(port)) + "\n" numSpaces = 8 set_doc = "\n".join((numSpaces * " ") + i for i in str( typ.__doc__).splitlines()) port_doc += set_doc + "\n" # set methods for member in dir(typ): if member.startswith('set_'): port_doc += (8 * " ") + member + inspect.formatargspec( *inspect.getargspec(getattr(typ, member))) set_doc = str(inspect.getdoc(getattr(typ, member))) numSpaces = 16 set_doc = "\n".join(( numSpaces * " ") + i for i in set_doc.splitlines()) port_doc += "\n" + set_doc + "\n" # GETTERS/SETTERS getset_doc = "\n\nAPPENDIX C: (GETTERS/SETTERS)\n\n" getset_doc += (8 * " ") + "Node Initialization Setters: (initUI())\n\n" getset_doc += self.formatFuncDoc(self.addWidget) getset_doc += self.formatFuncDoc(self.addInPort) getset_doc += self.formatFuncDoc(self.addOutPort) getset_doc += ( 8 * " ") + "Node Compute Getters/Setters: (compute())\n\n" getset_doc += self.formatFuncDoc(self.getVal) getset_doc += self.formatFuncDoc(self.getAttr) getset_doc += self.formatFuncDoc(self.setAttr) getset_doc += self.formatFuncDoc(self.getData) getset_doc += self.formatFuncDoc(self.setData) self._docText = node_doc # + wdg_doc + port_doc + getset_doc self.doc_text_win.setPlainText(self._docText) return self._docText def formatFuncDoc(self, func): """Generate auto-doc for passed func obj.""" numSpaces = 24 fdoc = inspect.getdoc(func) set_doc = "\n".join(( numSpaces * " ") + i for i in str(fdoc).splitlines()) rdoc = (16 * " ") + func.__name__ + \ inspect.formatargspec(*inspect.getargspec(func)) \ + "\n" + set_doc + "\n\n" return rdoc def printWidgetValues(self): # for debugging for parm in self.parmList: log.debug("widget: " + str(parm)) log.debug("value: " + str(parm.get_val())) # abstracting IF for user def addInPort(self, title=None, type=None, obligation=REQUIRED, menuWidget=None, cyclic=False, **kwargs): """Add an input port to the node. Input ports collect data from other nodes for use/processing within a node compute function. Ports may only be added in the :py:meth:`initUI` routine. Data at an input node can be accessed using :py:meth:`getData`, but is typically read-only. Args: title (str): port title (shown in tooltips) and unique identifier type (str): class name of extended type (e.g. ``np.complex64``) obligation: ``gpi.REQUIRED`` (default) or ``gpi.OPTIONAL`` menuWidget: `for internal use`, devs should leave as default ``None`` cyclic (bool): whether the port should allow reverse-flow from downstream nodes (default is ``False``) kwargs: any set_<arg> method belonging to the ``GPIDefaultType`` derived class """ self.node.addInPort(title, type, obligation, menuWidget, cyclic, **kwargs) self.node.update() # abstracting IF for user def addOutPort(self, title=None, type=None, obligation=REQUIRED, menuWidget=None, **kwargs): """Add an output port to the node. Output nodes provide a conduit for passing data to downstream nodes. Ports may only be added in the :py:meth:`initUI` routine. Data at an output port can be accessed using :py:meth:`setData` and :py:meth:`getData` from within :py:meth:`validate` and :py:meth:`compute`. Args: title (str): port title (shown in tooltips) and unique identifier type (str): class name of extended type (e.g. ``np.float32``) obligation: ``gpi.REQUIRED`` (default) or ``gpi.OPTIONAL`` menuWidget: `for internal use`, debs should leave as default ``None`` kwargs: any set_<arg> method belonging to the ``GPIDefaultType`` derived class """ self.node.addOutPort(title, type, obligation, menuWidget, **kwargs) self.node.update() def addWidgetInPortByName(self, title): wdg = self.findWidgetByName(title) self.addWidgetInPort(wdg) def addWidgetInPortByID(self, wdgID): wdg = self.findWidgetByID(wdgID) self.addWidgetInPort(wdg) def addWidgetInPort(self, wdg): self.addInPort(title=wdg.getTitle(), type=wdg.getDataType(), obligation=OPTIONAL, menuWidget=wdg) def removeWidgetInPort(self, wdg): port = self.findWidgetInPortByName(wdg.getTitle()) self.node.removePortByRef(port) def addWidgetOutPortByID(self, wdgID): wdg = self.findWidgetByID(wdgID) self.addWidgetOutPort(wdg) def addWidgetOutPortByName(self, title): wdg = self.findWidgetByName(title) self.addWidgetOutPort(wdg) def addWidgetOutPort(self, wdg): self.addOutPort( title=wdg.getTitle(), type=wdg.getDataType(), menuWidget=wdg) def removeWidgetOutPort(self, wdg): port = self.findWidgetOutPortByName(wdg.getTitle()) self.node.removePortByRef(port) def addWidget(self, wdg=None, title=None, **kwargs): """Add a widget to the node UI. Args: wdg (str): The widget class name (see :doc:`widgets` for a list of built-in widget classes) title (str): A unique name for the widget, and title shown in the Node Menu kwargs: Additional arguments passed to the set_<arg> methods specific to the chosen widget class """ if (wdg is None) or (title is None): log.critical("addWidget(): widgets need a title" \ + " AND a wdg-str! Aborting.") return # check existence first if self.node.titleExists(title): log.critical("addWidget(): Widget title \'" + str(title) \ + "\' is already in use! Aborting.") return ypos = len(self.parmList) # try widget def's packaged with node def first wdgGroup = None # first see if the node code contains the widget def wdgGroup = self.node.item.getWidget(wdg) # get widget from standard gpi widgets if wdgGroup is None: if hasattr(BUILTIN_WIDGETS, wdg): wdgGroup = getattr(BUILTIN_WIDGETS, wdg) # if its still not found then its a big deal if wdgGroup is None: log.critical("\'" + wdg + "\' widget not found.") raise Exception("Widget not found.") # instantiate if not None else: wdgGroup = wdgGroup(title) wdgGroup.setNodeName(self.node.getModuleName()) wdgGroup._setNodeLabel(self.label) wdgGroup.valueChanged.connect(lambda: self.wdgEvent(title)) wdgGroup.portStateChange.connect(lambda: self.changePortStatus(title)) wdgGroup.returnWidgetToOrigin.connect(self.returnWidgetToNodeMenu) self.wdglabel.textChanged.connect(wdgGroup._setNodeLabel) # add to menu layout self.layout.addWidget(wdgGroup, ypos, 0) self.parmList.append(wdgGroup) self.parmDict[title] = wdgGroup # the new way self.modifyWidget_direct(title, **kwargs) def returnWidgetToNodeMenu(self, wdgid): # find widget by id wdgid = int(wdgid) ind = 0 for parm in self.parmList: if wdgid == parm.get_id(): self.layout.addWidget(parm, ind, 0) if self.node.isMarkedForDeletion(): parm.hide() else: parm.show() break ind += 1 def blockWdgSignals(self, val): """Block all signals, especially valueChanged.""" for parm in self.parmList: parm.blockSignals(val) def blockSignals_byWdg(self, title, val): # get the over-reporting widget by name and block it self.parmDict[title].blockSignals(val) def changePortStatus(self, title): """Add a new in- or out-port tied to this widget.""" log.debug("changePortStatus: " + str(title)) wdg = self.findWidgetByName(title) # make sure the port is either added or will be added. if wdg.inPort_ON: if self.findWidgetInPortByName(title) is None: self.addWidgetInPort(wdg) else: if self.findWidgetInPortByName(title): self.removeWidgetInPort(wdg) if wdg.outPort_ON: if self.findWidgetOutPortByName(title) is None: self.addWidgetOutPort(wdg) else: if self.findWidgetOutPortByName(title): self.removeWidgetOutPort(wdg) def findWidgetByName(self, title): for parm in self.parmList: if parm.getTitle() == title: return parm def findWidgetByID(self, wdgID): for parm in self.parmList: if parm.id() == wdgID: return parm def findWidgetInPortByName(self, title): for port in self.node.inportList: if port.isWidgetPort(): if port.portTitle == title: return port def findWidgetOutPortByName(self, title): for port in self.node.outportList: if port.isWidgetPort(): if port.portTitle == title: return port def modifyWidget_setter(self, src, kw, val): sfunc = "set_" + kw if hasattr(src, sfunc): func = getattr(src, sfunc) func(val) else: try: ttl = src.getTitle() except: ttl = str(src) log.critical("modifyWidget_setter(): Widget \'" + stw(ttl) \ + "\' doesn't have attr \'" + stw(sfunc) + "\'.") def modifyWidget_direct(self, pnumORtitle, **kwargs): src = self.getWidget(pnumORtitle) for k, v in list(kwargs.items()): if k != 'val': self.modifyWidget_setter(src, k, v) # set 'val' last so that bounds don't cause a temporary conflict. if 'val' in kwargs: self.modifyWidget_setter(src, 'val', kwargs['val']) def modifyWidget_buffer(self, title, **kwargs): """GPI_PROCESSes have to use buffered widget attributes to effect the same changes to attributes during compute() as with GPI_THREAD or GPI_APPLOOP. """ src = self.getWdgFromBuffer(title) try: for k, v in list(kwargs.items()): src['kwargs'][k] = v except: log.critical("modifyWidget_buffer() FAILED to modify buffered attribute") def lockWidgetUpdates(self): self._widgetUpdateMutex_locked = True def unlockWidgetUpdates(self): self._widgetUpdateMutex_locked = False def widgetsUpdatesLocked(self): return self._widgetUpdateMutex_locked def wdgEvent(self, title): # Captures all valueChanged events from widgets. # 'title' is for interrogating who changed in compute(). # Once the event status has been set, disable valueChanged signals # until the node has successfully completed. # -this was b/c sliders etc. were causing too many signals resulting # in a recursion overload to this function. #self.blockWdgSignals(True) self.blockSignals_byWdg(title, True) if self.node.hasEventPending(): self.node.appendEvent({GPI_WIDGET_EVENT: title}) return else: self.node.setEventStatus({GPI_WIDGET_EVENT: title}) # Can start event from any applicable 'check' transition if self.node.graph.inIdleState(): self.node.graph._switchSig.emit('check') # for re-drawing the menu title to match module/node instance def updateTitle(self): # Why is this trying the scrollArea? isn't it always a scroll??? if self.label == '': try: self.node._nodeIF_scrollArea.setWindowTitle(self.node.name) except: self.setWindowTitle(self.node.name) else: try: augtitle = self.node.name + ": " + self.label self.node._nodeIF_scrollArea.setWindowTitle(augtitle) except: augtitle = self.node.name + ": " + self.label self.setWindowTitle(augtitle) def setTitle(self, title): self.node.name = title self.updateTitle() # Queue actions for widgets and ports def setAttr(self, title, **kwargs): """Set specific attributes of a given widget. This method may be used to set attributes of any widget during any of the core node functions: :py:meth:`initUI`, :py:meth:`validate`, or :py:meth:`compute`. Args: title (str): the widget name (unique identifier) kwargs: args corresponding to the ``get_<arg>`` methods of the widget class. See :doc:`widgets` for the list of built-in widgets and associated attributes. """ try: # start = time.time() # either emit a signal or save a queue if self.node.inDisabledState(): return if self.node.nodeCompute_thread.execType() == GPI_PROCESS: # PROCESS self.node.nodeCompute_thread.addToQueue(['modifyWdg', title, kwargs]) #self.modifyWidget_buffer(title, **kwargs) elif self.node.nodeCompute_thread.execType() == GPI_THREAD: # THREAD # can't modify QObjects in thread, but we can send a signal to do # so self.modifyWdg.emit(title, kwargs) else: # APPLOOP self.modifyWidget_direct(str(title), **kwargs) except: raise GPIError_nodeAPI_setAttr('self.setAttr(\''+stw(title)+'\',...) failed in the node definition, check the widget name, attribute name and attribute type().') # log.debug("modifyWdg(): time: "+str(time.time() - start)+" sec") def allocArray(self, shape=(1,), dtype=np.float32, name='local'): """return a shared memory array if the node is run as a process. -the array name needs to be unique """ if self.node.nodeCompute_thread.execType() == GPI_PROCESS: buf, shd = DataProxy()._genNDArrayMemmap(shape, dtype, self.node.getID(), name) if shd is not None: # saving the reference id allows the node developer to decide # on the fly if the preallocated array will be used in the final # setData() call. self.shdmDict[str(id(buf))] = shd.filename return buf else: return np.ndarray(shape, dtype=dtype) def setData(self, title, data): """Set the data at an :py:class:`OutPort`. This is typically called in :py:meth:`compute` to set data at an output port, making the data available to downstream nodes. :py:class:`InPort` ports are read-only, so this method should only be used with :py:class:`OutPort` ports. Args: title (str): name of the port to send the object reference data: any object corresponding to a ``GPIType`` class allowed by this port """ try: # start = time.time() # either set directly or save a queue if self.node.inDisabledState(): return if self.node.nodeCompute_thread.execType() == GPI_PROCESS: # numpy arrays if type(data) is np.memmap or type(data) is np.ndarray: if str(id(data)) in self.shdmDict: # pre-alloc s = DataProxy().NDArray(data, shdf=self.shdmDict[str(id(data))], nodeID=self.node.getID(), portname=title) else: s = DataProxy().NDArray(data, nodeID=self.node.getID(), portname=title) # for split objects to pass thru individually # this will be a list of DataProxy objects if type(s) is list: for i in s: self.node.nodeCompute_thread.addToQueue(['setData', title, i]) # a single DataProxy object else: self.node.nodeCompute_thread.addToQueue(['setData', title, s]) # all other non-numpy data that are pickleable else: # PROCESS output other than numpy self.node.nodeCompute_thread.addToQueue(['setData', title, data]) else: # THREAD or APPLOOP self.node.setData(title, data) # log.debug("setData(): time: "+str(time.time() - start)+" sec") except: print((str(traceback.format_exc()))) raise GPIError_nodeAPI_setData('self.setData(\''+stw(title)+'\',...) failed in the node definition, check the output name and data type().') def getData(self, title): """Get the data from a :py:class:`Port` for this node. Usually this is used to get input data from a :py:class:`InPort`, though in some circumstances (e.g. in the `Glue` node) it may be used to get data from an :py:class:`OutPort`. This method is available for devs to use in :py:meth:`validate` and :py:meth:`compute`. Args: title (str): the name of the GPI :py:class:`Port` object Returns: Data from the :py:class:`Port` object. The return will have a type corresponding to a ``GPIType`` class allowed by this port. """ try: port = self.node.getPortByNumOrTitle(title) if isinstance(port, InPort): data = port.getUpstreamData() if type(data) is np.ndarray: # don't allow users to change original array attributes # that aren't protected by the 'writeable' flag buf = np.frombuffer(data.data, dtype=data.dtype) buf.shape = tuple(data.shape) return buf else: return data elif isinstance(port, OutPort): return port.data else: raise Exception("getData", "Invalid Port Title") except: raise GPIError_nodeAPI_getData('self.getData(\''+stw(title)+'\') failed in the node definition check the port name.') def getInPort(self, pnumORtitle): return self.node.getInPort(pnumORtitle) def getOutPort(self, pnumORtitle): return self.node.getOutPort(pnumORtitle) ############### DEPRECATED NODE API # TTD v0.3 def getAttr_fromWdg(self, title, attr): """title = (str) wdg-class name attr = (str) corresponds to the get_<arg> of the desired attribute. """ log.warn('The \'getAttr_fromWdg()\' function is deprecated, use \'getAttr()\' instead. '+str(self.node.getFullPath())) return self.getAttr(title, attr) def getVal_fromParm(self, title): """Returns get_val() from wdg-class (see getAttr()). """ log.warn('The \'getVal_fromParm()\' function is deprecated, use \'getVal()\' instead. '+str(self.node.getFullPath())) return self.getVal(title) def getData_fromPort(self, title): """title = (str) the name of the InPort. """ log.warn('The \'getData_fromPort()\' function is deprecated, use \'getData()\' instead. '+str(self.node.getFullPath())) return self.getData(title) def setData_ofPort(self, title, data): """title = (str) name of the OutPort to send the object reference. data = (object) any object corresponding to a GPIType class. """ log.warn('The \'setData_ofPort()\' function is deprecated, use \'setData()\' instead. '+str(self.node.getFullPath())) self.setData(title, data) def modifyWidget(self, title, **kwargs): """title = (str) the corresponding widget name. kwargs = args corresponding to the get_<arg> methods of the wdg-class. """ log.warn('The \'modifyWidget()\' function is deprecated, use \'setAttr()\' instead. '+str(self.node.getFullPath())) self.setAttr(title, **kwargs) def getEvent(self): """Allow node developer to get information about what event has caused the node to run.""" log.warn('The \'getEvent()\' function is deprecated, use \'getEvents()\' (its the plural form). '+str(self.node.getFullPath())) return self.node.getPendingEvent() def portEvent(self): """Specifically check for a port event.""" log.warn('The \'portEvent()\' function is deprecated, use \'portEvents()\' (its the plural form). '+str(self.node.getFullPath())) if GPI_PORT_EVENT in self.getEvent(): return self.getEvent()[GPI_PORT_EVENT] return None def widgetEvent(self): """Specifically check for a wdg event.""" log.warn('The \'widgetEvent()\' function is deprecated, use \'widgetEvents()\' (its the plural form). '+str(self.node.getFullPath())) if GPI_WIDGET_EVENT in self.getEvent(): return self.getEvent()[GPI_WIDGET_EVENT] return None ############### DEPRECATED NODE API def getEvents(self): """Get information about events that caused the node to run. Returns a dictionary containing names of widgets and ports that have changed values since the last time the node ran. Additionally contains information regarding ``init`` or ``requeue`` events trigered by GPI's node-evaluation routines. `Note: events from widget-ports count as both widget and port events.` Returns: dict: all events accumulated since the node was last run The event dictionary contains four key:value pairs: * ``GPI_WIDGET_EVENT`` : `set(widget_titles (string)`) * ``GPI_PORT_EVENT`` : `set(port_titles (string)`) * ``GPI_INIT_EVENT`` : ``True`` or ``False`` * ``GPI_REQUEUE_EVENT`` : ``True`` or ``False`` """ return self.node.getPendingEvents().events def portEvents(self): """Specifically check for port events. Get the names (unique identifier strings) of any ports that have changed data since the last run. `Note: events from widget-ports count as both widget and port events.` Returns: set: names (strings) of all ports modified since the node last ran """ return self.node.getPendingEvents().port def widgetEvents(self): """Specifically check for a widget events. Get the names (unique identifier strings) of any widgets that have changed data since the last run. `Note: events from widget-ports count as both widget and port events.` Returns: set: names (strings) of all widgets modified since the node last ran """ return self.node.getPendingEvents().widget def widgetMovingEvent(self, wdgid): """Called when a widget drag is being initiated. """ pass def getWidgetByID(self, wdgID): for parm in self.parmList: if parm.id() == wdgID: return parm log.critical("getWidgetByID(): Cannot find widget id:" + str(wdgID)) def getWidget(self, pnum): """Returns the widget desc handle and position number""" # fetch by widget number if type(pnum) is int: if (pnum < 0) or (pnum >= len(self.parmList)): log.error("getWidget(): Target widget out of range: " + str(pnum)) return src = self.parmList[pnum] # fetch by widget title elif type(pnum) is str: src = None cnt = 0 for parm in self.parmList: if parm.getTitle() == pnum: src = parm pnum = cnt # change pnum back to int cnt += 1 if src is None: log.error("getWidget(): Failed to find widget: \'" + stw(pnum) + "\'") return else: log.error("getWidget(): Widget identifier must be" + " either int or str") return return src def bufferParmSettings(self): """Get list of parms (dict) in self.parmSettings['parms']. Called by GPI_PROCESS functor to capture all widget settings needed in compute(). """ self.parmSettings = self.getSettings() def getWdgFromBuffer(self, title): # find a specific widget by title for wdg in self.parmSettings['parms']: if 'name' in wdg: if wdg['name'] == title: return wdg def getVal(self, title): """Returns the widget value. Each widget class has a corresponding "main" value. This method will return the value of the widget, by passing along the return from its `get_val()` method. Returns: The widget value. The type of the widget value is defined by the widget class. See :doc:`widgets` for value types for the built-in widget classes. """ try: # Fetch widget value by title if self.node.inDisabledState(): return if self.node.nodeCompute_thread.execType() == GPI_PROCESS: return self.getAttr(title, 'val') # threads and main loop can access directly return self.getAttr(title, 'val') except: print(str(traceback.format_exc())) raise GPIError_nodeAPI_getVal('self.getVal(\''+stw(title)+'\') failed in the node definition, check the widget name.') def getAttr(self, title, attr): """Get a specific attribute value from a widget. This returns the value of a specific attribute of a widget. Widget attributes may be modified by the user manipulating the Node Menu, during widget creation using the `kwargs` in :py:meth:`addWidget`, or programmatically by :py:meth:`setAttr`. Args: title (str): The widget name (unique identifier) attr (str): The desired attribute Returns: The desired widget attribute. This value is retrieved by calling ``get_<attr>`` on the indicated widget. See :doc:`widgets` for a list of attributes for the buil-in widget classes. """ try: # Fetch widget value by title if self.node.inDisabledState(): return if self.node.nodeCompute_thread.execType() == GPI_PROCESS: wdg = self.getWdgFromBuffer(title) return self._getAttr_fromWdg(wdg, attr) # threads and main loop can access directly wdg = self.getWidget(title) return self._getAttr_fromWdg(wdg, attr) except: print(str(traceback.format_exc())) raise GPIError_nodeAPI_getAttr('self.getAttr(\''+stw(title)+'\',...) failed in the node definition, check widget name and attribute name.') def _getAttr_fromWdg(self, wdg, attr): try: # input is either a dict or a wdg instance. if isinstance(wdg, dict): # buffered values if attr in wdg['kwargs']: return wdg['kwargs'][attr] else: # build error msg title = wdg['name'] funame = attr else: # direct widget access funame = 'get_' + attr if hasattr(wdg, funame): return getattr(wdg, funame)() else: try: title = wdg.getTitle() except: title = "\'no name\'" log.critical("_getAttr(): widget \'" + stw(title) + "\' has no attr \'" + stw(funame) + "\'.") #return None raise GPIError_nodeAPI_getAttr('_getAttr() failed for widget \''+stw(title)+'\'') except: #log.critical("_getAttr_fromWdg(): Likely the wrong input arg type.") #raise print(str(traceback.format_exc())) raise GPIError_nodeAPI_getAttr('_getAttr() failed for widget \''+stw(title)+'\'') def validate(self): """The pre-compute validation step. This function is intended to be reimplemented by the external node developer. Here the developer can access widget and port data (see :ref:`accessing-widgets` and :ref:`accessing-ports`) to perform validation checks before :py:meth:`compute` is called. Returns: An integer corresponding to the result of the validation: 0: The node successfully passed validation 1: The node failed validation, compute will not be called and the canvas will be paused """ log.debug("Default module validate().") return 0 def compute(self): """The module compute routine. This function is intended to be reimplemented by the external node developer. This is where the main computation of the node is performed. The developer has full access to the widget and port data (see :ref:`accessing-widgets` and :ref:`accessing-ports`). Returns: An integer corresponding to the result of the computation: 0: Compute completed successfully 1: Compute failed in some way, the canvas will be paused """ log.debug("Default module compute().") return 0 def post_compute_widget_update(self): # reset any widget that requires it (i.e. PushButton) for parm in self.parmList: if hasattr(parm, 'set_reset'): parm.set_reset()
class MatplotDisplay2(gpi.GenericWidgetGroup): valueChanged = gpi.Signal() def __init__(self, title, parent=None): super(MatplotDisplay2, self).__init__(title, parent) #self.data = self.get_data2() self._data = None self.create_main_frame() self.on_draw() # setters def set_val(self, data): '''Takes a list of npy arrays. ''' if isinstance(data, list): self._data = data self.on_draw() else: return # getters def get_val(self): return self._data # support def create_main_frame(self): self.fig = Figure((5.0, 4.0), dpi=100) self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self) self.canvas.setFocusPolicy(QtCore.Qt.StrongFocus) self.canvas.setFocus() self.mpl_toolbar = NavigationToolbar(self.canvas, self) self.canvas.mpl_connect('key_press_event', self.on_key_press) vbox = QtGui.QVBoxLayout() vbox.addWidget(self.canvas) # the matplotlib canvas vbox.addWidget(self.mpl_toolbar) self.setLayout(vbox) def get_data2(self): return np.arange(20).reshape([4, 5]).copy() def on_draw(self): self.fig.clear() self.axes = self.fig.add_subplot(111) # self.axes.plot(self.x, self.y, 'ro') # self.axes.imshow(self.data, interpolation='nearest') # self.axes.plot([1,2,3]) if self._data is None: return self.fig.hold(True) # plot each set # print "--------------------plot the data" for data in self._data: # check for x, y data if data.shape[-1] == 2: self.axes.plot(data[..., 0], data[..., 1], alpha=0.8, lw=2.0) else: self.axes.plot(data, alpha=0.8, lw=2.0) self.canvas.draw() def on_key_press(self, event): # print 'Matplotlib-> you pressed:' + str(event.key) # implement the default mpl key press events described at # http://matplotlib.org/users/navigation_toolbar.html#navigation- # keyboard-shortcuts try: from matplotlib.backend_bases import key_press_handler key_press_handler(event, self.canvas, self.mpl_toolbar) except: print("key_press_handler import failed. -old matplotlib version.")
class GPINodeQueue(QtCore.QObject): '''The list of nodes to process based on UI and hierarchy changes. ''' finished = gpi.Signal() def __init__(self, parent=None): super(GPINodeQueue, self).__init__(parent) self._queue = [] self._paused = False self._last_node_started = '-init-str-' def __str__(self): # stringify the status of the queue msg = "GPINodeQueue object:\n" msg += "\tis paused: "+str(self.isPaused())+"\n" msg += "\tis empty: "+str(self.isEmpty())+"\n" msg += "\tqueue len: "+str(self.getQueueLen())+"\n" msg += "\tlast node: "+str(self._last_node_started)+"\n" return msg def setPause(self, val): self._paused = val def isPaused(self): return self._paused def isEmpty(self): if len(self._queue) > 0: return False return True def put(self, node): if isinstance(node, Node): self._queue.append(node) def getQueueLen(self): return len(self._queue) def resetQueue(self): self._queue = [] def setQueue(self, nlist): self._queue = nlist def removeNode(self, node): # removes the first instance of given 'node' if node in self._queue: ind = self._queue.index(node) self._queue.pop(ind) log.debug("removeNode(): Node(" + node.name + \ "): Removed node from queue.") return True # SUCCESS else: log.debug("removeNode(): Node(" + node.name + \ "): This node was not found in the queue.") return False # FAILURE def startNextNode(self): if self.isPaused(): log.debug("startNextNode(): blocking for pause.") return 'paused' # find next node in queue if len(self._queue) > 0: while not self._queue[0].isReady(): self._queue.pop(0) if len(self._queue) == 0: break # if queue is done then finalize if len(self._queue) == 0: log.debug("startNextNode(): node queue empty, finished.") self.finished.emit() return 'finished' # run next node node = self._queue.pop(0) self._last_node_started = node.getName() if node.hasEventPending(): node.setEventStatus(None) log.debug("startNextNode(): node: "+node.getName()) node.start() return 'started'
class COMBINE_SPLIT_GROUP(gpi.GenericWidgetGroup): """Dimension specific widget to specify data reshaping""" valueChanged = gpi.Signal() def __init__(self, title, parent=None): super().__init__(title, parent) self._val = {} self._val['action'] = 0 self._val['dim_position'] = 1 # 0 - first, 1 - middle, 2 - last self._val['combine_dir'] = 0 self._val['split_prod'] = 1 self._val['split_val1'] = 1 self._val['split_val2'] = 1 # create exclusive push buttons to choose action self.action = gpi.ExclusivePushButtons('action') self.buttons = ['pass', 'combine', 'split'] self.action.set_buttons(self.buttons) # create exclusive push buttons to choose action self.combdir = gpi.ExclusivePushButtons('direction') self.buttons = [u'\u2B06', u'\u2B07'] self.combdir.set_buttons(self.buttons) self.combdir.set_visible(False) # create integer boxes for splitting a value self.sb1 = gpi.BasicSpinBox() self.sb1.set_min(1) self.sb1.set_val(1) self.sb1.set_max(gpi.GPI_INT_MAX) self.sb2 = gpi.BasicSpinBox() self.sb2.set_min(1) self.sb2.set_val(1) self.sb2.set_max(gpi.GPI_INT_MAX) self.set_sb_visible(False) self.action.valueChanged.connect(self.actionChange) self.combdir.valueChanged.connect(self.combdirChange) self.sb1.valueChanged.connect(self.splitChangeLeft) self.sb2.valueChanged.connect(self.splitChangeRight) hbox = QtWidgets.QHBoxLayout() hbox.addWidget(self.action) hbox.addWidget(self.combdir) hbox.addWidget(self.sb1) hbox.addWidget(self.sb2) hbox.setSpacing(0) self.setLayout(hbox) # setters def set_action(self, val): self._val['action'] = val self.action.set_val(val) self.actionChange() def set_dimpos(self, val): """A marker of the dimension position. 0-first, 1-middle, 2-last""" self._val['dim_position'] = val pos = self._val['dim_position'] if pos == 0: self.buttons = [u'\u2B07'] elif pos == 1: self.buttons = [u'\u2B06', u'\u2B07'] elif pos == 2: self.buttons = [u'\u2B06'] else: self.log.warn("Not a valid dimension position in widget of type "\ "COMBINE_SPLIT_GROUP. Must be between 0 and 2.") self.combdir.set_buttons(self.buttons) def set_split_prod(self, val): """set a new product of the split dimension.""" self._val['split_prod'] = val self.splitChangeRight() def set_sb_visible(self, val): self.sb1.setVisible(val) self.sb2.setVisible(val) # support def actionChange(self): self._val['action'] = self.action.get_val() if self._val['action'] == 0: self.combdir.set_visible(False) self.set_sb_visible(False) elif self._val['action'] == 1: self.combdir.set_visible(True) self.set_sb_visible(False) else: self.combdir.set_visible(False) self.set_sb_visible(True) self.valueChanged.emit() def combdirChange(self): self._val['combine_dir'] = self.combdir.get_val() self.valueChanged.emit() def splitChangeLeft(self): prod = self._val['split_prod'] val1 = self.sb1.get_val() old_val = self._val['split_val1'] new_val = val1 while (prod % val1 != 0 and val1 > 1 and val1 < prod): if new_val >= old_val: val1 = val1 + 1 else: val1 = val1 - 1 val2 = prod // val1 self._val['split_val1'] = val1 self._val['split_val2'] = val2 self.setSplitValsQuietly(self._val) self.valueChanged.emit() def splitChangeRight(self): prod = self._val['split_prod'] val2 = self.sb2.get_val() old_val = self._val['split_val2'] new_val = val2 while (prod % val2 != 0 and val2 > 1 and val2 < prod): if new_val > old_val: val2 = val2 + 1 else: val2 = val2 - 1 val1 = prod // val2 self._val['split_val1'] = val1 self._val['split_val2'] = val2 self.setSplitValsQuietly(self._val) self.valueChanged.emit() def setSplitValsQuietly(self, val): self.sb1.blockSignals(True) self.sb2.blockSignals(True) self.sb1.set_val(self._val['split_val1']) self.sb2.set_val(self._val['split_val2']) self.sb1.blockSignals(False) self.sb2.blockSignals(False) # getters def get_action(self): return self._val['action'] def get_combine_dir(self): return self._val['combine_dir'] def get_split_dims(self): """return split dims as a list""" dims = [] dims.append(self._val['split_val1']) dims.append(self._val['split_val2']) return dims
class GPIFunctor(QtCore.QObject): '''A common parent API for each execution type (i.e. ATask, PTask, TTask). Handles the data communications to and from each task type. ''' finished = gpi.Signal(int) terminated = gpi.Signal() applyQueuedData_finished = gpi.Signal() _setData_finished = gpi.Signal() def __init__(self, node, parent=None): super(GPIFunctor, self).__init__(parent) self._node = node self._title = node.name self._func = node.getModuleCompute() self._validate = node.getModuleValidate() self._retcode = None self._validate_retcode = None # for applying data when a GPI_PROCESS is finished # this is done in a thread to keep the GUI responsive self._applyData_thread = None self._setData_finished.connect(self.applyQueuedData_finalMatter) self.applyQueuedData_finished.connect(self.finalMatter) self._ap_st_time = 0 # flag for segmented types that need reconstitution on this side self._segmentedDataProxy = False # For Windows just make them all apploops for now to be safe self._execType = node._nodeIF.execType() if Specs.inWindows() and (self._execType == GPI_PROCESS): # if (self._execType == GPI_PROCESS): log.info( "init(): <<< WINDOWS Detected >>> Forcing GPI_PROCESS -> GPI_THREAD" ) self._execType = GPI_THREAD # self._execType = GPI_APPLOOP self._label = node._nodeIF.getLabel() self._isTerminated = False self._compute_start = 0 self._manager = None self._proxy = None self._proc = None if self._execType == GPI_PROCESS: log.debug("init(): set as GPI_PROCESS: " + str(self._title)) self._manager = multiprocessing_context.Manager() self._proxy = self._manager.list() self._proc = PTask(self._func, self._title, self._label, self._proxy) # apply data in a thread to make the GUI more responsive self._applyData_thread = GPIRunnable(self.applyQueuedData_setData) elif self._execType == GPI_THREAD: log.debug("init(): set as GPI_THREAD: " + str(self._title)) self._proc = TTask(self._func, self._title, self._label, self._proxy) else: # default to GPI_APPLOOP log.debug("init(): set as GPI_APPLOOP: " + str(self._title)) self._proc = ATask(self._func, self._title, self._label, self._proxy) self._proc.finished.connect(self.computeFinished) # In Qt5, terminated was removed: its emission wasn't gauranteed # self._proc.terminated.connect(self.computeTerminated) def execType(self): return self._execType def terminate(self): self._isTerminated = True self.cleanup() self._proc.terminate() # self.wait() # so that something is waiting self.computeTerminated() def cleanup(self): # make sure the proxy manager for processes is shutdown. if self._manager: self._manager.shutdown() # try to minimize leftover memory from the segmented array transfers # force cleanup of mmap #if self._segmentedDataProxy: gc.collect() def curTime(self): return time.time() - self._compute_start def start(self): self._compute_start = time.time() # VALIDATE # temporarily trick all widget calls to use GPI_APPLOOP for validate() tmp_exec = self._execType self._execType = GPI_APPLOOP try: self._validate_retcode = self._validate() except: self._validate_retcode = 1 # validate error self._execType = tmp_exec # None as zero # send validate() return code thru same channels if self._validate_retcode != 0 and self._validate_retcode is not None: self._node.appendWallTime(time.time() - self._compute_start) self.finished.emit(1) # validate error return # COMPUTE if self._execType == GPI_PROCESS: log.debug("start(): buffer process parms") self._node._nodeIF.bufferParmSettings() # keep objects on death-row from being copied into processes # before they've finally terminated. -otherwise they'll try # and terminate within child process and cause a fork error. log.debug('start(): garbage collect before spawning GPI_PROCESS') gc.collect() log.debug("start(): call task.start()") self._proc.start() def wait(self): self._proc.wait() def isRunning(self): return self._proc.isRunning() def returnCode(self): return self._retcode # GPI_PROCESS support def addToQueue(self, item): # add internal calls (port, widget, retcode...) # to a queue that is processed after compute() self._proc._proxy.append(item) def computeTerminated(self): self.terminated.emit() def computeFinished(self): if self._execType == GPI_PROCESS: self.applyQueuedData() else: self._retcode = 0 # success if Return.isError(self._proc._retcode): self._retcode = Return.ComputeError self.finalMatter() def finalMatter(self): log.info("computeFinished():Node \'" + str(self._title) + "\': compute time:" + str(time.time() - self._compute_start) + " sec.") self._node.appendWallTime(time.time() - self._compute_start) self.finished.emit(self._retcode) # success def applyQueuedData_setData(self): for o in self._proxy: try: log.debug("applyQueuedData_setData(): apply object " + str(o[0]) + ', ' + str(o[1])) if o[0] == 'setData': # DataProxy is used for complex data types like numpy if type(o[2]) is DataProxy: # segmented types must be gathered before reassembly if o[2].isSegmented(): log.debug("seg proxy is True") self._segmentedDataProxy = True else: log.debug("o[2].getData()") self._node.setData(o[1], o[2].getData()) # all other simple types get set directly else: log.debug("direct setData()") self._node.setData(o[1], o[2]) except: log.error("applyQueuedData() failed. " + str(traceback.format_exc())) self._retcode = Return.ComputeError self._setData_finished.emit() # Assemble Segmented Data if self._segmentedDataProxy: log.warn("Using segmented data proxy...") # group all segmented types oportData = [ o for o in self._proxy if (o[0] == 'setData') and (type(o[2]) is DataProxy) ] # take only those that are segmented oportData = [o for o in oportData if o[2].isSegmented()] # consolidate all outports with large data largeports = set([o[1] for o in oportData]) for port in largeports: log.info( "applyQueuedData(): ------ APPENDING SEGMENTED PROXY OBJECTS" ) # gather port segs curport = [o for o in oportData if o[1] == port] # gather all DataProxy segs segs = [o[2] for o in curport] buf = DataProxy().getDataFromSegments(segs) # if the pieces fail to assemble move on if buf is None: log.warn( "applyQueuedData(): segmented proxy object failed to assemble, skipping..." ) continue self._node.setData(port, buf) # run self.applyQueuedData_finalMatter() self._setData_finished.emit() def applyQueuedData(self): # Replay all external compute() events after execution. # This ensures that the target is not being used by another set method. self._ap_st_time = time.time() if self._isTerminated: return log.debug("applyQueuedData(): Sending data to main loop...") if len(self._proxy) == 0: log.debug( "applyQueuedData(): no data in output queue. Terminated.") self.computeTerminated() return self._segmentedDataProxy = False for o in self._proxy: try: log.debug("applyQueuedData(): apply object " + str(o[0]) + ', ' + str(o[1])) if o[0] == 'retcode': self._retcode = o[1] if Return.isError(self._retcode): self._retcode = Return.ComputeError else: self._retcode = 0 # squash Nones if o[0] == 'modifyWdg': self._node.modifyWdg(o[1], o[2]) if o[0] == 'setReQueue': self._node.setReQueue(o[1]) except: log.error("applyQueuedData() failed. " + str(traceback.format_exc())) self._retcode = Return.ComputeError # transfer all setData() calls to a thread log.debug("applyQueuedData(): run _applyData_thread") ExecRunnable(self._applyData_thread) def applyQueuedData_finalMatter(self): if Return.isComputeError(self._retcode): self.finished.emit(self._retcode) elapsed = (time.time() - self._ap_st_time) log.info("applyQueuedData(): time (total queue): " + str(elapsed) + " sec") # shutdown the proxy manager self.cleanup() # start self.finalMatter self.applyQueuedData_finished.emit()
class AddBlockWidgets(gpi.GenericWidgetGroup): """A unique widget that display a variable number of StringBoxes (or FileBrowser) depending on the Event being configured.""" valueChanged = gpi.Signal() def __init__(self, title, parent=None): super(AddBlockWidgets, self).__init__(title, parent) self.button_names_list = [ 'Off', 'Delay', 'SincRF', 'BlockRF', 'G', 'GyPre', 'ArbGrad', 'ADC' ] self.clicked_button_name, self.clicked_button_index = '', 0 self.buttons_list, self.string_box_list = [], [] # Labels for StringBoxes to configure Events self.delay_labels = ['Unique Event name', 'Delay (s)'] self.sinc_rf_labels = [ 'Unique Event name', 'Maximum Gradient (mT/m)', 'Maximum Slew (T/m/s)', 'Flip Angle (deg)', 'Duration (s)', 'Frequency Offset', 'Phase Offset', 'Time Bw Product', 'Apodization', 'Slice Thickness (m)' ] self.block_rf_labels = [ 'Unique Event name', 'Maximum Gradient (mT/m)', 'Maximum Slew (T/m/s)', 'Flip Angle (deg)', 'Duration (s)', 'Frequency Offset', 'Phase Offset', 'Time Bw Product', 'Bandwidth', 'Slice Thickness (m)' ] self.trap_labels = [ 'Unique Event name', 'Channel', 'Maximum Gradient (mT/m)', 'Maximum Slew (T/m/s)', 'Duration (s)', 'Area', 'Flat Time (s)', 'Flat Area', 'Amplitude (Hz)', 'Rise Time (s)' ] self.gy_pre_labels = ['Unique Event name', 'Duration (s)', 'Area'] self.arb_grad_labels = [ 'Unique Event name', 'Channel', 'Maximum Gradient (mT/m)', 'Maximum Slew (T/m/s)' ] self.adc_labels = [ 'Unique Event name', 'Number of samples', 'Dwell (s)', 'Duration (s)', 'Delay (s)', 'Frequency Offset', 'Phase Offset' ] # Placeholders for StringBoxes to configure Events self.delay_placeholders = ['event_unique_name', 'delay'] self.sinc_rf_placeholders = [ 'event_unique_name', 'max_grad', 'max_slew', 'flip_angle', 'duration', 'freq_offset', 'phase_offset', 'time_bw_prod', 'apodization', 'slice_thickness' ] self.block_rf_placeholders = [ 'event_unique_name', 'max_grad', 'max_slew', 'flip_angle', 'duration', 'freq_offset', 'phase_offset', 'time_bw_prod', 'bandwidth', 'slice_thickness' ] self.trap_placeholders = [ 'event_unique_name', 'channel', 'max_grad', 'max_slew', 'duration', 'area', 'flat_time', 'flat_area', 'amplitude', 'rise_time' ] self.gy_pre_placeholders = ['event_unique_name', 'duration', 'area'] self.arb_grad_placeholders = [ 'event_unique_name', 'channel', 'max_grad', 'max_slew' ] self.adc_placeholders = [ 'event_unique_name', 'num_samples', 'dwell', 'duration', 'delay', 'freq_offset', 'phase_offset' ] # Variable to denote the maximum number of StringBoxes to be added; obviously depends on the Event which has the # maximum number of configuration parameters self.num_string_boxes = max(len(self.delay_labels), len(self.sinc_rf_labels), len(self.block_rf_labels), len(self.trap_labels), len(self.gy_pre_labels), len(self.arb_grad_labels), len(self.adc_labels)) # First index is None because the first button is 'Off'. Look into event_def['event_values'] in get_val() self.labels = [ None, self.delay_labels, self.sinc_rf_labels, self.block_rf_labels, self.trap_labels, self.gy_pre_labels, self.arb_grad_labels, self.adc_labels ] self.placeholders = [ None, self.delay_placeholders, self.sinc_rf_placeholders, self.block_rf_placeholders, self.trap_placeholders, self.gy_pre_placeholders, self.arb_grad_placeholders, self.adc_placeholders ] self.wdg_layout = QtGui.QGridLayout() self.add_event_pushbuttons() self.add_config_stringboxes() self.add_include_in_loop_pushbutton() self.add_include_gz_pushbutton() self.add_file_browser() self.setLayout(self.wdg_layout) self.buttons_list[0].setChecked(True) def add_event_pushbuttons(self): """Adding PushButtons for the Events.""" col_count = 0 for name in self.button_names_list: new_button = QtGui.QPushButton(name) new_button.setCheckable(True) new_button.setAutoExclusive(True) new_button.clicked.connect(self.button_clicked) new_button.clicked.connect(self.valueChanged) # Syntax: addWidget(widget, row, col, rowSpan, colSpan) self.wdg_layout.addWidget(new_button, 0, col_count, 1, 1) self.buttons_list.append(new_button) col_count += 1 def add_config_stringboxes(self): """Adding StringBoxes for configuring the Events.""" for x in range(self.num_string_boxes): string_box = gpi.StringBox(str(x)) string_box.set_visible(False) # Syntax: addWidget(widget, row, col, rowSpan, colSpan) self.wdg_layout.addWidget(string_box, x + 1, 1, 1, 6) self.string_box_list.append(string_box) def add_include_in_loop_pushbutton(self): """Adding PushButton to toggle Event being included/excluded in loop.""" self.include_in_loop_pushbutton = QtGui.QPushButton( 'Add event in loop') self.include_in_loop_pushbutton.setCheckable(True) self.include_in_loop_pushbutton.setChecked(True) self.include_in_loop_pushbutton.setVisible(False) self.include_in_loop_pushbutton.clicked.connect(self.button_clicked) self.include_in_loop_pushbutton.clicked.connect(self.valueChanged) # Syntax: addWidget(widget, row, col, rowSpan, colSpan) self.wdg_layout.addWidget(self.include_in_loop_pushbutton, 11, 1, 1, 6) def add_include_gz_pushbutton(self): """Adding PushButton toggle for Gz along with Rf.""" self.include_gz_pushbutton = QtGui.QPushButton('Add Gz event with Rf') self.include_gz_pushbutton.setCheckable(True) self.include_gz_pushbutton.setVisible(False) self.include_gz_pushbutton.clicked.connect(self.button_clicked) self.include_gz_pushbutton.clicked.connect(self.valueChanged) # Syntax: addWidget(widget, row, col, rowSpan, colSpan) self.wdg_layout.addWidget(self.include_gz_pushbutton, 12, 1, 1, 6) def add_file_browser(self): """Adding FileBrowser necessary for ArbGrad Event.""" self.file_browser = gpi.OpenFileBrowser('Read .hdf5') self.file_browser.set_button_title('Read .hdf5') self.file_browser.set_visible(False) # Syntax: addWidget(widget, row, col, rowSpan, colSpan) self.wdg_layout.addWidget(self.file_browser, 5, 1, 1, 6) # Getter def get_val(self): if self.clicked_button_index == self.button_names_list.index('Off'): # 'Off' PushButton selected, return empty dict return {} else: """ event_def contains: - event_name : str Event name, corresponds to Event button that is selected - event_unique_name : str Unique Event name; user input - event_values : OrderedDict key-value pairs of Event parameters_params and values - include_in_loop : bool If Event should be added to Sequence Ny times - include_gz : bool If Gz Event should be added to Sequence with Rf Event - file_path : str Path to .hdf5 file required for arbitrary gradient Event """ event_def = dict() keys = self.placeholders[self.clicked_button_index][1:] values = [x.get_val() for x in self.string_box_list[1:]] for x in values: if x == '': values[values.index(x)] = '0' event_def['event_values'] = OrderedDict(zip(keys, values)) event_def['event_name'] = self.clicked_button_name event_def['event_unique_name'] = self.string_box_list[0].get_val() if event_def['event_unique_name'] == '': # Return None to raise an error in compute() return None event_def[ 'include_in_loop'] = self.include_in_loop_pushbutton.isChecked( ) if self.clicked_button_index == 2: # For Rf event, check if Gz has to be included event_def['include_gz'] = self.include_gz_pushbutton.isChecked( ) elif self.clicked_button_index == 6: # For arbitrary gradient event, retrieve file path event_def['file_path'] = self.file_browser.get_val() return event_def # Setter def set_val(self, val): self.hide_config_widgets() if len(val) != 0: event_values = val['event_values'] event_values['event_unique_name'] = val['event_unique_name'] self.clicked_button_name = val['event_name'] self.clicked_button_index = self.button_names_list.index( self.clicked_button_name) self.buttons_list[self.clicked_button_index].setChecked(True) self.show_config_widgets() self.include_in_loop_pushbutton.setChecked( bool(val['include_in_loop'])) if 'include_gz' in val: self.include_gz_pushbutton.setChecked(val['include_gz']) labels = self.labels[self.clicked_button_index] placeholders = self.placeholders[self.clicked_button_index] for x in range(len(placeholders)): self.string_box_list[x].setTitle(labels[x]) self.string_box_list[x].set_placeholder(placeholders[x]) self.string_box_list[x].set_val(event_values[placeholders[x]]) def button_clicked(self): """Identifies the button that was clicked and stores the name and ID of the button.""" for button in self.buttons_list: if button.isChecked(): self.clicked_button_index = self.buttons_list.index(button) self.clicked_button_name = self.button_names_list[ self.clicked_button_index] self.show_config_widgets() def show_config_widgets(self): """Show appropriate number of StringBoxes and relevant Widgets based on the button that was clicked.""" self.hide_config_widgets() if self.clicked_button_index != 0 and self.clicked_button_index != 5: self.include_in_loop_pushbutton.setVisible(True) if self.clicked_button_index == 1: # Delay [ self.string_box_list[x].set_visible(True) for x in range(len(self.delay_placeholders)) ] [ self.string_box_list[x].setTitle(self.delay_labels[x]) for x in range(len(self.delay_labels)) ] [ self.string_box_list[x].set_placeholder( self.delay_placeholders[x]) for x in range(len(self.delay_placeholders)) ] elif self.clicked_button_index == 2: # SincRF [ self.string_box_list[x].set_visible(True) for x in range(len(self.sinc_rf_placeholders)) ] [ self.string_box_list[x].setTitle(self.sinc_rf_labels[x]) for x in range(len(self.sinc_rf_labels)) ] [ self.string_box_list[x].set_placeholder( self.sinc_rf_placeholders[x]) for x in range(len(self.sinc_rf_placeholders)) ] self.include_gz_pushbutton.setVisible(True) elif self.clicked_button_index == 3: # BlockRF [ self.string_box_list[x].set_visible(True) for x in range(len(self.block_rf_placeholders)) ] [ self.string_box_list[x].setTitle(self.block_rf_labels[x]) for x in range(len(self.block_rf_labels)) ] [ self.string_box_list[x].set_placeholder( self.block_rf_placeholders[x]) for x in range(len(self.block_rf_placeholders)) ] self.include_gz_pushbutton.setVisible(True) elif self.clicked_button_index == 3: # G [ self.string_box_list[x].set_visible(True) for x in range(len(self.trap_placeholders)) ] [ self.string_box_list[x].setTitle(self.trap_labels[x]) for x in range(len(self.trap_labels)) ] [ self.string_box_list[x].set_placeholder( self.trap_placeholders[x]) for x in range(len(self.trap_placeholders)) ] elif self.clicked_button_index == 4: # G [ self.string_box_list[x].set_visible(True) for x in range(len(self.trap_placeholders)) ] [ self.string_box_list[x].setTitle(self.trap_labels[x]) for x in range(len(self.trap_labels)) ] [ self.string_box_list[x].set_placeholder( self.trap_placeholders[x]) for x in range(len(self.trap_placeholders)) ] elif self.clicked_button_index == 5: # GyPre [ self.string_box_list[x].set_visible(True) for x in range(len(self.gy_pre_placeholders)) ] [ self.string_box_list[x].setTitle(self.gy_pre_labels[x]) for x in range(len(self.gy_pre_labels)) ] [ self.string_box_list[x].set_placeholder( self.gy_pre_placeholders[x]) for x in range(len(self.gy_pre_placeholders)) ] elif self.clicked_button_index == 6: # Arbitrary Grad [ self.string_box_list[x].set_visible(True) for x in range(len(self.arb_grad_placeholders)) ] [ self.string_box_list[x].setTitle(self.arb_grad_labels[x]) for x in range(len(self.arb_grad_labels)) ] [ self.string_box_list[x].set_placeholder( self.arb_grad_placeholders[x]) for x in range(len(self.arb_grad_placeholders)) ] self.file_browser.set_visible(True) elif self.clicked_button_index == 7: # ADC [ self.string_box_list[x].set_visible(True) for x in range(len(self.adc_placeholders)) ] [ self.string_box_list[x].setTitle(self.adc_labels[x]) for x in range(len(self.adc_labels)) ] [ self.string_box_list[x].set_placeholder( self.adc_placeholders[x]) for x in range(len(self.adc_placeholders)) ] def hide_config_widgets(self): """Hide all Widgets.""" [x.set_visible(False) for x in self.string_box_list] [x.set_val("") for x in self.string_box_list] self.include_in_loop_pushbutton.setVisible(False) self.include_gz_pushbutton.setVisible(False) self.file_browser.set_visible(False)
class OpenGLWindow(gpi.GenericWidgetGroup): """Provides an embedded GL window. """ valueChanged = gpi.Signal() def __init__(self, title, parent=None): super(OpenGLWindow, self).__init__(title, parent) f = QtOpenGL.QGLFormat() f.setAccum(True) f.setDoubleBuffer(True) f.setRgba(True) f.setDepth(True) f.setAlpha(True) self.glWidget = GPIGLWidget(f) self.glWidgetArea = QtGui.QScrollArea() self.glWidgetArea.setWidget(self.glWidget) self.glWidgetArea.setWidgetResizable(True) self.glWidgetArea.setHorizontalScrollBarPolicy( QtCore.Qt.ScrollBarAlwaysOff) self.glWidgetArea.setVerticalScrollBarPolicy( QtCore.Qt.ScrollBarAlwaysOff) self.glWidgetArea.setSizePolicy(QtGui.QSizePolicy.Ignored, QtGui.QSizePolicy.Ignored) self.glWidgetArea.setMinimumSize(50, 50) # self.pixmapLabelArea = QtGui.QScrollArea() # self.pixmapLabelArea.setWidget(self.pixmapLabel) # self.pixmapLabelArea.setSizePolicy(QtGui.QSizePolicy.Ignored, # QtGui.QSizePolicy.Ignored) # self.pixmapLabelArea.setMinimumSize(50, 50) xSlider = self.createSlider(self.glWidget.xRotationChanged, self.glWidget.setXRotation) ySlider = self.createSlider(self.glWidget.yRotationChanged, self.glWidget.setYRotation) zSlider = self.createSlider(self.glWidget.zRotationChanged, self.glWidget.setZRotation) glBlend = self.createCheckOption('PolySmooth', self.glWidget.setBlend) polyFill = self.createCheckOption( 'Poly-Fill/Line/Point', self.glWidget.setPolyFill, tristate=True) # antialiasing requires the accumulation buffer enableaccum = True if not self.glWidget.format().accum(): enableaccum = False antialiasing = self.createCheckOption( 'AntiAliasing', self.glWidget.setAntiAliasing, initstate=0, enabled=enableaccum) hardwareRender = QtGui.QLabel() hardwareRender.setFrameStyle(2) if self.glWidget.format().directRendering(): hardwareRender.setText('Rendering: Hardware') else: hardwareRender.setText('Rendering: Software') #testOption = self.createCheckOption( # 'Test Option', self.glWidget.setTest, tristate=False) # self.createActions() # self.createMenus() centralLayout = QtGui.QGridLayout() centralLayout.addWidget(self.glWidgetArea, 2, 0, 4, 4) # centralLayout.setColumnStretch(0,3) centralLayout.setRowStretch(2, 2) # centralLayout.addWidget(self.pixmapLabelArea, 0, 1) #centralLayout.addWidget(xSlider, 2, 0, 1, 2) #centralLayout.addWidget(ySlider, 3, 0, 1, 2) #centralLayout.addWidget(zSlider, 4, 0, 1, 2) centralLayout.addWidget(polyFill, 1, 0, 1, 1) centralLayout.addWidget(glBlend, 1, 1, 1, 1) centralLayout.addWidget(antialiasing, 1, 2, 1, 1) centralLayout.addWidget(hardwareRender, 0, 0, 1, 1) #centralLayout.addWidget(testOption, 0, 2, 1, 1) self.setLayout(centralLayout) xSlider.setValue(15 * 16) ySlider.setValue(345 * 16) zSlider.setValue(0 * 16) # self.setWindowTitle("Grabber") self.resize(400, 300) # setters def set_val(self, val): """set a list of GL command list interface (dict).""" self.glWidget.setGPIglList(val) self.glWidget.glInit() self.glWidget.updateGL() def set_resetView(self, val): """reset the viewing window""" self.glWidget.resetViewingWindow() def set_imageARGB(self, val): pass def set_imageBGRA(self, val): pass # getters def get_val(self): """get the held list of GPI-GL objects""" return self.glWidget.getGPIglList() def get_resetView(self): '''This is a one-shot operation, so there is nothing to get''' pass def get_imageARGB(self): '''Render a copy of the GL window and convert to NPY array. ''' fbuff = self.glWidget.grabFrameBuffer(withAlpha=True) arr = qimage2numpy(fbuff) arr = arr[..., ::-1] return arr def get_imageBGRA(self): '''Render a copy of the GL window and convert to NPY array. BGRA is the fastest since it native. ''' fbuff = self.glWidget.grabFrameBuffer(withAlpha=True) arr = qimage2numpy(fbuff) return arr # support def renderIntoPixmap(self): size = self.getSize() if size.isValid(): pixmap = self.glWidget.renderPixmap(size.width(), size.height()) self.setPixmap(pixmap) def grabFrameBuffer(self): image = self.glWidget.grabFrameBuffer() self.setPixmap(QtGui.QPixmap.fromImage(image)) def clearPixmap(self): self.setPixmap(QtGui.QPixmap()) def about(self): QtGui.QMessageBox.about(self, "About Grabber", "The <b>Grabber</b> example demonstrates two approaches for " "rendering OpenGL into a Qt pixmap.") def createActions(self): self.renderIntoPixmapAct = QtGui.QAction("&Render into Pixmap...", self, shortcut="Ctrl+R", triggered=self.renderIntoPixmap) self.grabFrameBufferAct = QtGui.QAction("&Grab Frame Buffer", self, shortcut="Ctrl+G", triggered=self.grabFrameBuffer) self.clearPixmapAct = QtGui.QAction("&Clear Pixmap", self, shortcut="Ctrl+L", triggered=self.clearPixmap) self.exitAct = QtGui.QAction("E&xit", self, shortcut="Ctrl+Q", triggered=self.close) self.aboutAct = QtGui.QAction("&About", self, triggered=self.about) self.aboutQtAct = QtGui.QAction("About &Qt", self, triggered=QtGui.qApp.aboutQt) def createMenus(self): self.fileMenu = self.menuBar().addMenu("&File") self.fileMenu.addAction(self.renderIntoPixmapAct) self.fileMenu.addAction(self.grabFrameBufferAct) self.fileMenu.addAction(self.clearPixmapAct) self.fileMenu.addSeparator() self.fileMenu.addAction(self.exitAct) self.helpMenu = self.menuBar().addMenu("&Help") self.helpMenu.addAction(self.aboutAct) self.helpMenu.addAction(self.aboutQtAct) def createCheckOption(self, title, setterSlot, tristate=False, initstate=0, enabled=True): checkbox = QtGui.QCheckBox(title) checkbox.setTristate(tristate) checkbox.setCheckState(initstate) checkbox.stateChanged.connect(setterSlot) if not enabled: checkbox.setEnabled(False) return checkbox def createSlider(self, changedSignal, setterSlot): slider = QtGui.QSlider(QtCore.Qt.Horizontal) slider.setRange(0, 360 * 16) slider.setSingleStep(16) slider.setPageStep(15 * 16) slider.setTickInterval(15 * 16) slider.setTickPosition(QtGui.QSlider.TicksRight) slider.valueChanged.connect(setterSlot) changedSignal.connect(slider.setValue) return slider def setPixmap(self, pixmap): self.pixmapLabel.setPixmap(pixmap) size = pixmap.size() if size - QtCore.QSize(1, 0) == self.pixmapLabelArea.maximumViewportSize(): size -= QtCore.QSize(1, 0) self.pixmapLabel.resize(size) def getSize(self): text, ok = QtGui.QInputDialog.getText(self, "Grabber", "Enter pixmap size:", QtGui.QLineEdit.Normal, "%d x %d" % (self.glWidget.width(), self.glWidget.height())) if not ok: return QtCore.QSize() regExp = QtCore.QRegExp("([0-9]+) *x *([0-9]+)") if regExp.exactMatch(text): width = regExp.cap(0).toInt() height = regExp.cap(1).toInt() if width > 0 and width < 2048 and height > 0 and height < 2048: return QtCore.QSize(width, height) return self.glWidget.size()
class SpynAxys(gpi.GenericWidgetGroup): """A combination of SpinBoxes, DoubleSpinBoxes, and PushButtons to form a unique widget suitable for the Spin Generator Axes. """ valueChanged = gpi.Signal() def __init__(self, title, parent=None): super().__init__(title, parent) self._val = {} self._val['length'] = 1 self._val['start'] = 0. self._val['end'] = 0. # the original array length self.sb = gpi.BasicSpinBox() # length self.sb.set_label('# Spins:') self.sb.set_min(1) self.sb.set_val(1) self.sb.set_max(gpi.GPI_INT_MAX) self.db1 = gpi.BasicDoubleSpinBox() # start self.db1.set_label('Start:') self.db1.set_min(gpi.GPI_FLOAT_MIN) self.db1.set_max(gpi.GPI_FLOAT_MAX) self.db1.set_decimals(3) self.db1.set_singlestep(1.) self.db1.set_val(0) self.db2 = gpi.BasicDoubleSpinBox() # end self.db2.set_label('End:') self.db2.set_min(gpi.GPI_FLOAT_MIN) self.db2.set_max(gpi.GPI_FLOAT_MAX) self.db2.set_decimals(3) self.db2.set_singlestep(1.) self.db2.set_val(0) self.sb.valueChanged.connect(self.lenChange) self.db1.valueChanged.connect(self.startChange) self.db2.valueChanged.connect(self.endChange) vbox = QtWidgets.QHBoxLayout() vbox.addWidget(self.sb) vbox.addWidget(self.db1) vbox.addWidget(self.db2) vbox.setStretch(0, 0) vbox.setStretch(1, 0) vbox.setStretch(2, 0) vbox.setContentsMargins(0, 0, 0, 0) # we don't need margins here vbox.setSpacing(0) self.setLayout(vbox) # setters def set_val(self, val): """A python-dict containing: size, and width parms. """ sig = False if 'length' in val: self._val['length'] = val['length'] self.setLenQuietly(val['length']) sig = True if 'start' in val: self._val['start'] = val['start'] self.setStartQuietly(val['start']) sig = True if 'end' in val: self._val['end'] = val['end'] self.setEndQuietly(val['end']) sig = True if sig: self.valueChanged.emit() # getters def get_val(self): return self._val # support def lenChange(self, val): self._val['length'] = val self.setLenQuietly(self._val['length']) self.valueChanged.emit() def startChange(self, val): self._val['start'] = val self.setStartQuietly(self._val['start']) self.valueChanged.emit() def endChange(self, val): self._val['end'] = val self.setEndQuietly(self._val['end']) self.valueChanged.emit() def setLenQuietly(self, val): self.sb.blockSignals(True) self.sb.set_val(val) self.sb.blockSignals(False) def setStartQuietly(self, val): self.db1.blockSignals(True) self.db1.set_val(val) self.db1.blockSignals(False) def setEndQuietly(self, val): self.db2.blockSignals(True) self.db2.set_val(val) self.db2.blockSignals(False)
class LayoutWindow(QtWidgets.QFrame): '''This class controls the low level operations on "Layout Windows". It handles the drag and drop events for pulling widgets out of "Node Menus". It also handles low-level serialization. ''' changed = gpi.Signal() def __init__(self, graph, layoutType, label, parent=None): super(LayoutWindow, self).__init__(parent) self.setAcceptDrops(True) self._graph = graph self.setLabel(label) self.setLayoutType(layoutType) # keep track of child order since Qt doesn't self._wdgidList = [] def setLayoutType(self, typ): '''The basic layout is VBox or HBox. ''' self._layoutType = typ if typ == 'vbox': self._layout = QtWidgets.QVBoxLayout() elif typ == 'hbox': self._layout = QtWidgets.QHBoxLayout() def setLabel(self, lab): '''This label allows the layoutWindow to be placed in the correct place within the MasterLayout. ''' self._label = lab def pushWdgID(self, wdgid): '''Push a newly dropped widget to the end of the list. ''' self._wdgidList.append(wdgid) def widgetMovingEvent(self, wdgid): '''If a drag is initiated the widget will always be moved. --see the widget drag event for details in widgets.py. ''' self._wdgidList.remove(wdgid) def label(self): return self._label def dragEnterEvent(self, event): if event.mimeData().hasFormat('application/gpi-widget'): # event.acceptProposedAction() event.accept() def dragMoveEvent(self, event): if event.mimeData().hasFormat('application/gpi-widget'): # event.acceptProposedAction() event.accept() def dropEvent(self, event): log.debug("dropped in test window") if event.mimeData().hasFormat('application/gpi-widget'): mime = event.mimeData() itemData = mime.data('application/gpi-widget') dataStream = QtCore.QDataStream(itemData, QtCore.QIODevice.ReadOnly) text = QtCore.QByteArray() offset = QtCore.QPoint() dataStream >> text >> offset if self.addWidgetByID(self._graph.getAllNodes(), int(text)): # event.acceptProposedAction() event.accept() else: event.ignore() def dragLeaveEvent(self, event): event.accept() def addWidgetByID(self, nodeList, wdgid): '''Only search for widgets within the nodeList. ''' parm = self._graph.findWidgetByID(nodeList, wdgid) if parm is None: log.error("LayoutWindow(): addWidget failed, wdgid not found!!!") return False # save the actual id self.pushWdgID(id(parm)) self._layout.addWidget(parm) self.setLayout(self._layout) parm.setDispTitle() # parent changed self.changed.emit() return True def count(self): return len(self._wdgidList) def getSettings(self): '''Get widget id's from this layout. NOTE: widgets are not stored by the QObject in any particular order. ''' s = {} s['label'] = self.label() s['layoutType'] = self._layoutType s['wdgids'] = self._wdgidList return s def loadSettings(self, s, nodeList): '''Given the information generated by getSettings(), load the corresponding widgets. Searches for widgets within the supplied nodeList. ''' self.setLayoutType(s['layoutType']) for wdg in s['wdgids']: log.debug("\t adding wdgid: " + str(wdg)) self.blockSignals(True) # keep from calling columnAdjust() self.addWidgetByID(nodeList, int(wdg)) self.blockSignals(False) def getWdgByID(self, wdgid): for wdg in self.getGPIWidgets(): if wdgid == wdg.get_id(): return wdg def getGPIWidgets(self): '''Search all children for valid GPI widget objects and list them. ''' gpichilds = [] for wdg in self.children(): if isWidget(wdg): gpichilds.append(wdg) return gpichilds
class SHAPES_GROUP(gpi.GenericWidgetGroup): """A combination of Sliders and SpinBoxes to form a unique widget suitable for defining the filter shape. """ valueChanged = gpi.Signal() def __init__(self, title, parent=None): super().__init__(title, parent) self._val = {} self._val['size'] = 1 self._val['width'] = 1 self.sb = gpi.BasicSpinBox() # size self.sb.set_label('size:') self.sb.set_min(1) self.sb.set_val(1) self.sb.set_max(gpi.GPI_INT_MAX) self.slb = gpi.BasicSlider() # width self.slb.set_min(1) self.slb.set_val(1) self.slb.set_max(1) self.slider_label = QtWidgets.QLabel('width:') self.sb.valueChanged.connect(self.sizeChange) self.slb.valueChanged.connect(self.widthChange) hbox = QtWidgets.QHBoxLayout() hbox.addWidget(self.slider_label) hbox.addWidget(self.slb) hbox.setStretch(0, 0) hbox.setSpacing(5) hboxGroup = QtWidgets.QHBoxLayout() hboxGroup.addWidget(self.sb) hboxGroup.addLayout(hbox) hboxGroup.setStretch(0, 0) hboxGroup.setContentsMargins(0, 0, 0, 0) # we don't need margins here hboxGroup.setSpacing(30) self.setLayout(hboxGroup) # setters def set_val(self, val): """A python-dict containing: size, and width parms. """ sig = False if 'size' in val: self._val['size'] = val['size'] self.setSizeQuietly(val['size']) sig = True if 'width' in val: self._val['width'] = val['width'] self.setWidthQuietly(val['width']) sig = True if sig: self.valueChanged.emit() # getters def get_val(self): return self._val # support def sizeChange(self, val): self._val['size'] = val self.setSizeQuietly(val) self.valueChanged.emit() def widthChange(self, val): self._val['width'] = val self.setWidthQuietly(val) self.valueChanged.emit() def setSizeQuietly(self, val): self.sb.blockSignals(True) self.sb.set_val(val) self.slb.set_max(val) self.sb.blockSignals(False) def setWidthQuietly(self, val): self.slb.blockSignals(True) self.slb.set_val(val) self.slb.blockSignals(False)
class GPI_FSM(QtCore.QObject): """The GPI Canvase and Nodes operate within different states. The finite state machine allows for fast and easy checking to determine whether the user has performed an invalid operation or not. """ switched = gpi.Signal() def __init__(self, name=''): super(GPI_FSM, self).__init__() self._name = name self._states = [] self._cur_state = None @property def curState(self): return self._cur_state @property def curStateName(self): return self._cur_state.name @property def name(self): return self._name def next(self, dsig): '''Validate input signal as part of _cur_state, then switch to indicated state.''' if isinstance(dsig, str): sig = str(dsig) dsig = sig elif isinstance(dsig, str): sig = str(dsig) dsig = sig elif isinstance(dsig, dict): if 'sig' in dsig: if isinstance(dsig['sig'], str) or isinstance( dsig['sig'], str): sig = str(dsig['sig']) dsig['sig'] = sig else: msg = "expecting str in dict[\'sig\'] in arg: GPI_FSM(" + self._name + ").next(>str<)" log.critical(msg) else: msg = "expecting str key \'sig\' in arg: GPI_FSM(" + self._name + ").next(>str<)" log.critical(msg) return else: msg = "expecting str (or str in dict[\'sig\']) arg: GPI_FSM(" + \ self._name + ").next(>str<) type:" + str(type(dsig)) log.critical(msg) return if sig in self._cur_state.transitions(): self._cur_state.onExit(dsig) self._cur_state = self._cur_state.transitions()[sig] msg = "GPI_FSM(" + self._name + "):next(): Switched to state(" + \ self._cur_state.name + ")" log.debug(msg) self._cur_state.onEntry(dsig) self.switched.emit() else: msg = "GPI_FSM(" + self._name + "):next(): state(" + self._cur_state.name + \ ") has no transition: \'" + str( sig) + "\'\n" + str(self._cur_state.transitions()) log.debug(msg) def addState(self, state): if state in self._states: msg = "EMPHATIC WARNING!!!: GPI_FSM(" + self._name + "):addState(): Warning, state(" + state.name \ + ") already exists, skipping..." log.critical(msg) else: self._states.append(state) def start(self, state): if state in self._states: self._cur_state = state msg = "GPI_FSM(" + self._name + \ "):start(): in state(" + state.name + ")" log.debug(msg) self._cur_state.onEntry('init') else: msg = "GPI_FSM(" + self._name + "):start(): ERROR, state(" + state.name \ + ") state not in list, can't start GPI_FSM!" log.critical(msg)
class GPIGLWidget(QtOpenGL.QGLWidget): xRotationChanged = gpi.Signal(int) yRotationChanged = gpi.Signal(int) zRotationChanged = gpi.Signal(int) def __init__(self, parent=None): super(GPIGLWidget, self).__init__(parent) self.xRot = 0 self.yRot = 0 self.zRot = 0 self._vScale = 1.0 self._xPan = 0.0 self._yPan = 0.0 # a list of object descriptions held in dictionaries self._GPI_glList = glo.ObjectList() # trac number of exceptions thrown in problem code self._glError_cnt = 0 # cache objects in a gl list self._glList_cache = None self._glList_nonCacheable = [] # lighting self._default_lightPos = [0.0, 0.0, 10.0, 1.0] self._lightPos = self._default_lightPos[:] # built-in options self._blend = 0 self._test = 0 self._polyfill = 0 self._accum_antialiasing = 0 # self.setMouseTracking(True) self.checkFormat() def checkFormat(self): msg = "\n\taccum buffer:" + str(self.format().accum()) + "\n" msg += "\talpha buffer:" + str(self.format().alpha()) + "\n" msg += "\tdepth buffer:" + str(self.format().depth()) + "\n" msg += "\tdirectRendering:" + \ str(self.format().directRendering()) + "\n" msg += "\tdoubleBuffer:" + str(self.format().doubleBuffer()) + "\n" msg += "\thasOverlay:" + str(self.format().hasOverlay()) + "\n" msg += "\tplane:" + str(self.format().plane()) + "\n" msg += "\trgba:" + str(self.format().rgba()) + "\n" log.node(msg) def setGPIglList(self, lst): if isinstance(lst, glo.ObjectList): self._GPI_glList = lst def getGPIglList(self): return self._GPI_glList def __del__(self): # cannot guarantee that the underlying object hasn't been deleted # before this context is made current try: self.makeCurrent() except: log.node(traceback.format_exc()) self.resetGLCache() def setViewScale(self, s): self._vScale += s self.updateGL() def setXRotation(self, angle): self.normalizeAngle(angle) if angle != self.xRot: self.xRot = angle self.xRotationChanged.emit(angle) self.updateGL() def setYRotation(self, angle): self.normalizeAngle(angle) if angle != self.yRot: self.yRot = angle self.yRotationChanged.emit(angle) self.updateGL() def setZRotation(self, angle): self.normalizeAngle(angle) if angle != self.zRot: self.zRot = angle self.zRotationChanged.emit(angle) self.updateGL() def initializeGL(self): # from basic gear pos GL.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, self._lightPos) GL.glEnable(GL.GL_LIGHTING) GL.glEnable(GL.GL_LIGHT0) GL.glEnable(GL.GL_DEPTH_TEST) GL.glEnable(GL.GL_NORMALIZE) # set scene to black GL.glClearColor(0.0, 0.0, 0.0, 0.0) # cache input objects self._clipON = False # self.instantiateRefs() self.cacheGLCommands() def paintGL(self): GL.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, self._lightPos) if self._blend: GL.glCullFace(GL.GL_BACK) GL.glEnable(GL.GL_CULL_FACE) GL.glBlendFunc(GL.GL_SRC_ALPHA_SATURATE, GL.GL_ONE) # something is not properly initialized for this to run # correctly at the beginning. try: GL.glClear(GL.GL_COLOR_BUFFER_BIT) except: log.node(traceback.format_exc()) self._glError_cnt += 1 GL.glEnable(GL.GL_BLEND) GL.glEnable(GL.GL_POLYGON_SMOOTH) GL.glDisable(GL.GL_DEPTH_TEST) else: GL.glDisable(GL.GL_CULL_FACE) # something is not properly initialized for this to run # correctly at the beginning. try: GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) except: log.node(traceback.format_exc()) self._glError_cnt += 1 GL.glDisable(GL.GL_BLEND) GL.glDisable(GL.GL_POLYGON_SMOOTH) GL.glEnable(GL.GL_DEPTH_TEST) if self._polyfill == 2: GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_POINT) elif self._polyfill == 1: GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE) else: GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL) if self._accum_antialiasing: GL.glClear(GL.GL_ACCUM_BUFFER_BIT) cnt = 3 mult = cnt * 2 mult *= mult invmult = 1.0 / mult for i in range(-cnt, cnt): for j in range(-cnt, cnt): # jitter and paint GL.glPushMatrix() GL.glTranslatef(i * 0.00511, j * 0.00511, 0.0) self.paintScene() GL.glPopMatrix() # accum GL.glAccum(GL.GL_ACCUM, invmult) GL.glAccum(GL.GL_RETURN, 1.0) else: self.paintScene() if self._test: pass else: pass # force all commands to complete GL.glFlush() def paintScene(self): '''Does the actuall object rendering calls. ''' # rotate view GL.glPushMatrix() GL.glRotatef(self.xRot / 16.0, 1.0, 0.0, 0.0) GL.glRotatef(self.yRot / 16.0, 0.0, 1.0, 0.0) GL.glRotatef(self.zRot / 16.0, 0.0, 0.0, 1.0) # pan mtx = np.array(GL.glGetDoublev(GL.GL_MODELVIEW_MATRIX)) A = mtx[0:3, 0:2] x = np.transpose(np.array([[self._xPan, self._yPan]])) b = np.dot(A, x) GL.glTranslatef(b[0], b[1], b[2]) # zoom GL.glScale(self._vScale, self._vScale, self._vScale) # render all non-cacheable first if len(self._glList_nonCacheable): for desc in self._glList_nonCacheable: desc.run() # use cached list for mouse interactions try: GL.glCallList(self._glList_cache) except: log.node(traceback.format_exc()) self._glError_cnt += 1 # rotate view GL.glPopMatrix() def resizeGL(self, width, height): side = min(width, height) if side < 0: return GL.glViewport((width - side) // 2, (height - side) // 2, side, side) GL.glMatrixMode(GL.GL_PROJECTION) GL.glLoadIdentity() GL.glFrustum(-1.0, +1.0, -1.0, 1.0, 5.0, 60.0) GL.glMatrixMode(GL.GL_MODELVIEW) GL.glLoadIdentity() GL.glTranslatef(0.0, 0.0, -40.0) def resetGLCache(self): '''reset both cacheable and non-cacheable items ''' if self._glList_cache: GL.glDeleteLists(self._glList_cache, 1) self._glList_cache = None self._glList_nonCacheable = [] def instantiateRefs(self): '''Run thru all objects calling their instantiateLibs() method. ''' for desc in self._GPI_glList: desc.instantiateRefs() if type(desc) is glo.ClipPlane: print("enable clipping", desc.getPlaneNumTr()) GL.glEnable(desc.getPlaneNumTr()) def cacheGLCommands(self): '''Cache object commands. ''' if self._glList_cache: self.resetGLCache() self._glList_cache = GL.glGenLists(1) GL.glNewList(self._glList_cache, GL.GL_COMPILE) # cache all commands in obj list for desc in self._GPI_glList.getCacheableList(): desc.run() # special objects for plane, desc in self._GPI_glList.getClipPlanes().items(): print('render: ' + plane) desc.run() GL.glEndList() # get all non-cacheables self._glList_nonCacheable = self._GPI_glList.getNonCacheableList() if len(self._glList_nonCacheable): for desc in self._glList_nonCacheable: desc.setGLWidgetRef(self) def wheelEvent(self, event): if event.delta() > 0: self.setViewScale(0.1) else: self.setViewScale(-0.1) def mousePressEvent(self, event): self.lastPos = event.pos() def mouseMoveEvent(self, event): dx = event.x() - self.lastPos.x() dy = event.y() - self.lastPos.y() printMouseEvent(event) modifiers = getKeyboardModifiers() modmidbutton_event = (event.buttons() & QtCore.Qt.RightButton and modifiers & QtCore.Qt.ShiftModifier) if event.buttons() & QtCore.Qt.LeftButton: self._xPan += dx * 0.05 self._yPan += -dy * 0.05 self.updateGL() elif modmidbutton_event: self.setXRotation(self.xRot + 8 * dy) self.setYRotation(self.yRot + 8 * dx) elif event.buttons() & QtCore.Qt.MidButton: self._lightPos[0] += dx * 0.05 self._lightPos[1] += -dy * 0.05 self.updateGL() elif event.buttons() & QtCore.Qt.RightButton: self.setXRotation(self.xRot + 8 * dy) self.setZRotation(self.zRot + 8 * dx) self.lastPos = event.pos() def resetViewingWindow(self): '''reset pan, zoom, rotate ''' self._xPan = 0.0 self._yPan = 0.0 self._vScale = 1.0 self.xRot = 0.0 self.yRot = 0.0 self.zRot = 0.0 self._lightPos = self._default_lightPos[:] def xRotation(self): return self.xRot def yRotation(self): return self.yRot def zRotation(self): return self.zRot def normalizeAngle(self, angle): while (angle < 0): angle += 360 * 16 while (angle > 360 * 16): angle -= 360 * 16 def setBlend(self, val): self._blend = val self.updateGL() def setTest(self, val): self._test = val self.updateGL() def setPolyFill(self, val): self._polyfill = val self.updateGL() def setAntiAliasing(self, val): self._accum_antialiasing = val self.updateGL()
class ConfigSeqWidgets(gpi.GenericWidgetGroup): """A unique widget that display a variable number of StringBoxes depending on the Pulse being configured.""" valueChanged = gpi.Signal() def __init__(self, title, parent=None): super(ConfigSeqWidgets, self).__init__(title, parent) self.button_names_list = [ 'Off', 'SincRfPulse', 'HardRfPulse', 'TrapGradPulse', 'TriangleGradPulse' ] self.clicked_button_name, self.clicked_button_index = '', 0 self.buttons_list, self.string_box_list = [], [] # Labels for StringBoxes to configure Events self.sinc_rf_labels = [ 'Name', 'Observe', 'ADCs', 'Apodization', 'Bandwidth', 'Axis', 'FlipAngle', 'Frequency', 'HardwareMode', 'InitialDelay', 'InitialPhase', 'Refocusing', 'Symmetry', 'Vector', ' Zeros' ] self.hard_rf_labels = [ 'Name', 'Observe', 'ADCs', 'Channel', 'Duration', 'FlipAngle', 'Frequency', 'HardwareMode', 'InitialDelay', 'InitialPhase', 'Refocusing', 'Symmetry', 'Vector' ] self.trap_grad_labels = [ 'Name', 'Observe', 'ADCs', 'Area', 'Asymmetric', 'Axis', 'Duration', 'EddyConvLength', 'EddyCurrents', 'FlatTopArea', 'FlatTopTime', 'Frequency', 'HardwareMode', 'Hide', 'InitialDelay', 'InitialPhase', 'MaxAmpl', 'NLG_field', 'PhaseLock', 'SlewRate' ] self.triangle_grad_labels = [ 'Name', 'Observe', 'ADCs', 'Amplitude', 'Axis', 'Duration', 'EddyConvLength', 'EddyCurrents', 'HardwareMode', 'Hide', 'InitialDelay', 'InitialPhase', 'MaxAmpl', 'NLG_field', 'PhaseLock', 'TriangleType', 'Vector' ] # Variable to denote the maximum number of StringBoxes to be added; obviously depends on the Event which has the # maximum number of configuration parameters_params self.num_string_boxes = max(len(self.hard_rf_labels), len(self.trap_grad_labels)) # First index is None because the first button is 'Off'. Look into event_def['event_values'] in get_val() self.labels = [ None, self.sinc_rf_labels, self.hard_rf_labels, self.trap_grad_labels, self.triangle_grad_labels ] self.wdg_layout = QtGui.QGridLayout() self.add_event_pushbuttons() self.add_config_stringboxes() self.setLayout(self.wdg_layout) self.buttons_list[0].setChecked(True) def add_event_pushbuttons(self): """Adding PushButtons for the Pulses.""" col_count = 0 for name in self.button_names_list: new_button = QtGui.QPushButton(name) new_button.setCheckable(True) new_button.setAutoExclusive(True) new_button.clicked.connect(self.button_clicked) new_button.clicked.connect(self.valueChanged) # Syntax: addWidget(widget, row, col, rowSpan, colSpan) self.wdg_layout.addWidget(new_button, 0, col_count, 1, 1) self.buttons_list.append(new_button) col_count += 1 def add_config_stringboxes(self): """Adding StringBoxes for configuring the Pulses.""" for x in range(self.num_string_boxes): string_box = gpi.StringBox(str(x)) string_box.set_visible(False) # Syntax: addWidget(widget, row, col, rowSpan, colSpan) self.wdg_layout.addWidget(string_box, x + 1, 1, 1, 6) self.string_box_list.append(string_box) # Getter def get_val(self): if self.clicked_button_index == self.button_names_list.index('Off'): # 'Off' PushButton selected, return empty dict return None else: """ event_def contains: - event_name : str Event name, corresponds to Event button that is selected. See make_pulse() in JWriteXML_GPI - key-value pairs of Event parameters_params and values """ event_def = {} labels = self.labels[self.clicked_button_index] for i in range(len(self.string_box_list)): val = self.string_box_list[i].get_val() if val != '': event_def[labels[i]] = val event_def['event_name'] = self.clicked_button_name return event_def # Setter def set_val(self, val): self.hide_config_widgets() if val is not None and len(val) != 0: # If Event is not configured, self.clicked_button_name will be '' self.clicked_button_name = val['event_name'] if self.clicked_button_name != '': self.clicked_button_index = self.button_names_list.index( self.clicked_button_name) self.buttons_list[self.clicked_button_index].setChecked(True) self.show_config_widgets() labels = self.labels[self.clicked_button_index] for x in range(len(labels)): # Set value only if value is present in val dict if labels[x] in val: self.string_box_list[x].setTitle(labels[x]) self.string_box_list[x].set_val(val[labels[x]]) def button_clicked(self): """Identifies the button that was clicked and stores the name and ID of the button.""" for button in self.buttons_list: if button.isChecked(): self.clicked_button_index = self.buttons_list.index(button) self.clicked_button_name = self.button_names_list[ self.clicked_button_index] self.show_config_widgets() def show_config_widgets(self): """Show appropriate number of StringBoxes and relevant Widgets based on the button that was clicked.""" self.hide_config_widgets() if self.clicked_button_index != 0: [ self.string_box_list[x].set_visible(True) for x in range(len(self.labels[self.clicked_button_index])) ] [ self.string_box_list[x].setTitle( self.labels[self.clicked_button_index][x]) for x in range(len(self.labels[self.clicked_button_index])) ] def hide_config_widgets(self): """Hide all Widgets.""" [x.set_visible(False) for x in self.string_box_list] [x.set_val("") for x in self.string_box_list]
class FFTW_GROUP(gpi.GenericWidgetGroup): """A combination of SpinBoxes, DoubleSpinBoxes, and PushButtons to form a unique widget suitable for FFT options on dimensions. """ valueChanged = gpi.Signal() def __init__(self, title, parent=None): super().__init__(title, parent) self._val = {} self._val['compute'] = False self._val['length'] = 1 self._val['in_len'] = 1 # the original array length self.pb = gpi.BasicPushButton() self.pb.set_toggle(True) self.db = gpi.BasicDoubleSpinBox() # factor self.db.set_label('factor:') self.db.set_min(0.001) self.db.set_max(gpi.GPI_FLOAT_MAX) self.db.set_decimals(3) self.db.set_singlestep(0.001) self.db.set_val(1) self.sb = gpi.BasicSpinBox() # length self.sb.set_label('length:') self.sb.set_min(1) self.sb.set_val(1) self.sb.set_max(gpi.GPI_INT_MAX) self.db.valueChanged.connect(self.factChange) self.sb.valueChanged.connect(self.lenChange) self.pb.valueChanged.connect(self.compChange) vbox = QtWidgets.QHBoxLayout() vbox.addWidget(self.pb) vbox.addWidget(self.db) vbox.addWidget(self.sb) vbox.setStretch(0, 0) vbox.setStretch(1, 0) vbox.setStretch(2, 0) vbox.setContentsMargins(0, 0, 0, 0) # we don't need margins here vbox.setSpacing(0) self.setLayout(vbox) # setters def set_val(self, val): """A python-dict containing: in_len, length, and compute parms. """ sig = False if 'in_len' in val: # otherwise this would change every time compute() was called if self._val['in_len'] != val['in_len']: self._val['in_len'] = val['in_len'] fact = self.db.get_val() # set len based on factor fact *= self._val['in_len'] self.setLenQuietly(int(fact)) self._val['length'] = int(fact) if 'length' in val: self._val['length'] = val['length'] self.setLenQuietly(val['length']) self.setFactQuietly( float(val['length']) / float(self._val['in_len'])) sig = True if 'compute' in val: self._val['compute'] = val['compute'] self.setCompQuietly(val['compute']) sig = True if sig: self.valueChanged.emit() def set_reset(self): """An override that communicates with the embedded pushbutton. """ self.pb.set_reset() # getters def get_val(self): return self._val # support def factChange(self, val): self._val['length'] = int(self._val['in_len'] * val) self.setLenQuietly(self._val['length']) self.valueChanged.emit() def lenChange(self, val): self._val['length'] = val self.setFactQuietly(float(val) / float(self._val['in_len'])) self.valueChanged.emit() def compChange(self, val): self._val['compute'] = val self.valueChanged.emit() if val: self.pb.set_button_title('ON') else: self.pb.set_button_title('') def setFactQuietly(self, val): self.db.blockSignals(True) self.db.set_val(val) self.db.blockSignals(False) def setLenQuietly(self, val): self.sb.blockSignals(True) self.sb.set_val(val) self.sb.blockSignals(False) def setCompQuietly(self, val): self.pb.blockSignals(True) self.pb.set_val(val) self.pb.blockSignals(False)
class MatplotDisplay(gpi.GenericWidgetGroup): """Embeds the matplotlib figure window. """ valueChanged = gpi.Signal() def __init__(self, title, parent=None): super().__init__(title, parent) # gpi interface self._collapsables = [] self._subplotSettings = {} #self._subplotPosition = {'right': 0.9, 'bottom': 0.12, 'top': 0.9, 'wspace': 0.2, 'hspace': 0.2, 'left': 0.125} self._subplotPosition = {'right': 0.913, 'bottom': 0.119, 'top': 0.912, 'wspace': 0.2, 'hspace': 0.2, 'left': 0.111} #self._subplot_keepers = ['yscale', 'xscale'] # linear, log self._subplot_keepers = [] self._lineSettings = [] self._line_keepers = ['linewidth', 'linestyle', 'label', 'marker', 'markeredgecolor', 'markerfacecolor', 'markersize', 'color', 'alpha'] # since drawing is slow, don't do it as often, use the timer as a # debouncer self._on_draw_cnt = 0 self._updatetimer = QtCore.QTimer() self._updatetimer.setSingleShot(True) self._updatetimer.timeout.connect(self._on_draw) self._updatetimer.setInterval(10) # plot specific UI side panel # -sets options for plot window so this needs to be run first vbox = QtWidgets.QVBoxLayout() vbox.setContentsMargins(0, 0, 0, 0) # no spaces around this item vbox.setSpacing(0) # AUTOSCALE self._autoscale_btn = gpi.widgets.BasicPushButton(self) self._autoscale_btn.set_toggle(True) self._autoscale_btn.set_button_title('autoscale') self._autoscale_btn.valueChanged.connect(self.on_draw) self._collapsables.append(self._autoscale_btn) # GRID self._grid_btn = gpi.widgets.BasicPushButton(self) self._grid_btn.set_toggle(True) self._grid_btn.set_button_title('grid') self._grid_btn.valueChanged.connect(self.on_draw) self._collapsables.append(self._grid_btn) # X/Y LIMITS lims = QtWidgets.QGridLayout() self._xl = gpi.widgets.BasicDoubleSpinBox(self) self._xh = gpi.widgets.BasicDoubleSpinBox(self) self._yl = gpi.widgets.BasicDoubleSpinBox(self) self._yh = gpi.widgets.BasicDoubleSpinBox(self) self._xl.valueChanged.connect(self.on_draw) self._xh.valueChanged.connect(self.on_draw) self._yl.valueChanged.connect(self.on_draw) self._yh.valueChanged.connect(self.on_draw) self._xl.set_immediate(True) self._xh.set_immediate(True) self._yl.set_immediate(True) self._yh.set_immediate(True) self._xl.set_label('max') self._xh.set_label('min') self._xl.set_decimals(7) self._xh.set_decimals(7) self._yl.set_decimals(7) self._yh.set_decimals(7) self._xlab = QtWidgets.QLabel('x limits') self._ylab = QtWidgets.QLabel('y limits') #self._maxlab = QtWidgets.QLabel('max') #self._minlab = QtWidgets.QLabel('min') #lims.addWidget(self._maxlab,1,0,1,1) #lims.addWidget(self._minlab,2,0,1,1) lims.addWidget(self._xlab,0,1,1,1,alignment=QtCore.Qt.AlignHCenter) lims.addWidget(self._xh,1,1,1,1,alignment=QtCore.Qt.AlignHCenter) lims.addWidget(self._xl,2,1,1,1,alignment=QtCore.Qt.AlignHCenter) lims.addWidget(self._ylab,0,2,1,1,alignment=QtCore.Qt.AlignHCenter) lims.addWidget(self._yh,1,2,1,1,alignment=QtCore.Qt.AlignHCenter) lims.addWidget(self._yl,2,2,1,1,alignment=QtCore.Qt.AlignHCenter) self._collapsables.append(self._xlab) self._collapsables.append(self._ylab) self._collapsables.append(self._xl) self._collapsables.append(self._xh) self._collapsables.append(self._yl) self._collapsables.append(self._yh) #self._collapsables.append(self._minlab) #self._collapsables.append(self._maxlab) # TICK MARKS ticks = QtWidgets.QGridLayout() self._x_numticks = gpi.widgets.BasicSpinBox(self) self._x_numticks.valueChanged.connect(self.on_draw) self._y_numticks = gpi.widgets.BasicSpinBox(self) self._y_numticks.valueChanged.connect(self.on_draw) self._x_ticks = QtWidgets.QLineEdit() self._y_ticks = QtWidgets.QLineEdit() self._x_ticks.textChanged.connect(lambda txt: self.check_validticks(self._x_ticks)) self._y_ticks.textChanged.connect(lambda txt: self.check_validticks(self._y_ticks)) self._x_ticks.setPlaceholderText('comma separated list of x labels') self._y_ticks.setPlaceholderText('comma separated list of y labels') self._x_ticks.returnPressed.connect(self.on_draw) self._y_ticks.returnPressed.connect(self.on_draw) self._x_numticks.set_immediate(True) self._y_numticks.set_immediate(True) self._x_numticks.set_min(2) self._y_numticks.set_min(2) self._x_numticks.set_max(100) self._y_numticks.set_max(100) self._x_numticks.set_val(5) self._y_numticks.set_val(5) self._x_numticks.set_label('x ticks') self._y_numticks.set_label('y ticks') ticks.addWidget(self._x_numticks, 0,0,1,1) ticks.addWidget(self._y_numticks, 1,0,1,1) ticks.addWidget(self._x_ticks, 0,1,1,1) ticks.addWidget(self._y_ticks, 1,1,1,1) self._collapsables.append(self._x_numticks) self._collapsables.append(self._y_numticks) self._collapsables.append(self._x_ticks) self._collapsables.append(self._y_ticks) # TITLE, XLABEL, YLABEL plotlabels = QtWidgets.QHBoxLayout() self._plot_title = QtWidgets.QLineEdit() self._plot_xlab = QtWidgets.QLineEdit() self._plot_ylab = QtWidgets.QLineEdit() self._plot_title.setPlaceholderText('title') self._plot_xlab.setPlaceholderText('x label') self._plot_ylab.setPlaceholderText('y label') self._plot_title.returnPressed.connect(self.on_draw) self._plot_xlab.returnPressed.connect(self.on_draw) self._plot_ylab.returnPressed.connect(self.on_draw) plotlabels.addWidget(self._plot_title) plotlabels.addWidget(self._plot_xlab) plotlabels.addWidget(self._plot_ylab) self._collapsables.append(self._plot_title) self._collapsables.append(self._plot_xlab) self._collapsables.append(self._plot_ylab) # XSCALE, YSCALE self._xscale_btn = gpi.widgets.BasicPushButton(self) self._xscale_btn.set_toggle(True) self._xscale_btn.set_button_title('log(x)') self._xscale_btn.valueChanged.connect(self.on_draw) self._collapsables.append(self._xscale_btn) self._yscale_btn = gpi.widgets.BasicPushButton(self) self._yscale_btn.set_toggle(True) self._yscale_btn.set_button_title('log(y)') self._yscale_btn.valueChanged.connect(self.on_draw) self._collapsables.append(self._yscale_btn) scale_options_layout = QtWidgets.QHBoxLayout() scale_options_layout.addWidget(self._xscale_btn) scale_options_layout.addWidget(self._yscale_btn) # LEGEND self._legend_btn = gpi.widgets.BasicPushButton(self) self._legend_btn.set_toggle(True) self._legend_btn.set_button_title('legend') self._legend_btn.valueChanged.connect(self.on_draw) self._collapsables.append(self._legend_btn) # HOLD self._hold_btn = gpi.widgets.BasicPushButton(self) self._hold_btn.set_toggle(True) self._hold_btn.set_button_title('hold') #self._hold_btn.valueChanged.connect(self.on_draw) self._collapsables.append(self._hold_btn) # MOVE AXES TO ORIGIN # self._origin_axes_btn = gpi.widgets.BasicPushButton(self) # self._origin_axes_btn.set_toggle(True) # self._origin_axes_btn.set_button_title("axes at (0,0)") # self._collapsables.append(self._origin_axes_btn) # RESET self._reset_btn = gpi.widgets.BasicPushButton(self) self._reset_btn.set_toggle(False) self._reset_btn.set_button_title('reset') self._reset_btn.valueChanged.connect(self._init_parms_) self._collapsables.append(self._reset_btn) # X=0, Y=0 self._xeq0_btn = gpi.widgets.BasicPushButton(self) self._xeq0_btn.set_toggle(True) self._xeq0_btn.set_button_title('x=0') self._xeq0_btn.set_val(True) self._xeq0_btn.valueChanged.connect(self.on_draw) self._collapsables.append(self._xeq0_btn) self._yeq0_btn = gpi.widgets.BasicPushButton(self) self._yeq0_btn.set_toggle(True) self._yeq0_btn.set_button_title('y=0') self._yeq0_btn.set_val(True) self._yeq0_btn.valueChanged.connect(self.on_draw) self._collapsables.append(self._yeq0_btn) # LINE OPTIONS self._lino_btn = gpi.widgets.BasicPushButton(self) self._lino_btn.set_toggle(False) self._lino_btn.set_button_title('line options') self._lino_btn.valueChanged.connect(self.lineOptionsDialog) self._collapsables.append(self._lino_btn) # SUBPLOT SPACING OPTIONS self._subplotso_btn = gpi.widgets.BasicPushButton(self) self._subplotso_btn.set_toggle(False) self._subplotso_btn.set_button_title('spacing options') self._subplotso_btn.valueChanged.connect(self.subplotSpacingOptions) self._collapsables.append(self._subplotso_btn) self.adj_window = None plot_options_layout = QtWidgets.QHBoxLayout() plot_options_layout.addWidget(self._subplotso_btn) plot_options_layout.addWidget(self._lino_btn) grid_legend_lyt = QtWidgets.QHBoxLayout() grid_legend_lyt.addWidget(self._legend_btn) grid_legend_lyt.addWidget(self._grid_btn) autoscale_scale_lyt = QtWidgets.QHBoxLayout() autoscale_scale_lyt.addWidget(self._autoscale_btn) autoscale_scale_lyt.addWidget(self._xscale_btn) autoscale_scale_lyt.addWidget(self._yscale_btn) autoscale_scale_lyt.addWidget(self._xeq0_btn) autoscale_scale_lyt.addWidget(self._yeq0_btn) # HLINES self._hline1 = QtWidgets.QFrame() self._hline1.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken) self._hline1.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) self._collapsables.append(self._hline1) self._hline2 = QtWidgets.QFrame() self._hline2.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken) self._hline2.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) self._collapsables.append(self._hline2) self._hline3 = QtWidgets.QFrame() self._hline3.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken) self._hline3.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) self._collapsables.append(self._hline3) spc = 10 self._spacer1 = QtWidgets.QSpacerItem(1,spc,QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self._spacer2 = QtWidgets.QSpacerItem(1,spc,QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self._spacer3 = QtWidgets.QSpacerItem(1,spc,QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self._spacer4 = QtWidgets.QSpacerItem(1,spc,QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self._spacer5 = QtWidgets.QSpacerItem(1,spc,QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self._spacer6 = QtWidgets.QSpacerItem(1,spc,QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self._collapsables.append(self._spacer1) self._collapsables.append(self._spacer2) self._collapsables.append(self._spacer3) self._collapsables.append(self._spacer4) self._collapsables.append(self._spacer5) self._collapsables.append(self._spacer6) # panel layout vbox.addLayout(plotlabels) vbox.addSpacerItem(self._spacer1) vbox.addWidget(self._hline1) vbox.addSpacerItem(self._spacer2) vbox.addLayout(lims) #vbox.addLayout(scale_options_layout) #vbox.addWidget(self._autoscale_btn) vbox.addLayout(autoscale_scale_lyt) vbox.addSpacerItem(self._spacer3) vbox.addWidget(self._hline2) vbox.addSpacerItem(self._spacer4) vbox.addLayout(ticks) #vbox.addWidget(self._legend_btn) vbox.addLayout(grid_legend_lyt) vbox.addLayout(plot_options_layout) #vbox.addWidget(self._lino_btn) #vbox.addWidget(self._subplotso_btn) vbox.addSpacerItem(self._spacer5) vbox.addWidget(self._hline3) vbox.addSpacerItem(self._spacer6) vbox.addWidget(self._hold_btn) # vbox.addWidget(self._origin_axes_btn) vbox.insertStretch(-1,1) vbox.addWidget(self._reset_btn) # plot window self._data = None self._plotwindow = self.create_main_frame() # put side panel and plot window together hbox = QtWidgets.QHBoxLayout() hbox.addLayout(vbox) hbox.addLayout(self._plotwindow) hbox.setStretch(0,0) hbox.setStretch(1,11) self.setLayout(hbox) #self._on_draw() # draw once to get initial settings #self.copySubplotSettings() # Don't hide side-panel options by default self.set_collapsed(False) self.set_grid(True) self.set_autoscale(True) # DEFAULTS self._init_parms_() # setters def set_val(self, data): '''Takes a list of npy arrays. ''' if isinstance(data, list): self._data = data self.on_draw() def set_grid(self, val): self._grid_btn.set_val(val) self.on_draw() def set_autoscale(self, val): self._autoscale_btn.set_val(val) self.on_draw() def set_collapsed(self, val): """bool | Only collapse the display options, not the Plot window. """ self._isCollapsed = val for wdg in self._collapsables: if hasattr(wdg, 'setVisible'): wdg.setVisible(not val) def set_xlim(self, val, quiet=False): '''tuple of floats (min, max) ''' self._xh.set_val(val[0]) self._xl.set_val(val[1]) if not quiet: self.on_draw() def set_ylim(self, val, quiet=False): '''tuple of floats (min, max) ''' self._yh.set_val(val[0]) self._yl.set_val(val[1]) if not quiet: self.on_draw() def set_plotOptions(self, val): self._subplotSettings = val def set_lineOptions(self, val): self._lineSettings = val def set_plotPosition(self, val): self._subplotPosition = val def set_ticks(self, s): self._x_numticks.set_val(s['xticknum']) self._y_numticks.set_val(s['yticknum']) self._x_ticks.setText(s['xticks']) self._y_ticks.setText(s['yticks']) def set_plotlabels(self, s): self._plot_title.setText(s['title']) self._plot_xlab.setText(s['xlabel']) self._plot_ylab.setText(s['ylabel']) self.on_draw() def set_legend(self, val): self._legend_btn.set_val(val) self.on_draw() def set_xline(self, val): self._yeq0_btn.set_val(val) self.on_draw() def set_yline(self, val): self._xeq0_btn.set_val(val) self.on_draw() def set_scale(self, val): self._xscale_btn.set_val(val['xscale']) self._yscale_btn.set_val(val['yscale']) self.on_draw() # getters def get_val(self): return self._data def get_grid(self): return self._grid_btn.get_val() def get_autoscale(self): return self._autoscale_btn.get_val() def get_xlim(self): return (self._xh.get_val(), self._xl.get_val()) def get_ylim(self): return (self._yh.get_val(), self._yl.get_val()) def get_plotOptions(self): return self._subplotSettings def get_lineOptions(self): return self._lineSettings def get_plotPosition(self): return self._subplotPosition def get_ticks(self): s = {} s['xticknum'] = self._x_numticks.get_val() s['yticknum'] = self._y_numticks.get_val() s['xticks'] = str(self._x_ticks.text()) s['yticks'] = str(self._y_ticks.text()) return s def get_plotlabels(self): s = {} s['title'] = str(self._plot_title.text()) s['xlabel'] = str(self._plot_xlab.text()) s['ylabel'] = str(self._plot_ylab.text()) return s def get_legend(self): return self._legend_btn.get_val() def get_xline(self): return self._yeq0_btn.get_val() def get_yline(self): return self._xeq0_btn.get_val() def get_scale(self): s = {} s['xscale'] = self._xscale_btn.get_val() s['yscale'] = self._yscale_btn.get_val() return s # support def check_validticks(self, tickwdg): s = tickwdg.text() comma_cnt = s.count(',') if (comma_cnt > 0) or (len(s) == 0): color = '#ffffff' # white tickwdg.setStyleSheet('QLineEdit { background-color: %s }' % color) return #color = '#fff79a' # yellow color = '#f6989d' # red tickwdg.setStyleSheet('QLineEdit { background-color: %s }' % color) return def create_main_frame(self): self.fig = Figure((6.0, 4.8), dpi=100, facecolor='0.98', linewidth=6.0, edgecolor='0.93') self.axes = None self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self) self.canvas.setFocusPolicy(QtCore.Qt.StrongFocus) self.canvas.setFocus() self.canvas.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) #self.mpl_toolbar = NavigationToolbar(self.canvas, self) self.mpl_toolbar = NavbarTools(self.canvas, self) self.mpl_toolbar.actionTriggered.connect(self.copySubplotSettings) self.canvas.mpl_connect('key_press_event', self.on_key_press) vbox = QtWidgets.QVBoxLayout() vbox.addWidget(self.canvas) # the matplotlib canvas vbox.addWidget(self.mpl_toolbar) return vbox #self.setLayout(vbox) def lineOptionsDialog(self): if self.axes is None: print("Matplotlib: no lines to modify, skipping line editor") return figure_edit(self.axes, self) self.copySubplotSettings() self.on_draw() def subplotSpacingOptions(self): if self.fig is None: return # don't allow the user to open extra windows if self.adj_window is not None: if self.adj_window.isActive(): self.adj_window.raise_() return self.adj_window = MainWin_close() self.adj_window.window_closed.connect(self.copySubplotSettings) win = self.adj_window win.setWindowTitle("Subplot Configuration Tool") image = os.path.join( matplotlib.rcParams['datapath'],'images','matplotlib.png' ) win.setWindowIcon(QtGui.QIcon( image )) tool = SubplotToolQt(self.fig, win) win.setCentralWidget(tool) win.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) win.show() def copySubplotSettings(self): '''Get a copy of the settings found in the 'Figure Options' editor. ''' if self.axes is None: return # subplot settings for k in self._subplot_keepers: self._subplotSettings[k] = getattr(self.axes, 'get_'+k)() # line settings self._lineSettings = [] for l in self.axes.get_lines(): s = {} for k in self._line_keepers: s[k] = getattr(l, 'get_'+k)() self._lineSettings.append(s) # subplot position self._subplotPosition = {} self._subplotPosition['left'] = self.fig.subplotpars.left self._subplotPosition['right'] = self.fig.subplotpars.right self._subplotPosition['top'] = self.fig.subplotpars.top self._subplotPosition['bottom'] = self.fig.subplotpars.bottom self._subplotPosition['wspace'] = self.fig.subplotpars.wspace self._subplotPosition['hspace'] = self.fig.subplotpars.hspace def applySubplotSettings(self): '''Everytime the plot is drawn it looses its 'Figure Options' so just make sure they are applied again. ''' # subplot settings for k in self._subplot_keepers: if k in self._subplotSettings: getattr(self.axes, 'set_'+k)(self._subplotSettings[k]) #subplot position self.fig.subplots_adjust(**self._subplotPosition) def _init_parms_(self): '''Default parameter settings ''' self._subplotSettings = {} self._subplotPosition = {'right': 0.913, 'bottom': 0.119, 'top': 0.912, 'wspace': 0.2, 'hspace': 0.2, 'left': 0.111} self._lineSettings = [] self.set_autoscale(True) self.set_grid(True) s = {} s['xticknum'] = 5 s['yticknum'] = 5 s['xticks'] = '' s['yticks'] = '' self.set_ticks(s) s = {} s['title'] = '' s['xlabel'] = '' s['ylabel'] = '' self.set_plotlabels(s) self.set_legend(False) s = {} s['xscale'] = False # linear s['yscale'] = False self.set_scale(s) self.on_draw() def on_draw(self): self._on_draw_cnt += 1 if not self._updatetimer.isActive(): self._updatetimer.start() def _on_draw(self): # HOLD / Create New AXES if not self._hold_btn.get_val(): self.fig.clear() self.axes = self.fig.add_subplot(111) # AUTOSCALE and LIMITS self.axes.set_autoscale_on(self.get_autoscale()) if not self.get_autoscale(): self.axes.set_xlim(self.get_xlim()) self.axes.set_ylim(self.get_ylim()) # TITLE, XLABEL and YLABEL self.axes.set_title(self.get_plotlabels()['title'], fontweight='bold', fontsize=16) self.axes.set_xlabel(self.get_plotlabels()['xlabel'], fontsize=14) self.axes.set_ylabel(self.get_plotlabels()['ylabel'], fontsize=14) # self.axes.plot(self.x, self.y, 'ro') # self.axes.imshow(self.data, interpolation='nearest') # self.axes.plot([1,2,3]) # XSCALE if self.get_scale()['xscale']: self.axes.set_xscale('log') else: self.axes.set_xscale('linear') # YSCALE if self.get_scale()['yscale']: self.axes.set_yscale('log') else: self.axes.set_yscale('linear') # GRID ax_color = '0.5' if self.get_grid(): self.axes.grid(self.get_grid(), color=ax_color) else: self.axes.grid(self.get_grid()) # AXES SPINE COLOR self.axes.spines['bottom'].set_color(ax_color) self.axes.spines['top'].set_color(ax_color) self.axes.spines['right'].set_color(ax_color) self.axes.spines['left'].set_color(ax_color) try: # deprecated in Matplotlib 2.0 self.axes.set_axis_bgcolor('0.97') except AttributeError: self.axes.set_facecolor('0.97') # if self._origin_axes_btn.get_val(): # self.axes.spines['left'].set_position('zero') # self.axes.spines['bottom'].set_position('zero') # self.axes.spines['left'].set_smart_bounds(True) # self.axes.spines['bottom'].set_smart_bounds(True) # self.axes.xaxis.set_ticks_position('bottom') # self.axes.yaxis.set_ticks_position('left') if self._data is None: return try: self.fig.hold(True) except: pass # plot each set # print "--------------------plot the data" for data in self._data: ln = max(data.shape) lw = max(5.0-np.log10(ln), 1.0) if ln > 0: al = max(1.0-1.0/np.log2(ln), 0.75) else: al = 0 if data.shape[-1] == 2: self.axes.plot(data[..., 0], data[..., 1], alpha=al, lw=lw) else: self.axes.plot(data, alpha=al, lw=lw) # X=0, Y=0 if self.get_xline(): self.axes.axhline(y=0, color='k', zorder=-1, label="y=0") if self.get_yline(): self.axes.axvline(x=0, color='k', zorder=-1, label="x=0") # LEGEND if self.get_legend(): handles, labels = self.axes.get_legend_handles_labels() self.axes.legend(handles, labels) # AUTOSCALE if self.get_autoscale(): self.set_xlim(self.axes.get_xlim(), quiet=True) self.set_ylim(self.axes.get_ylim(), quiet=True) # X TICKS xl = self._x_ticks.text().split(',') if len(xl) > 1: self.axes.set_xticks(np.linspace(*self.axes.get_xlim(), num=len(xl))) self.axes.set_xticklabels(xl) else: self.axes.set_xticks(np.linspace(*self.axes.get_xlim(), num=self._x_numticks.get_val())) # Y TICKS yl = self._y_ticks.text().split(',') if len(yl) > 1: self.axes.set_yticks(np.linspace(*self.axes.get_ylim(), num=len(yl))) self.axes.set_yticklabels(yl) else: self.axes.set_yticks(np.linspace(*self.axes.get_ylim(), num=self._y_numticks.get_val())) self.applySubplotSettings() self.canvas.draw() #print 'draw count: ', self._on_draw_cnt self._on_draw_cnt = 0 def on_key_press(self, event): # print 'Matplotlib-> you pressed:' + str(event.key) # implement the default mpl key press events described at # http://matplotlib.org/users/navigation_toolbar.html#navigation- # keyboard-shortcuts try: from matplotlib.backend_bases import key_press_handler key_press_handler(event, self.canvas, self.mpl_toolbar) except: print("key_press_handler import failed. -old matplotlib version.")