CMS 3D CMS Logo

ConnectableWidget.py
Go to the documentation of this file.
1 import logging
2 
3 from PyQt4.QtCore import QCoreApplication, QRect, QSize, QPoint, Qt
4 from PyQt4.QtGui import QMouseEvent, QPen, QColor
5 
6 from Vispa.Gui.VispaWidget import VispaWidget
7 from Vispa.Gui.PortWidget import PortWidget,SinkPort,SourcePort
8 from Vispa.Gui.VispaWidgetOwner import VispaWidgetOwner
9 from Vispa.Gui.MenuWidget import MenuWidget
10 
12  """ Widget which can be connection by PortConnections to other selectable widgets.
13 
14  Supports showing source and sink ports.
15  The widget is owner of PortWidgets.
16  """
17 
18  BACKGROUND_SHAPE = 'ROUNDRECT'
19  SHOW_PORT_NAMES = False
20  SHOW_PORT_LINES = False
21 
22  # possible positions for port names
23  PORT_NAMES_NEXT_TO_PORTS = 0
24  PORT_NAMES_ABOVE_PORTS = 1
25 
26  # default position for port names
27  PORT_NAMES_POSITION = PORT_NAMES_NEXT_TO_PORTS
28 
29  NO_VALID_PORT_NAMES_POSITION_MESSAGE = "No valid position for port names was set."
30 
31  PORT_LINES_TARGET_X = -1 # See setShowPortNames()
32  PORT_LINES_TARGET_Y = -1
33 
34  def __init__(self, parent=None, name=None):
35  """ Constructor.
36  """
37  self._ports = []
38  self._showPortNames = False
39  self._portNamesPosition = None
40  self._showPortLines = False
41  self._menuWidget = None
42  VispaWidget.__init__(self, parent)
43  self.setShowPortNames(self.SHOW_PORT_NAMES)
44  self.setPortNamesPosition(self.PORT_NAMES_POSITION)
45  self.setShowPortLines(self.SHOW_PORT_LINES)
46 
47  if name:
48  self.setTitle(name)
49 
50  def setShowPortNames(self, show):
51  """ If True the port name's will be drawn.
52 
53  The port names wont be on the port itself.
54  Instead they will appear next to the port icons on the ConnectableWidget.
55  """
56  self._showPortNames = show
57 
58  def setPortNamesPosition(self, position):
59  """ Sets position where port names will be shown.
60 
61  Possible values are self.PORT_NAMES_NEXT_TO_PORTS and self.PORT_NAMES_ABOVE_PORTS.
62  """
63  self._portNamesPosition = position
64 
65  def setShowPortLines(self, show):
66  """ If True lines from all ports to a specific target point are drawn.
67 
68  The target point is defined by PORT_LINES_TARGET_X and PORT_LINES_TARGET_Y.
69  If both of these values are -1 the target point is set to the widget's centre.
70  """
71  self._showPortLines = show
72 
73  def getPortsHeight(self, portType):
74  """ Returns height of all ports of given type.
75 
76  portType can either be 'sink" or 'source'.
77  """
78  if portType == "sink":
79  ports = self.sinkPorts()
80  elif portType == "source":
81  ports = self.sourcePorts()
82  else:
83  return 0
84 
85  if len(ports) > 1:
86  return ports[0].y() - ports[len(ports) -1].y() + 0.5 * self.getEffectivePortHeight(ports[len(ports) -1])
87  elif len(ports) == 1:
88  return self.getEffectivePortHeight(ports[0])
89  else:
90  return 0
91 
92  def sizeHint(self):
93  """ Returns size needed to draw widget's content.
94  """
95  #logging.debug(self.__class__.__name__ + ": sizeHint()")
96  # arrangePorts() needed because it will be called in rearnangeContent() after sizeHint()
97  self.arrangePorts()
98 
99  neededWidth = self.getDistance('leftMargin', 1) + self.getDistance('rightMargin', 1)
100  neededHeight = self.getDistance('topMargin', 1) + self.getDistance('bottomMargin', 1)
101  imageSizeF = self.imageSizeF()
102 
103  # width
104  titleWidth = 0
105  if self.titleIsSet():
106  titleWidth = self.getDistance('titleFieldWidth', 1)
107 
108  bodyWidth = 0
109  sinkPortsWidth = 0
110  sourcePortsWidth = 0
111  if len(self.sinkPorts()) > 0:
112  sinkPortsWidth = self.getDistance('leftMargin', 1) + PortWidget.WIDTH
113  if len(self.sourcePorts()) > 0:
114  sourcePortsWidth = self.getDistance('rightMargin', 1) + PortWidget.WIDTH
115 
116  if self._showPortNames:
117  maxSinkTitleWidth = self._getMaxSinkTitleWidth()
118  maxSourceTitleWidth = self._getMaxSourceTitleWidth()
119  if self._portNamesPosition == self.PORT_NAMES_NEXT_TO_PORTS:
120  bodyWidth += maxSinkTitleWidth + self.getDistance('rightMargin', 1) + maxSourceTitleWidth
121  elif self._portNamesPosition == self.PORT_NAMES_ABOVE_PORTS:
122  if maxSinkTitleWidth > PortWidget.WIDTH:
123  sinkPortsWidth = 0#self.getDistance('leftMargin', 1)
124  if maxSourceTitleWidth > PortWidget.WIDTH:
125  sourcePortsWidth = 0#self.getDistance('rightMargin', 1)
126  #bodyWidth += maxSinkTitleWidth + self.getDistance('rightMargin', 1) + maxSourceTitleWidth
127  bodyWidth += maxSinkTitleWidth + maxSourceTitleWidth
128  else:
129  logging.waring(self.__class__.__name__ +": sizeHint() - "+ self.NO_VALID_PORT_NAMES_POSITION_MESSAGE)
130  bodyWidth += sinkPortsWidth + sourcePortsWidth
131 
132  if self.textFieldIsSet():
133  bodyWidth += self.getDistance('textFieldWidth', 1)
134  bodyWidth = max(imageSizeF.width() + self.getDistance("leftMargin", 1) + self.getDistance("rightMargin", 1), bodyWidth)
135 
136  neededWidth += max(titleWidth, bodyWidth)
137 
138  # height
139  if self.titleIsSet():
140  neededHeight += self.getDistance('titleFieldHeight', 1)
141 
142  sinkPortsHeight = self.getPortsHeight("sink") / self.scale()
143  sourcePortsHeight = self.getPortsHeight("source") / self.scale()
144  textFieldHeight = 0
145  if self.textFieldIsSet():
146  textFieldHeight += self.textField().getHeight()
147  neededHeight += max(sinkPortsHeight, sourcePortsHeight, textFieldHeight, imageSizeF.height())
148  if bodyWidth != 0:
149  neededHeight += self.getDistance('bottomMargin', 1) # gap between header and body
150  if self._showPortNames and (len(self.sinkPorts()) > 1 or len(self.sourcePorts()) > 1):
151  neededHeight += self.getDistance('bottomMargin', 1) # additional gap for port names
152 
153  return QSize(neededWidth, neededHeight)
154 
155  def defineDistances(self, keepDefaultRatio=False):
156  """ Extends distances of VispaWidget by the additionally needed distances for displaying ports.
157  """
158  #if scale == None:
159  # scale = self.scale()
160  scale = 1.0
161 
162  if not VispaWidget.defineDistances(self, keepDefaultRatio):
163  return False
164  if len(self.sinkPorts()) > 0:
165  self.distances()['textFieldX'] += PortWidget.WIDTH * scale + self.distances()['leftMargin']
166  self.distances()['textFieldRight'] = self.distances()['textFieldX'] + self.distances()['textFieldWidth']
167  if self._showPortNames:
168  self.distances()['textFieldX'] += self._getMaxSinkTitleWidth() + self.distances()['leftMargin']
169  self.distances()['textFieldRight'] += self._getMaxSinkTitleWidth() + self.distances()['leftMargin']
170 
171  firstPortY = self.distances()['height'] - self.distances()['bottomMargin'] - PortWidget.HEIGHT * scale
172  self.distances()['firstSinkX'] = self.distances()['leftMargin']
173  self.distances()['firstSinkY'] = firstPortY
174 
175  if self.textFieldIsSet():
176  self.distances()['firstSourceX'] = self.distances()['textFieldRight'] + self.distances()['leftMargin']
177  #else:
178  self.distances()['firstSourceX'] = self.distances()['width'] - self.distances()['leftMargin'] - PortWidget.WIDTH * scale
179  self.distances()['firstSourceY'] = firstPortY
180 
181  return True
182 
183 # def scaleChanged(self):
184 # """ Arranges ports when scale has changed.
185 # """
186 # VispaWidget.scaleChanged(self)
187 # self.arrangePorts()
188 
189  def setZoom(self, zoom):
190  """ Arranges ports when zoom has changed.
191  """
192  VispaWidget.setZoom(self, zoom)
193  #self.arrangePorts()
194 
195  def mousePressEvent(self, event):
196  """ Makes sure event is forwarded to both base classes.
197 
198  If position of event is within the dropArea of a port a QMouseEvent is sent to the port. See dropArea().
199  """
200  dropAreaPort = self.dropAreaPort(event.pos())
201  if dropAreaPort and dropAreaPort.isDragable():
202  dropAreaPort.grabMouse()
203  newEvent = QMouseEvent(event.type(), dropAreaPort.mapFromParent(event.pos()), event.button(), event.buttons(), event.modifiers())
204  QCoreApplication.instance().sendEvent(dropAreaPort, newEvent)
205  else:
206  VispaWidgetOwner.mousePressEvent(self, event)
207  VispaWidget.mousePressEvent(self, event)
208 
209  def mouseReleaseEvent(self, event):
210  """ Calls realeseMouse() to make sure the widget does not grab the mouse.
211 
212  Necessary because ConnectableWidgetOwner.propagateEventUnderConnectionWidget() may call grabMouse() on this widget.
213  """
214  #logging.debug(self.__class__.__name__ +": mouseReleaseEvent()")
215  self.releaseMouse()
216  VispaWidget.mouseReleaseEvent(self, event)
217 
218  def ports(self):
219  """ Returns list containing all source and sink port widgets.
220  """
221  return self._ports
222 
223  def addSinkPort(self, name, description=None):
224  """ Adds sink port with name and optional description text.
225  """
226  port=SinkPort(self, name)
227  self._addPort(port, description)
228  return port
229 
230  def addSourcePort(self, name, description=None):
231  """ Adds source port with name and optional description text.
232  """
233  port=SourcePort(self, name)
234  self._addPort(port, description)
235  return port
236 
237  def _addPort(self, port, description=None):
238  self._ports.append(port)
239  port.show()
240  if description:
241  self._ports[len(self._ports) - 1].setDescription(description)
243 
244  def deleteLater(self):
245  if self._menuWidget:
246  self.removeMenu()
247  for port in self._ports:
248  port.deleteAttachedConnections()
249  VispaWidget.deleteLater(self)
250 
251  def removePort(self, port):
252  """ Removes given port if it is port of this widget.
253  """
254  if port in self._ports:
255  port.deleteAttachedConnections()
256  self._ports.remove(port)
257  port.setParent(None)
258  port.deleteLater()
260 
261  def portExists(self, name, description=None):
262  for port in self._ports:
263  if port.name() == name and port.description() == description:
264  return True
265  return False
266 
267  def removePorts(self, filter=None):
268  """ Remove registered ports.
269 
270  If filter is "sink" only sinks are removed, if it is "source" only sources are removed, otherwise all ports are removed.
271  """
272  if filter and (filter != "sink" and filter != "source"):
273  filter = None
274 
275  parentIsWidgetOwner = False
276  ports = self._ports[:]
277  for port in ports:
278  if not filter or port.portType() == filter:
279  port.deleteAttachedConnections()
280  self._ports.remove(port)
281  port.setParent(None)
282  port.deleteLater()
284  self.update()
285 
286  def sinkPorts(self):
287  """ Returns list of all sink ports set.
288  """
289  return [port for port in self._ports if port.portType() == "sink"]
290  def isSink(port):
291  return port.portType() == 'sink'
292  return filter(isSink, self._ports)
293 
294  def sourcePorts(self):
295  """ Returns list of all source ports set.
296  """
297  return [port for port in self._ports if port.portType() == "source"]
298  def isSource(port):
299  return port.portType() == 'source'
300  return filter(isSource, self._ports)
301 
302  def sinkPort(self, name):
303  """ Returns sink port with given name or None if no such port is found.
304  """
305  return self.port(name, 'sink')
306 
307  def sourcePort(self, name):
308  """ Returns source port with given name or None if no such port is found.
309  """
310  return self.port(name, 'source')
311 
312  def port(self, name, type):
313  """ Returns port with given name and of given type.
314  """
315  if name == None:
316  return None
317  for port in self._ports:
318  if port.portType() == type and port.name() == name:
319  return port
320  return None
321 
322  def _getMaxPortTitleWidth(self, type):
323  if type == 'sink':
324  ports = self.sinkPorts()
325  elif type == 'source':
326  ports = self.sourcePorts()
327  else:
328  return 0
329 
330  if len(ports) < 1:
331  return 0
332  return max([port.titleField().getWidth() for port in ports])
333 
335  return self._getMaxPortTitleWidth('sink')
336 
338  return self._getMaxPortTitleWidth('source')
339 
340  def getEffectivePortHeight(self, port):
341  """ Returns the bigger value of the source height and the height of the port name text field.
342  """
343  portHeight = port.height()
344  if not self._showPortNames:
345  return portHeight
346 
347  titleHeight = port.titleField().getHeight() * self.scale()
348 
349  if self._portNamesPosition == self.PORT_NAMES_NEXT_TO_PORTS:
350  return max(portHeight, titleHeight)
351  elif self._portNamesPosition == self.PORT_NAMES_ABOVE_PORTS:
352  return portHeight + titleHeight
353  logging.waring(self.__class__.__name__ +": getEffectivePortHeight() - "+ self.NO_VALID_PORT_NAMES_POSITION_MESSAGE)
354  return 0
355 
356  def rearangeContent(self):
357  """ Arranges ports after content is rearranged by VispaWidget.
358  """
359  VispaWidget.rearangeContent(self)
360  self.arrangePorts() # has to be after rearangeContent(), prevents infinite loop (..getDistance())
361 
362  def centerSinglePortVertically(self, ports, portX):
363  """ Centers port vertically within body part (widget without title) of ModuleWidget.
364 
365  ports can either be the list of source or sink ports of ModuleWidget.
366  portX specifies the designated x coordinate to be adjustable for sinks and sources.
367  """
368  if len(ports) != 1 or not isinstance(ports[0], PortWidget):
369  logging.warning(self.__class__.__name__ + ": centerSinglePortVertically() - This method was designed for plugins with one port. Falling back to default arrangement.")
370  return False
371  ports[0].move(portX, (self.height() + self.getDistance("titleFieldHeight")) * 0.5)
372  return True
373 
374  def arrangePorts(self, filter=None):
375  """ Sets positions of set ports depending on zoom factor.
376 
377  If filter is set it may be 'sink' or 'source'.
378  """
379  if filter and (filter != "sink" and filter != "source"):
380  filter = None
381  sinkCounter = 0
382  sourceCounter = 0
383 
384  sinkX = self.getDistance('firstSinkX')
385  sinkY = self.getDistance('firstSinkY')
386  sourceX = self.getDistance('firstSourceX')
387  sourceY = self.getDistance('firstSourceY')
388 
389  for port in self._ports:
390  if port.portType() == 'sink' and (not filter or filter == "sink"):
391  sinkCounter += 1
392  port.move(sinkX, sinkY)
393  sinkY -= self.getDistance('topMargin') + self.getEffectivePortHeight(port) #+ PortWidget.HEIGHT * self.scale()
394 
395  elif port.portType() == 'source' and (not filter or filter == "source"):
396  sourceCounter += 1
397  port.move(sourceX, sourceY)
398  sourceY -= self.getDistance('topMargin') + self.getEffectivePortHeight(port) # + PortWidget.HEIGHT * self.scale()
399 
400  def drawBody(self, painter):
401  """ Takes care of painting widget content on given painter.
402  """
403  self.drawPortLines(painter)
404  self.drawTextField(painter)
405  self.drawImage(painter)
406  self.drawPortNames(painter)
407 
408  def drawPortNames(self, painter):
409  """ Paints port names next to PortWidget.
410 
411  See setShowPortNames().
412  """
413  if not self._showPortNames:
414  return
415 
416  # factor should be 0.5, but text height is calculated to big
417  titleHeightFactor = 0.4
418 
419  if self._portNamesPosition == self.PORT_NAMES_NEXT_TO_PORTS:
420  for port in self.sinkPorts():
421  if port.titleField():
422  port.titleField().paint(painter, port.x() + self.getDistance('rightMargin') + port.width(), port.y() - titleHeightFactor * port.getDistance('titleFieldHeight'), self.scale())
423 
424  for port in self.sourcePorts():
425  if port.titleField():
426  #logging.debug(self.__class__.__name__ +": drawPortNames() - "+ port.name() +", "+ str(port.titleField()._autoscaleFlag))
427  port.titleField().paint(painter, port.x() - port.getDistance('titleFieldWidth') - self.getDistance('rightMargin'), port.y() - titleHeightFactor * port.getDistance('titleFieldHeight'), self.scale())
428  elif self._portNamesPosition == self.PORT_NAMES_ABOVE_PORTS:
429  painter.pen().setWidth(2)
430  for port in self.sinkPorts():
431  if port.titleField():
432  port.titleField().paint(painter, self.getDistance('firstSinkX'), port.y() - titleHeightFactor * port.getDistance('titleFieldHeight') - port.height(), self.scale())
433 
434  for port in self.sourcePorts():
435  if port.titleField():
436  #logging.debug(self.__class__.__name__ +": drawPortNames() - "+ port.name() +", "+ str(port.titleField()._autoscaleFlag))
437  port.titleField().paint(painter, self.width() - port.getDistance('titleFieldWidth')- port.width()*0.5, port.y() - titleHeightFactor * port.getDistance('titleFieldHeight') - port.height(), self.scale())
438  else:
439  logging.waring(self.__class__.__name__ +": drawPortNames() - "+ self.NO_VALID_PORT_NAMES_POSITION_MESSAGE)
440 
441  def drawPortLines(self, painter):
442  """ Draws lines from every port to a common point.
443 
444  See setShowPortLines().
445  """
446  if not self._showPortLines:
447  return
448 
449  if self.PORT_LINES_TARGET_X == -1 and self.PORT_LINES_TARGET_Y == -1:
450  targetPoint = QPoint(self.width(), self.height() + self.getDistance("titleFieldBottom")) * 0.5
451  else:
452  targetPoint = QPoint(self.PORT_LINES_TARGET_X, self.PORT_LINES_TARGET_Y) * self.scale()
453 
454  painter.setPen(QPen(QColor('black')))
455  painter.pen().setWidth(1)
456 
457  for port in self.ports():
458  painter.drawLine(port.connectionPoint("widget"), targetPoint)
459 
460  def dropArea(self, port):
461  """ A drop area is a QRect in which the ConnectableWidget accepts dropping of PortWidgets to create connections.
462 
463  The area is greater than the port itself to make dropping easier.
464  """
465  if self._showPortNames:
466  return port.frameGeometry().united(port.titleField().getDrawRect())
467  topMargin = self.getDistance("topMargin")
468  topMarginHalf = 0.5 * topMargin
469  frameGeometry = port.frameGeometry()
470  return QRect(frameGeometry.x() - topMarginHalf,
471  frameGeometry.y() - topMarginHalf,
472  frameGeometry.width() + topMargin,
473  frameGeometry.height() + topMargin)
474 
475  def dropAreaPort(self, position):
476  """ If a port's drop area is associated with position the port is returned.
477 
478  If there is no drop area associated with the position None is returned.
479  See dropArea().
480  """
481  for port in self._ports:
482  if self.dropArea(port).contains(position):
483  return port
484  return None
485 
486  def mouseMoveEvent(self, event):
487  if bool(event.buttons() & Qt.LeftButton):
488  VispaWidget.mouseMoveEvent(self, event)
489  elif self._menuWidget:
490  self.positionizeMenuWidget()
491 
492  self.showMenu()
493 
494  def showMenu(self):
495  if self._menuWidget:
496  self._menuWidget.show()
497  self._menuWidget.raise_()
498 
499  def leaveEvent(self, event):
500  #logging.debug("%s: leaveEvent()" % self.__class__.__name__)
501  parentCursorPos = self.parent().mapFromGlobal(self.cursor().pos())
502  bottomRight = self.geometry().bottomRight()
503  if ( (not self.isSelected() or (self._menuWidget and self._menuWidget.cursorHasEntered())) \
504  and (self._menuWidget and self.parent().childAt(parentCursorPos) != self._menuWidget) ) \
505  or (self._menuWidget and ( parentCursorPos.x() > bottomRight.x() or parentCursorPos.y() > bottomRight.y())):
506  self._menuWidget.hide()
507 
508  def menu(self):
509  return self._menuWidget
510 
511  def addMenuEntry(self, name, slot=None):
512  if not self._menuWidget:
513  self._menuWidget = MenuWidget(self.parent(), self)
514  self.setMouseTracking(True)
515  return self._menuWidget.addEntry(name, slot)
516 
517  def removeMenuEntry(self, entry):
518  if not self._menuWidget:
519  return
520  self._menuWidget.removeEntry(entry)
521  if self._menuWidget.len() == 0:
522  self.removeMenu()
523 
524  def removeMenu(self):
525  self._menuWidget.hide()
526  self._menuWidget.setParent(None)
527  self._menuWidget.deleteLater()
528  self._menuWidget = None
529 
531  if self._menuWidget:
532  headerOffset = 0
533  if isinstance(self.parent(), VispaWidget):
534  headerOffset = self.parent().getDistance("titleFieldBottom")
535  self._menuWidget.move(max(0, self.x() - 0.5* (self._menuWidget.width() - self.width())),
536  max(0, headerOffset, self.y() - self._menuWidget.height() +1))
537 
538  def dragWidget(self, pPos):
539  VispaWidget.dragWidget(self, pPos)
540  self.positionizeMenuWidget()
541 
542  def select(self, sel=True, multiSelect=False):
543  VispaWidget.select(self, sel, multiSelect)
544  if not sel and self._menuWidget:
545  self._menuWidget.hide()
546 
547  def move(self, *target):
548  VispaWidget.move(self, *target)
549  if self._menuWidget:
550  self._menuWidget.hide()
552 
554  for port in self._ports:
555  port.updateAttachedConnections()
556 
558  connections = []
559  for port in self._ports:
560  connections += port.attachedConnections()
561  return connections
562 
def portExists(self, name, description=None)
def addSourcePort(self, name, description=None)
bool contains(EventRange const &lh, EventID const &rh)
Definition: EventRange.cc:38
def paint(self, painter, event=None)
def select(self, sel=True, multiSelect=False)
def getDistance(self, name, scale=None, keepDefaultRatio=False)
def defineDistances(self, keepDefaultRatio=False)
def _addPort(self, port, description=None)
def addSinkPort(self, name, description=None)
def drawTextField(self, painter)
def __init__(self, parent=None, name=None)