2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ------------------------------------------------------------------------------------------------------------
10 # ------------------------------------------------------------------------------------------------------------
13 from qt_compat
import qt_config
16 from PyQt5
.QtCore
import Qt
, QSize
, QRect
, QEvent
17 from PyQt5
.QtGui
import QColor
, QPainter
, QPixmap
18 from PyQt5
.QtWidgets
import QAbstractItemView
, QListWidget
, QListWidgetItem
, QMessageBox
20 from PyQt6
.QtCore
import Qt
, QSize
, QRect
, QEvent
21 from PyQt6
.QtGui
import QColor
, QPainter
, QPixmap
22 from PyQt6
.QtWidgets
import QAbstractItemView
, QListWidget
, QListWidgetItem
, QMessageBox
24 # ------------------------------------------------------------------------------------------------------------
25 # Imports (Custom Stuff)
27 from carla_backend
import CUSTOM_DATA_TYPE_PROPERTY
, MACOS
28 from carla_shared
import gCarla
, CustomMessageBox
29 from carla_skin
import createPluginSlot
31 # ------------------------------------------------------------------------------------------------------------
34 class RackListItem(QListWidgetItem
):
35 kRackItemType
= QListWidgetItem
.UserType
+ 1
38 def __init__(self
, parent
, pluginId
, useClassicSkin
):
39 QListWidgetItem
.__init
__(self
, parent
, self
.kRackItemType
)
40 self
.host
= parent
.host
42 # ----------------------------------------------------------------------------------------------------
46 self
.fPluginId
= pluginId
49 color
= self
.host
.get_custom_data_value(pluginId
, CUSTOM_DATA_TYPE_PROPERTY
, "CarlaColor")
50 skin
= self
.host
.get_custom_data_value(pluginId
, CUSTOM_DATA_TYPE_PROPERTY
, "CarlaSkin")
51 compact
= bool(self
.host
.get_custom_data_value(pluginId
,
52 CUSTOM_DATA_TYPE_PROPERTY
,
53 "CarlaSkinIsCompacted") == "true")
57 color
= tuple(int(i
) for i
in color
.split(";",3))
58 except Exception as e
:
59 print("Color value decode failed for", color
, "error was:", e
)
64 if useClassicSkin
and not skin
:
70 'compact': compact
and skin
!= "classic",
73 self
.setFlags(Qt
.ItemIsSelectable|Qt
.ItemIsEnabled
)
74 #self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled|Qt.ItemIsDragEnabled)
76 # ----------------------------------------------------------------------------------------------------
79 self
.recreateWidget(firstInit
= True)
81 # --------------------------------------------------------------------------------------------------------
84 if self
.fWidget
is None:
90 self
.fParent
.customClearSelection()
91 self
.fParent
.setItemWidget(self
, None)
93 widget
.fEditDialog
.close()
94 widget
.fEditDialog
.setParent(None)
95 widget
.fEditDialog
.deleteLater()
96 widget
.fEditDialog
= None
99 widget
.setParent(None)
103 def getEditDialog(self
):
104 if self
.fWidget
is None:
107 return self
.fWidget
.fEditDialog
109 def getPluginId(self
):
110 return self
.fPluginId
115 def isCompacted(self
):
116 return self
.fOptions
['compact']
118 def isGuiShown(self
):
119 if self
.fWidget
is None or self
.fWidget
.b_gui
is not None:
121 return self
.fWidget
.b_gui
.isChecked()
123 # --------------------------------------------------------------------------------------------------------
125 def setPluginId(self
, pluginId
):
126 self
.fPluginId
= pluginId
128 if self
.fWidget
is not None:
129 self
.fWidget
.setPluginId(pluginId
)
131 def setSelected(self
, select
):
132 if self
.fWidget
is not None:
133 self
.fWidget
.setSelected(select
)
135 QListWidgetItem
.setSelected(self
, select
)
137 # --------------------------------------------------------------------------------------------------------
139 def setCompacted(self
, compact
):
140 self
.fOptions
['compact'] = compact
142 # --------------------------------------------------------------------------------------------------------
145 if self
.fOptions
['compact']:
147 self
.recreateWidget(True)
150 if not self
.fOptions
['compact']:
152 self
.recreateWidget(True)
154 def recreateWidget(self
, invertCompactOption
= False, firstInit
= False, newColor
= None, newSkin
= None):
155 if invertCompactOption
:
156 self
.fOptions
['compact'] = not self
.fOptions
['compact']
157 if newColor
is not None:
158 self
.fOptions
['color'] = newColor
159 if newSkin
is not None:
160 self
.fOptions
['skin'] = newSkin
164 if self
.fWidget
is not None and self
.fWidget
.b_gui
is not None:
165 wasGuiShown
= self
.fWidget
.b_gui
.isChecked()
169 self
.fWidget
= createPluginSlot(self
.fParent
, self
.host
, self
.fPluginId
, self
.fOptions
)
170 self
.fWidget
.setFixedHeight(self
.fWidget
.getFixedHeight())
172 if wasGuiShown
and self
.fWidget
.b_gui
is not None:
173 self
.fWidget
.b_gui
.setChecked(True)
175 self
.setSizeHint(QSize(self
.kMinimumWidth
, self
.fWidget
.getFixedHeight()))
177 self
.fParent
.setItemWidget(self
, self
.fWidget
)
180 self
.host
.set_custom_data(self
.fPluginId
, CUSTOM_DATA_TYPE_PROPERTY
,
181 "CarlaSkinIsCompacted", "true" if self
.fOptions
['compact'] else "false")
183 def recreateWidget2(self
, wasCompacted
, wasGuiShown
):
184 self
.fOptions
['compact'] = wasCompacted
188 self
.fWidget
= createPluginSlot(self
.fParent
, self
.host
, self
.fPluginId
, self
.fOptions
)
189 self
.fWidget
.setFixedHeight(self
.fWidget
.getFixedHeight())
191 if wasGuiShown
and self
.fWidget
.b_gui
is not None:
192 self
.fWidget
.b_gui
.setChecked(True)
194 self
.setSizeHint(QSize(self
.kMinimumWidth
, self
.fWidget
.getFixedHeight()))
196 self
.fParent
.setItemWidget(self
, self
.fWidget
)
198 self
.host
.set_custom_data(self
.fPluginId
, CUSTOM_DATA_TYPE_PROPERTY
,
199 "CarlaSkinIsCompacted", "true" if wasCompacted
else "false")
201 # ------------------------------------------------------------------------------------------------------------
204 class RackListWidget(QListWidget
):
205 def __init__(self
, parent
):
206 QListWidget
.__init
__(self
, parent
)
210 exts
= gCarla
.utils
.get_supported_file_extensions()
212 self
.fSupportedExtensions
= tuple(("." + i
) for i
in exts
)
213 self
.fLastSelectedItem
= None
214 self
.fWasLastDragValid
= False
216 self
.fPixmapL
= QPixmap(":/bitmaps/rack_interior_left.png")
217 self
.fPixmapR
= QPixmap(":/bitmaps/rack_interior_right.png")
218 self
.fPixmapWidth
= self
.fPixmapL
.width()
220 self
.setMinimumWidth(RackListItem
.kMinimumWidth
)
221 self
.setSelectionMode(QAbstractItemView
.SingleSelection
)
222 self
.setSortingEnabled(False)
224 self
.setDragEnabled(True)
225 self
.setDragDropMode(QAbstractItemView
.DropOnly
)
226 self
.setDropIndicatorShown(True)
227 self
.viewport().setAcceptDrops(True)
231 # --------------------------------------------------------------------------------------------------------
233 def createItem(self
, pluginId
, useClassicSkin
):
234 return RackListItem(self
, pluginId
, useClassicSkin
)
236 def getPluginCount(self
):
237 return self
.fParent
.getPluginCount()
239 def setHostAndParent(self
, host
, parent
):
241 self
.fParent
= parent
243 # --------------------------------------------------------------------------------------------------------
245 def customClearSelection(self
):
246 self
.setCurrentRow(-1)
247 self
.clearSelection()
250 def isDragUrlValid(self
, filename
):
254 if filename
[-1] == '/':
255 filename
= filename
[:-1]
257 lfilename
= filename
.lower()
259 if os
.path
.isdir(filename
):
260 #if os.path.exists(os.path.join(filename, "manifest.ttl")):
262 if MACOS
and lfilename
.endswith(".vst"):
264 if lfilename
.endswith(".vst3") and ".vst3" in self
.fSupportedExtensions
:
267 elif os
.path
.isfile(filename
):
268 if lfilename
.endswith(self
.fSupportedExtensions
):
273 # --------------------------------------------------------------------------------------------------------
275 def dragEnterEvent(self
, event
):
276 urls
= event
.mimeData().urls()
279 if self
.isDragUrlValid(url
.toLocalFile()):
280 self
.fWasLastDragValid
= True
281 event
.acceptProposedAction()
284 self
.fWasLastDragValid
= False
285 QListWidget
.dragEnterEvent(self
, event
)
287 def dragMoveEvent(self
, event
):
288 if not self
.fWasLastDragValid
:
289 QListWidget
.dragMoveEvent(self
, event
)
292 event
.acceptProposedAction()
294 tryItem
= self
.itemAt(event
.pos())
296 if tryItem
is not None:
297 self
.setCurrentRow(tryItem
.getPluginId())
299 self
.setCurrentRow(-1)
301 def dragLeaveEvent(self
, event
):
302 if self
.fWasLastDragValid
:
303 self
.fWasLastDragValid
= False
304 QListWidget
.dragLeaveEvent(self
, event
)
306 # --------------------------------------------------------------------------------------------------------
308 # FIXME: this needs some attention
310 # if dropping project file over 1 plugin, load it in rack or patchbay
311 # if dropping regular files over 1 plugin, keep replacing plugins
313 def dropEvent(self
, event
):
314 event
.acceptProposedAction()
316 urls
= event
.mimeData().urls()
321 tryItem
= self
.itemAt(event
.pos())
323 if tryItem
is not None:
324 pluginId
= tryItem
.getPluginId()
330 self
.host
.replace_plugin(pluginId
)
333 if pluginId
> self
.host
.get_current_plugin_count():
336 filename
= url
.toLocalFile()
341 if filename
[-1] == '/':
342 filename
= filename
[:-1]
344 if not self
.host
.load_file(filename
):
345 CustomMessageBox(self
, QMessageBox
.Critical
, self
.tr("Error"),
346 self
.tr("Failed to load file"),
347 self
.host
.get_last_error(), QMessageBox
.Ok
, QMessageBox
.Ok
)
350 if filename
.endswith(".carxp"):
351 gCarla
.gui
.loadExternalCanvasGroupPositionsIfNeeded(filename
)
353 if tryItem
is not None:
354 self
.host
.replace_plugin(self
.host
.get_max_plugin_number())
355 #tryItem.widget.setActive(True, True, True)
357 # --------------------------------------------------------------------------------------------------------
359 def mousePressEvent(self
, event
):
360 if self
.itemAt(event
.pos()) is None and self
.currentRow() != -1:
362 self
.customClearSelection()
365 QListWidget
.mousePressEvent(self
, event
)
367 def changeEvent(self
, event
):
368 if event
.type() in (QEvent
.StyleChange
, QEvent
.PaletteChange
):
370 QListWidget
.changeEvent(self
, event
)
372 def paintEvent(self
, event
):
373 painter
= QPainter(self
.viewport())
376 height
= self
.height()
377 imgL_rect
= QRect(0, 0, self
.fPixmapWidth
, height
)
378 imgR_rect
= QRect(width
-self
.fPixmapWidth
, 0, self
.fPixmapWidth
, height
)
380 painter
.setBrush(self
.rail_col
)
381 painter
.setPen(Qt
.NoPen
)
382 painter
.drawRects(imgL_rect
, imgR_rect
)
383 painter
.setCompositionMode(QPainter
.CompositionMode_Multiply
)
384 painter
.drawTiledPixmap(imgL_rect
, self
.fPixmapL
)
385 painter
.drawTiledPixmap(imgR_rect
, self
.fPixmapR
)
386 painter
.setCompositionMode(QPainter
.CompositionMode_Plus
)
387 painter
.drawTiledPixmap(imgL_rect
, self
.fPixmapL
)
388 painter
.drawTiledPixmap(imgR_rect
, self
.fPixmapR
)
389 painter
.setCompositionMode(QPainter
.CompositionMode_SourceOver
)
391 painter
.setPen(self
.edge_col
)
392 painter
.setBrush(Qt
.NoBrush
)
393 painter
.drawRect(self
.fPixmapWidth
, 0, width
-self
.fPixmapWidth
*2, height
)
395 QListWidget
.paintEvent(self
, event
)
397 def selectionChanged(self
, selected
, deselected
):
398 for index
in deselected
.indexes():
399 item
= self
.itemFromIndex(index
)
401 item
.setSelected(False)
403 for index
in selected
.indexes():
404 item
= self
.itemFromIndex(index
)
406 item
.setSelected(True)
408 QListWidget
.selectionChanged(self
, selected
, deselected
)
410 # --------------------------------------------------------------------------------------------------------
412 def _updateStyle(self
):
413 palette
= self
.palette()
415 bg_color
= palette
.window().color()
416 base_color
= palette
.base().color()
417 text_color
= palette
.text().color()
418 r0
,g0
,b0
,_
= bg_color
.getRgb()
419 r1
,g1
,b1
,_
= text_color
.getRgb()
421 self
.rail_col
= QColor(int((r0
*3+r1
)/4), int((g0
*3+g1
)/4), int((b0
*3+b1
)/4))
422 self
.edge_col
= (self
.rail_col
if self
.rail_col
.blackF() > base_color
.blackF() else base_color
).darker(115)
424 # ------------------------------------------------------------------------------------------------------------