Cleanup
[carla.git] / source / frontend / widgets / racklistwidget.py
blobf9513d17b132003a419870c48ea54357b5dea5f7
1 #!/usr/bin/env python3
2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ------------------------------------------------------------------------------------------------------------
6 # Imports (Global)
8 import os
10 # ------------------------------------------------------------------------------------------------------------
11 # Imports (PyQt)
13 from qt_compat import qt_config
15 if qt_config == 5:
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
19 elif qt_config == 6:
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 # ------------------------------------------------------------------------------------------------------------
32 # Rack Widget item
34 class RackListItem(QListWidgetItem):
35 kRackItemType = QListWidgetItem.UserType + 1
36 kMinimumWidth = 620
38 def __init__(self, parent, pluginId, useClassicSkin):
39 QListWidgetItem.__init__(self, parent, self.kRackItemType)
40 self.host = parent.host
42 # ----------------------------------------------------------------------------------------------------
43 # Internal stuff
45 self.fParent = parent
46 self.fPluginId = pluginId
47 self.fWidget = None
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")
55 if color:
56 try:
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)
60 color = None
61 else:
62 color = None
64 if useClassicSkin and not skin:
65 skin = "classic"
67 self.fOptions = {
68 'color' : color,
69 'skin' : skin,
70 'compact': compact and skin != "classic",
73 self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
74 #self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled|Qt.ItemIsDragEnabled)
76 # ----------------------------------------------------------------------------------------------------
77 # Set-up GUI
79 self.recreateWidget(firstInit = True)
81 # --------------------------------------------------------------------------------------------------------
83 def close(self):
84 if self.fWidget is None:
85 return
87 widget = self.fWidget
88 self.fWidget = 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
98 widget.close()
99 widget.setParent(None)
100 widget.deleteLater()
101 del widget
103 def getEditDialog(self):
104 if self.fWidget is None:
105 return None
107 return self.fWidget.fEditDialog
109 def getPluginId(self):
110 return self.fPluginId
112 def getWidget(self):
113 return self.fWidget
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:
120 return 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 # --------------------------------------------------------------------------------------------------------
144 def compact(self):
145 if self.fOptions['compact']:
146 return
147 self.recreateWidget(True)
149 def expand(self):
150 if not self.fOptions['compact']:
151 return
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
162 wasGuiShown = None
164 if self.fWidget is not None and self.fWidget.b_gui is not None:
165 wasGuiShown = self.fWidget.b_gui.isChecked()
167 self.close()
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)
179 if not firstInit:
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
186 self.close()
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 # ------------------------------------------------------------------------------------------------------------
202 # Rack Widget
204 class RackListWidget(QListWidget):
205 def __init__(self, parent):
206 QListWidget.__init__(self, parent)
207 self.host = None
208 self.fParent = None
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)
229 self._updateStyle()
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):
240 self.host = host
241 self.fParent = parent
243 # --------------------------------------------------------------------------------------------------------
245 def customClearSelection(self):
246 self.setCurrentRow(-1)
247 self.clearSelection()
248 self.clearFocus()
250 def isDragUrlValid(self, filename):
251 if not filename:
252 return False
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")):
261 #return True
262 if MACOS and lfilename.endswith(".vst"):
263 return True
264 if lfilename.endswith(".vst3") and ".vst3" in self.fSupportedExtensions:
265 return True
267 elif os.path.isfile(filename):
268 if lfilename.endswith(self.fSupportedExtensions):
269 return True
271 return False
273 # --------------------------------------------------------------------------------------------------------
275 def dragEnterEvent(self, event):
276 urls = event.mimeData().urls()
278 for url in urls:
279 if self.isDragUrlValid(url.toLocalFile()):
280 self.fWasLastDragValid = True
281 event.acceptProposedAction()
282 return
284 self.fWasLastDragValid = False
285 QListWidget.dragEnterEvent(self, event)
287 def dragMoveEvent(self, event):
288 if not self.fWasLastDragValid:
289 QListWidget.dragMoveEvent(self, event)
290 return
292 event.acceptProposedAction()
294 tryItem = self.itemAt(event.pos())
296 if tryItem is not None:
297 self.setCurrentRow(tryItem.getPluginId())
298 else:
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()
318 if not urls:
319 return
321 tryItem = self.itemAt(event.pos())
323 if tryItem is not None:
324 pluginId = tryItem.getPluginId()
325 else:
326 pluginId = -1
328 for url in urls:
329 if pluginId >= 0:
330 self.host.replace_plugin(pluginId)
331 pluginId += 1
333 if pluginId > self.host.get_current_plugin_count():
334 pluginId = -1
336 filename = url.toLocalFile()
338 if not filename:
339 continue
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)
348 continue
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:
361 event.accept()
362 self.customClearSelection()
363 return
365 QListWidget.mousePressEvent(self, event)
367 def changeEvent(self, event):
368 if event.type() in (QEvent.StyleChange, QEvent.PaletteChange):
369 self._updateStyle()
370 QListWidget.changeEvent(self, event)
372 def paintEvent(self, event):
373 painter = QPainter(self.viewport())
375 width = self.width()
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)
400 if item is not None:
401 item.setSelected(False)
403 for index in selected.indexes():
404 item = self.itemFromIndex(index)
405 if item is not None:
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 # ------------------------------------------------------------------------------------------------------------