2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ------------------------------------------------------------------------------------------------------------
10 from qt_compat
import qt_config
13 from PyQt5
.QtCore
import qCritical
, Qt
, QLineF
, QPointF
, QRectF
, QTimer
14 from PyQt5
.QtGui
import QCursor
, QFont
, QFontMetrics
, QPainter
, QPainterPath
, QPen
, QPolygonF
15 from PyQt5
.QtWidgets
import QGraphicsItem
, QMenu
17 from PyQt6
.QtCore
import qCritical
, Qt
, QLineF
, QPointF
, QRectF
, QTimer
18 from PyQt6
.QtGui
import QCursor
, QFont
, QFontMetrics
, QPainter
, QPainterPath
, QPen
, QPolygonF
19 from PyQt6
.QtWidgets
import QGraphicsItem
, QMenu
21 # ------------------------------------------------------------------------------------------------------------
35 ACTION_PORTS_DISCONNECT
,
44 from .canvasbezierlinemov
import CanvasBezierLineMov
45 from .canvaslinemov
import CanvasLineMov
46 from .theme
import Theme
47 from .utils
import CanvasGetFullPortName
, CanvasGetPortConnectionList
49 # ------------------------------------------------------------------------------------------------------------
51 class CanvasPort(QGraphicsItem
):
52 def __init__(self
, group_id
, port_id
, port_name
, port_mode
, port_type
, is_alternate
, parent
):
53 QGraphicsItem
.__init
__(self
)
54 self
.setParentItem(parent
)
56 # Save Variables, useful for later
57 self
.m_group_id
= group_id
58 self
.m_port_id
= port_id
59 self
.m_port_mode
= port_mode
60 self
.m_port_type
= port_type
61 self
.m_port_name
= port_name
62 self
.m_is_alternate
= is_alternate
65 self
.m_port_width
= 15
66 self
.m_port_height
= canvas
.theme
.port_height
67 self
.m_port_font
= QFont()
68 self
.m_port_font
.setFamily(canvas
.theme
.port_font_name
)
69 self
.m_port_font
.setPixelSize(canvas
.theme
.port_font_size
)
70 self
.m_port_font
.setWeight(canvas
.theme
.port_font_state
)
72 self
.m_line_mov
= None
73 self
.m_hover_item
= None
75 self
.m_mouse_down
= False
76 self
.m_cursor_moving
= False
78 self
.setFlags(QGraphicsItem
.ItemIsSelectable
)
80 if options
.auto_select_items
:
81 self
.setAcceptHoverEvents(True)
84 return self
.m_group_id
89 def getPortMode(self
):
90 return self
.m_port_mode
92 def getPortType(self
):
93 return self
.m_port_type
95 def getPortName(self
):
96 return self
.m_port_name
98 def getFullPortName(self
):
99 return self
.parentItem().getGroupName() + ":" + self
.m_port_name
101 def getPortWidth(self
):
102 return self
.m_port_width
104 def getPortHeight(self
):
105 return self
.m_port_height
107 def setPortMode(self
, port_mode
):
108 self
.m_port_mode
= port_mode
111 def setPortType(self
, port_type
):
112 self
.m_port_type
= port_type
115 def setPortName(self
, port_name
):
116 metrics
= QFontMetrics(self
.m_port_font
)
118 if QT_VERSION
>= 0x50b00:
119 width1
= metrics
.horizontalAdvance(port_name
)
120 width2
= metrics
.horizontalAdvance(self
.m_port_name
)
122 width1
= metrics
.width(port_name
)
123 width2
= metrics
.width(self
.m_port_name
)
126 QTimer
.singleShot(0, canvas
.scene
.update
)
128 self
.m_port_name
= port_name
131 def setPortWidth(self
, port_width
):
132 if port_width
< self
.m_port_width
:
133 QTimer
.singleShot(0, canvas
.scene
.update
)
135 self
.m_port_width
= port_width
139 return CanvasPortType
141 def hoverEnterEvent(self
, event
):
142 if options
.auto_select_items
:
143 self
.setSelected(True)
144 QGraphicsItem
.hoverEnterEvent(self
, event
)
146 def hoverLeaveEvent(self
, event
):
147 if options
.auto_select_items
:
148 self
.setSelected(False)
149 QGraphicsItem
.hoverLeaveEvent(self
, event
)
151 def mousePressEvent(self
, event
):
152 if event
.button() == Qt
.MiddleButton
or event
.source() == Qt
.MouseEventSynthesizedByApplication
:
156 if self
.m_mouse_down
:
157 self
.handleMouseRelease()
158 self
.m_hover_item
= None
159 self
.m_mouse_down
= bool(event
.button() == Qt
.LeftButton
)
160 self
.m_cursor_moving
= False
161 QGraphicsItem
.mousePressEvent(self
, event
)
163 def mouseMoveEvent(self
, event
):
164 if not self
.m_mouse_down
:
165 QGraphicsItem
.mouseMoveEvent(self
, event
)
170 if not self
.m_cursor_moving
:
171 self
.setCursor(QCursor(Qt
.CrossCursor
))
172 self
.m_cursor_moving
= True
174 for connection
in canvas
.connection_list
:
176 (connection
.group_out_id
== self
.m_group_id
and
177 connection
.port_out_id
== self
.m_port_id
)
179 (connection
.group_in_id
== self
.m_group_id
and
180 connection
.port_in_id
== self
.m_port_id
)
182 connection
.widget
.setLocked(True)
184 if not self
.m_line_mov
:
185 if options
.use_bezier_lines
:
186 self
.m_line_mov
= CanvasBezierLineMov(self
.m_port_mode
, self
.m_port_type
, self
)
188 self
.m_line_mov
= CanvasLineMov(self
.m_port_mode
, self
.m_port_type
, self
)
190 canvas
.last_z_value
+= 1
191 self
.m_line_mov
.setZValue(canvas
.last_z_value
)
192 canvas
.last_z_value
+= 1
193 self
.parentItem().setZValue(canvas
.last_z_value
)
196 items
= canvas
.scene
.items(event
.scenePos(), Qt
.ContainsItemShape
, Qt
.AscendingOrder
)
197 #for i in range(len(items)):
198 for _
, itemx
in enumerate(items
):
199 if itemx
.type() != CanvasPortType
:
203 if item
is None or itemx
.parentItem().zValue() > item
.parentItem().zValue():
206 if self
.m_hover_item
and self
.m_hover_item
!= item
:
207 self
.m_hover_item
.setSelected(False)
210 if item
.getPortMode() != self
.m_port_mode
and item
.getPortType() == self
.m_port_type
:
211 item
.setSelected(True)
212 self
.m_hover_item
= item
214 self
.m_hover_item
= None
216 self
.m_hover_item
= None
218 self
.m_line_mov
.updateLinePos(event
.scenePos())
220 def handleMouseRelease(self
):
221 if self
.m_mouse_down
:
222 if self
.m_line_mov
is not None:
223 item
= self
.m_line_mov
224 self
.m_line_mov
= None
225 canvas
.scene
.removeItem(item
)
228 for connection
in canvas
.connection_list
:
230 (connection
.group_out_id
== self
.m_group_id
and
231 connection
.port_out_id
== self
.m_port_id
)
233 (connection
.group_in_id
== self
.m_group_id
and
234 connection
.port_in_id
== self
.m_port_id
)
236 connection
.widget
.setLocked(False)
238 if self
.m_hover_item
:
239 # TODO: a better way to check already existing connection
240 for connection
in canvas
.connection_list
:
241 hover_group_id
= self
.m_hover_item
.getGroupId()
242 hover_port_id
= self
.m_hover_item
.getPortId()
244 # FIXME clean this big if stuff
246 (connection
.group_out_id
== self
.m_group_id
and
247 connection
.port_out_id
== self
.m_port_id
and
248 connection
.group_in_id
== hover_group_id
and
249 connection
.port_in_id
== hover_port_id
)
251 (connection
.group_out_id
== hover_group_id
and
252 connection
.port_out_id
== hover_port_id
and
253 connection
.group_in_id
== self
.m_group_id
and
254 connection
.port_in_id
== self
.m_port_id
)
256 canvas
.callback(ACTION_PORTS_DISCONNECT
, connection
.connection_id
, 0, "")
259 if self
.m_port_mode
== PORT_MODE_OUTPUT
:
260 conn
= "%i:%i:%i:%i" % (self
.m_group_id
, self
.m_port_id
,
261 self
.m_hover_item
.getGroupId(), self
.m_hover_item
.getPortId())
262 canvas
.callback(ACTION_PORTS_CONNECT
, 0, 0, conn
)
264 conn
= "%i:%i:%i:%i" % (self
.m_hover_item
.getGroupId(),
265 self
.m_hover_item
.getPortId(), self
.m_group_id
, self
.m_port_id
)
266 canvas
.callback(ACTION_PORTS_CONNECT
, 0, 0, conn
)
268 canvas
.scene
.clearSelection()
270 if self
.m_cursor_moving
:
273 self
.m_hover_item
= None
274 self
.m_mouse_down
= False
275 self
.m_cursor_moving
= False
277 def mouseReleaseEvent(self
, event
):
278 if event
.button() == Qt
.LeftButton
:
279 self
.handleMouseRelease()
280 QGraphicsItem
.mouseReleaseEvent(self
, event
)
282 def contextMenuEvent(self
, event
):
285 canvas
.scene
.clearSelection()
286 self
.setSelected(True)
289 discMenu
= QMenu("Disconnect", menu
)
291 conn_list
= CanvasGetPortConnectionList(self
.m_group_id
, self
.m_port_id
)
293 if len(conn_list
) > 0:
294 for conn_id
, group_id
, port_id
in conn_list
:
295 act_x_disc
= discMenu
.addAction(CanvasGetFullPortName(group_id
, port_id
))
296 act_x_disc
.setData(conn_id
)
297 act_x_disc
.triggered
.connect(canvas
.qobject
.PortContextMenuDisconnect
)
299 act_x_disc
= discMenu
.addAction("No connections")
300 act_x_disc
.setEnabled(False)
302 menu
.addMenu(discMenu
)
303 act_x_disc_all
= menu
.addAction("Disconnect &All")
304 act_x_sep_1
= menu
.addSeparator()
305 act_x_info
= menu
.addAction("Get &Info")
306 act_x_rename
= menu
.addAction("&Rename")
308 if not features
.port_info
:
309 act_x_info
.setVisible(False)
311 if not features
.port_rename
:
312 act_x_rename
.setVisible(False)
314 if not (features
.port_info
and features
.port_rename
):
315 act_x_sep_1
.setVisible(False)
317 act_selected
= menu
.exec_(event
.screenPos())
319 if act_selected
== act_x_disc_all
:
320 self
.triggerDisconnect(conn_list
)
322 elif act_selected
== act_x_info
:
323 canvas
.callback(ACTION_PORT_INFO
, self
.m_group_id
, self
.m_port_id
, "")
325 elif act_selected
== act_x_rename
:
326 canvas
.callback(ACTION_PORT_RENAME
, self
.m_group_id
, self
.m_port_id
, "")
328 def setPortSelected(self
, yesno
):
329 for connection
in canvas
.connection_list
:
331 (connection
.group_out_id
== self
.m_group_id
and
332 connection
.port_out_id
== self
.m_port_id
)
334 (connection
.group_in_id
== self
.m_group_id
and
335 connection
.port_in_id
== self
.m_port_id
)
337 connection
.widget
.updateLineSelected()
339 def itemChange(self
, change
, value
):
340 if change
== QGraphicsItem
.ItemSelectedHasChanged
:
341 self
.setPortSelected(value
)
342 return QGraphicsItem
.itemChange(self
, change
, value
)
344 def triggerDisconnect(self
, conn_list
=None):
346 conn_list
= CanvasGetPortConnectionList(self
.m_group_id
, self
.m_port_id
)
347 for conn_id
, group_id
, port_id
in conn_list
:
348 canvas
.callback(ACTION_PORTS_DISCONNECT
, conn_id
, 0, "")
350 def boundingRect(self
):
351 return QRectF(0, 0, self
.m_port_width
+ 12, self
.m_port_height
)
353 def paint(self
, painter
, option
, widget
):
355 painter
.setRenderHint(QPainter
.Antialiasing
, bool(options
.antialiasing
== ANTIALIASING_FULL
))
357 selected
= self
.isSelected()
359 if self
.m_port_type
== PORT_TYPE_AUDIO_JACK
:
360 poly_color
= theme
.port_audio_jack_bg_sel
if selected
else theme
.port_audio_jack_bg
361 poly_pen
= theme
.port_audio_jack_pen_sel
if selected
else theme
.port_audio_jack_pen
362 text_pen
= theme
.port_audio_jack_text_sel
if selected
else theme
.port_audio_jack_text
363 conn_pen
= QPen(theme
.port_audio_jack_pen_sel
)
364 elif self
.m_port_type
== PORT_TYPE_MIDI_JACK
:
365 poly_color
= theme
.port_midi_jack_bg_sel
if selected
else theme
.port_midi_jack_bg
366 poly_pen
= theme
.port_midi_jack_pen_sel
if selected
else theme
.port_midi_jack_pen
367 text_pen
= theme
.port_midi_jack_text_sel
if selected
else theme
.port_midi_jack_text
368 conn_pen
= QPen(theme
.port_midi_jack_pen_sel
)
369 elif self
.m_port_type
== PORT_TYPE_MIDI_ALSA
:
370 poly_color
= theme
.port_midi_alsa_bg_sel
if selected
else theme
.port_midi_alsa_bg
371 poly_pen
= theme
.port_midi_alsa_pen_sel
if selected
else theme
.port_midi_alsa_pen
372 text_pen
= theme
.port_midi_alsa_text_sel
if selected
else theme
.port_midi_alsa_text
373 conn_pen
= QPen(theme
.port_midi_alsa_pen_sel
)
374 elif self
.m_port_type
== PORT_TYPE_PARAMETER
:
375 poly_color
= theme
.port_parameter_bg_sel
if selected
else theme
.port_parameter_bg
376 poly_pen
= theme
.port_parameter_pen_sel
if selected
else theme
.port_parameter_pen
377 text_pen
= theme
.port_parameter_text_sel
if selected
else theme
.port_parameter_text
378 conn_pen
= QPen(theme
.port_parameter_pen_sel
)
380 qCritical("PatchCanvas::CanvasPort.paint() - invalid port type '%s'" % port_type2str(self
.m_port_type
))
384 # To prevent quality worsening
385 poly_pen
= QPen(poly_pen
)
386 poly_pen
.setWidthF(poly_pen
.widthF() + 0.00001)
388 if self
.m_is_alternate
:
389 poly_color
= poly_color
.darker(180)
390 #poly_pen.setColor(poly_pen.color().darker(110))
391 #text_pen.setColor(text_pen.color()) #.darker(150))
392 #conn_pen.setColor(conn_pen.color()) #.darker(150))
394 lineHinting
= poly_pen
.widthF() / 2
396 poly_locx
= [0, 0, 0, 0, 0]
397 poly_corner_xhinting
= (float(canvas
.theme
.port_height
)/2) % floor(float(canvas
.theme
.port_height
)/2)
398 if poly_corner_xhinting
== 0:
399 poly_corner_xhinting
= 0.5 * (1 - 7 / (float(canvas
.theme
.port_height
)/2))
401 if self
.m_port_mode
== PORT_MODE_INPUT
:
402 text_pos
= QPointF(3, canvas
.theme
.port_text_ypos
)
404 if canvas
.theme
.port_mode
== Theme
.THEME_PORT_POLYGON
:
405 poly_locx
[0] = lineHinting
406 poly_locx
[1] = self
.m_port_width
+ 5 - lineHinting
407 poly_locx
[2] = self
.m_port_width
+ 12 - poly_corner_xhinting
408 poly_locx
[3] = self
.m_port_width
+ 5 - lineHinting
409 poly_locx
[4] = lineHinting
410 elif canvas
.theme
.port_mode
== Theme
.THEME_PORT_SQUARE
:
411 poly_locx
[0] = lineHinting
412 poly_locx
[1] = self
.m_port_width
+ 5 - lineHinting
413 poly_locx
[2] = self
.m_port_width
+ 5 - lineHinting
414 poly_locx
[3] = self
.m_port_width
+ 5 - lineHinting
415 poly_locx
[4] = lineHinting
417 qCritical("PatchCanvas::CanvasPort.paint() - invalid theme port mode '%s'" % canvas
.theme
.port_mode
)
421 elif self
.m_port_mode
== PORT_MODE_OUTPUT
:
422 text_pos
= QPointF(9, canvas
.theme
.port_text_ypos
)
424 if canvas
.theme
.port_mode
== Theme
.THEME_PORT_POLYGON
:
425 poly_locx
[0] = self
.m_port_width
+ 12 - lineHinting
426 poly_locx
[1] = 7 + lineHinting
427 poly_locx
[2] = 0 + poly_corner_xhinting
428 poly_locx
[3] = 7 + lineHinting
429 poly_locx
[4] = self
.m_port_width
+ 12 - lineHinting
430 elif canvas
.theme
.port_mode
== Theme
.THEME_PORT_SQUARE
:
431 poly_locx
[0] = self
.m_port_width
+ 12 - lineHinting
432 poly_locx
[1] = 5 + lineHinting
433 poly_locx
[2] = 5 + lineHinting
434 poly_locx
[3] = 5 + lineHinting
435 poly_locx
[4] = self
.m_port_width
+ 12 - lineHinting
437 qCritical("PatchCanvas::CanvasPort.paint() - invalid theme port mode '%s'" % canvas
.theme
.port_mode
)
442 qCritical("PatchCanvas::CanvasPort.paint() - invalid port mode '%s'" % port_mode2str(self
.m_port_mode
))
446 polygon
= QPolygonF()
447 polygon
+= QPointF(poly_locx
[0], lineHinting
)
448 polygon
+= QPointF(poly_locx
[1], lineHinting
)
449 polygon
+= QPointF(poly_locx
[2], float(canvas
.theme
.port_height
)/2)
450 polygon
+= QPointF(poly_locx
[3], canvas
.theme
.port_height
- lineHinting
)
451 polygon
+= QPointF(poly_locx
[4], canvas
.theme
.port_height
- lineHinting
)
452 polygon
+= QPointF(poly_locx
[0], lineHinting
)
454 if canvas
.theme
.port_bg_pixmap
:
455 portRect
= polygon
.boundingRect().adjusted(-lineHinting
+1, -lineHinting
+1, lineHinting
-1, lineHinting
-1)
456 portPos
= portRect
.topLeft()
457 painter
.drawTiledPixmap(portRect
, canvas
.theme
.port_bg_pixmap
, portPos
)
459 painter
.setBrush(poly_color
) #.lighter(200))
461 painter
.setPen(poly_pen
)
462 painter
.drawPolygon(polygon
)
464 painter
.setPen(text_pen
)
465 painter
.setFont(self
.m_port_font
)
466 painter
.drawText(text_pos
, self
.m_port_name
)
468 if canvas
.theme
.idx
== Theme
.THEME_OOSTUDIO
and canvas
.theme
.port_bg_pixmap
:
469 conn_pen
.setCosmetic(True)
470 conn_pen
.setWidthF(0.4)
471 painter
.setPen(conn_pen
)
473 if self
.m_port_mode
== PORT_MODE_INPUT
:
474 connLineX
= portRect
.left()+1
476 connLineX
= portRect
.right()-1
477 conn_path
= QPainterPath()
478 conn_path
.addRect(QRectF(connLineX
-1, portRect
.top(), 2, portRect
.height()))
479 painter
.fillPath(conn_path
, conn_pen
.brush())
480 painter
.drawLine(QLineF(connLineX
, portRect
.top(), connLineX
, portRect
.bottom()))
484 # ------------------------------------------------------------------------------------------------------------