# Copyright (C) 2014 Dignity Health
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# NO CLINICAL USE. THE SOFTWARE IS NOT INTENDED FOR COMMERCIAL PURPOSES
# AND SHOULD BE USED ONLY FOR NON-COMMERCIAL RESEARCH PURPOSES. THE
# SOFTWARE MAY NOT IN ANY EVENT BE USED FOR ANY CLINICAL OR DIAGNOSTIC
# PURPOSES. YOU ACKNOWLEDGE AND AGREE THAT THE SOFTWARE IS NOT INTENDED FOR
# USE IN ANY HIGH RISK OR STRICT LIABILITY ACTIVITY, INCLUDING BUT NOT
# LIMITED TO LIFE SUPPORT OR EMERGENCY MEDICAL OPERATIONS OR USES. LICENSOR
# MAKES NO WARRANTY AND HAS NO LIABILITY ARISING FROM ANY USE OF THE
# SOFTWARE IN ANY HIGH RISK OR STRICT LIABILITY ACTIVITIES.
# gpi
import gpi
from gpi import QtCore, QtGui
from .defines import isWidget
from .widgets import HidableGroupBox
from .logger import manager
# start logger for this module
log = manager.getLogger(__name__)
[docs]class LayoutWindow(QtGui.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 = QtGui.QVBoxLayout()
elif typ == 'hbox':
self._layout = QtGui.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 self._layout.count()
def closeEvent(self, event):
event.accept()
return
# don't close if any are attached
if not self.count():
event.accept()
return
event.ignore()
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
[docs]class LayoutMaster(QtGui.QWidget):
'''This is the main "Layout-Window" API. It controls the layout config,
reinstantiation of layouts, opening and closing, etc...
'''
def __init__(self, graph, config=0, macro=False, labelWin=False, parent=None):
super(LayoutMaster, self).__init__(parent)
self._graph = graph
self._isMacroWdg = macro
self._labelWin = labelWin
self.initLayout(config)
def initLayout(self, config):
'''Given the config initialize all layout objects overwriting any
existing layouts.
'''
self._config = config
self._lwList = []
# delete layout if one exists
# TODO: this needs to work if Layouts are to be re-initialized
#if self.layout():
# print "delete layout before setting a new one."
# for lw in self._lwList:
# for c in lw.children():
# c.setParent(None)
# lw.setParent(None)
# self.layout().setParent(None)
hbox = QtGui.QVBoxLayout(self)
if config == 0:
top = LayoutWindow(self._graph, 'vbox', 'top')
top.setFrameShape(QtGui.QFrame.StyledPanel)
hbox.addWidget(top)
self._lwList.append(top)
elif config == 1:
top = LayoutWindow(self._graph, 'hbox', 'top')
top.setFrameShape(QtGui.QFrame.StyledPanel)
hbox.addWidget(top)
self._lwList.append(top)
elif config == 2:
top = LayoutWindow(self._graph, 'hbox', 'top')
top.setFrameShape(QtGui.QFrame.StyledPanel)
self._lwList.append(top)
mid = LayoutWindow(self._graph, 'hbox', 'mid')
mid.setFrameShape(QtGui.QFrame.StyledPanel)
self._lwList.append(mid)
bottomleft = LayoutWindow(self._graph, 'vbox', 'bottomleft')
bottomleft.setFrameShape(QtGui.QFrame.StyledPanel)
self._lwList.append(bottomleft)
bottommid = LayoutWindow(self._graph, 'vbox', 'bottommid')
bottommid.setFrameShape(QtGui.QFrame.StyledPanel)
self._lwList.append(bottommid)
bottomright = LayoutWindow(self._graph, 'vbox', 'bottomright')
bottomright.setFrameShape(QtGui.QFrame.StyledPanel)
self._lwList.append(bottomright)
splitter1 = QtGui.QSplitter(QtCore.Qt.Vertical)
splitter1.addWidget(top)
splitter1.addWidget(mid)
splitter2 = QtGui.QSplitter(QtCore.Qt.Horizontal)
splitter2.addWidget(bottomleft)
splitter2.addWidget(bottommid)
splitter2.addWidget(bottomright)
splitter3 = QtGui.QSplitter(QtCore.Qt.Vertical)
splitter3.addWidget(splitter1)
splitter3.addWidget(splitter2)
hbox.addWidget(splitter3)
elif config == 3:
top = LayoutWindow(self._graph, 'vbox', 'top')
top.setFrameShape(QtGui.QFrame.StyledPanel)
top.changed.connect(self.columnAdjust)
self.topsplitter = QtGui.QSplitter(QtCore.Qt.Horizontal)
self.topsplitter.addWidget(top)
bottom = LayoutWindow(self._graph, 'vbox', 'bottom')
bottom.setFrameShape(QtGui.QFrame.StyledPanel)
bottom.changed.connect(self.columnAdjust)
self.bottomsplitter = QtGui.QSplitter(QtCore.Qt.Horizontal)
self.bottomsplitter.addWidget(bottom)
vsplitter = QtGui.QSplitter(QtCore.Qt.Vertical)
vsplitter.addWidget(self.topsplitter)
vsplitter.addWidget(self.bottomsplitter)
hbox.addWidget(vsplitter)
if self._labelWin:
# make a label box with the unique id
self.wdglabel = QtGui.QLineEdit('')
labelGroup = HidableGroupBox("Node Label")
labelLayout = QtGui.QVBoxLayout()
labelLayout.addWidget(self.wdglabel)
labelGroup.setLayout(labelLayout)
hbox.addWidget(labelGroup)
labelGroup.set_collapsed(True)
labelGroup.setToolTip(
"Displays the Label on the Canvas (Double Click)")
self.setLayout(hbox)
def getSettings(self):
'''Serialize widget ids and layout patterns into a dict.
The list of layout keys is specific to each config.
'''
s = {}
s['config'] = self.config()
s['layouts'] = {}
for lw in self.findChildren(LayoutWindow):
s['layouts'][lw.label()] = lw.getSettings()
return s
def loadSettings(self, s, nodeList):
'''Deserialize layout settings and reconstruct layout.
'''
log.debug("LayoutMaster loadSettings():")
log.debug(str(s))
# Change layout if necessary.
# After this, all layoutWindows will be populated and their labels
# set.
# TODO: initLayout() can't be called twice, yet...
if s['config'] != self._config:
self.initLayout(s['config'])
# add all necessary top and bottom columns for expanding layout
# wls: window layout settings
if self._config == 3:
curlabels = [lw.label() for lw in self.findChildren(LayoutWindow)]
for label, wls in list(s['layouts'].items()):
if label not in curlabels:
if label.startswith('top'):
self.addTopColumn(label)
else:
self.addBottomColumn(label)
# assign widgets to each layout
# wls: window layout settings
for label, wls in list(s['layouts'].items()):
log.debug("trying " + label)
for lw in self.findChildren(LayoutWindow):
if lw.label() == label:
log.debug("adding " + label)
lw.loadSettings(wls, nodeList)
def config(self):
'''returns the layout config identifier.
'''
return self._config
def addDummyTopColumn(self):
'''To allow the user to add to the next topsplitter, there must be an
open LayoutBox to drop to. -so there always needs to be one extra.'''
lab = 'top' + str(self.topsplitter.count())
self.addTopColumn(lab)
def addTopColumn(self, lab):
top = LayoutWindow(self._graph, 'vbox', lab)
top.setFrameShape(QtGui.QFrame.StyledPanel)
top.changed.connect(self.columnAdjust)
self.topsplitter.addWidget(top)
def addBottomColumn(self, lab):
bottom = LayoutWindow(self._graph, 'vbox', lab)
bottom.setFrameShape(QtGui.QFrame.StyledPanel)
bottom.changed.connect(self.columnAdjust)
self.bottomsplitter.addWidget(bottom)
def emptyCount(self):
emptycnt = 0
for lw in self.topsplitter.findChildren(LayoutWindow):
emptycnt += (lw.count() == 0)
return emptycnt
def columnAdjust(self):
'''For config 3, variable column count.'''
# add leading dummy box
if self.emptyCount() > 1:
for lw in self.topsplitter.findChildren(LayoutWindow):
if self.emptyCount() > 1:
if lw.count() == 0:
lw.setParent(None)
lw.close()
elif self.emptyCount() < 1:
self.addDummyTopColumn()
# fewer columns than hstack
if (self.topsplitter.count() - 1) > self.bottomsplitter.count():
lab = 'bottom' + str(self.bottomsplitter.count())
self.addBottomColumn(lab)
# more columns than hstack
elif (self.topsplitter.count() - 1) < self.bottomsplitter.count():
# need to make sure they are empty first
for lw in self.bottomsplitter.findChildren(LayoutWindow):
if (self.topsplitter.count() - 1) < self.bottomsplitter.count():
if not lw.count():
lw.setParent(None)
lw.close()
def widgetCount(self):
cnt = 0
for lw in self.findChildren(LayoutWindow):
cnt += lw.count()
return cnt
def removeLayoutWindowRefs(self):
# remove self (THIS widget) from the graph's layout list
try:
l = self._graph._layoutwindowList
l[l.index(self)] = None
except:
log.critical("FixMe: this is why graph side operations shouldn't be " + "performed within a graph child.")
def forceClose(self):
'''Return all menu widgets to proper node before closing.
'''
log.debug("force closing widget")
if self.widgetCount():
for lw in self.findChildren(LayoutWindow):
for wdg in lw.getGPIWidgets():
wdg.returnToOrigin()
lw.close()
self.close()
def closeEvent(self, event):
if self._isMacroWdg:
log.debug("is a macro so continue to close")
event.accept()
return
# don't close if any are attached
if not self.widgetCount():
self.removeLayoutWindowRefs()
event.accept()
return
event.ignore()