Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

425 lines
14KB

  1. #!/usr/bin/env python3
  2. # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
  3. # SPDX-License-Identifier: GPL-2.0-or-later
  4. # ------------------------------------------------------------------------------------------------------------
  5. # Imports (Global)
  6. import os
  7. # ------------------------------------------------------------------------------------------------------------
  8. # Imports (PyQt)
  9. from qt_compat import qt_config
  10. if qt_config == 5:
  11. from PyQt5.QtCore import Qt, QSize, QRect, QEvent
  12. from PyQt5.QtGui import QColor, QPainter, QPixmap
  13. from PyQt5.QtWidgets import QAbstractItemView, QListWidget, QListWidgetItem, QMessageBox
  14. elif qt_config == 6:
  15. from PyQt6.QtCore import Qt, QSize, QRect, QEvent
  16. from PyQt6.QtGui import QColor, QPainter, QPixmap
  17. from PyQt6.QtWidgets import QAbstractItemView, QListWidget, QListWidgetItem, QMessageBox
  18. # ------------------------------------------------------------------------------------------------------------
  19. # Imports (Custom Stuff)
  20. from carla_backend import CUSTOM_DATA_TYPE_PROPERTY, MACOS
  21. from carla_shared import gCarla, CustomMessageBox
  22. from carla_skin import createPluginSlot
  23. # ------------------------------------------------------------------------------------------------------------
  24. # Rack Widget item
  25. class RackListItem(QListWidgetItem):
  26. kRackItemType = QListWidgetItem.UserType + 1
  27. kMinimumWidth = 620
  28. def __init__(self, parent, pluginId, useClassicSkin):
  29. QListWidgetItem.__init__(self, parent, self.kRackItemType)
  30. self.host = parent.host
  31. # ----------------------------------------------------------------------------------------------------
  32. # Internal stuff
  33. self.fParent = parent
  34. self.fPluginId = pluginId
  35. self.fWidget = None
  36. color = self.host.get_custom_data_value(pluginId, CUSTOM_DATA_TYPE_PROPERTY, "CarlaColor")
  37. skin = self.host.get_custom_data_value(pluginId, CUSTOM_DATA_TYPE_PROPERTY, "CarlaSkin")
  38. compact = bool(self.host.get_custom_data_value(pluginId,
  39. CUSTOM_DATA_TYPE_PROPERTY,
  40. "CarlaSkinIsCompacted") == "true")
  41. if color:
  42. try:
  43. color = tuple(int(i) for i in color.split(";",3))
  44. except Exception as e:
  45. print("Color value decode failed for", color, "error was:", e)
  46. color = None
  47. else:
  48. color = None
  49. if useClassicSkin and not skin:
  50. skin = "classic"
  51. self.fOptions = {
  52. 'color' : color,
  53. 'skin' : skin,
  54. 'compact': compact and skin != "classic",
  55. }
  56. self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
  57. #self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled|Qt.ItemIsDragEnabled)
  58. # ----------------------------------------------------------------------------------------------------
  59. # Set-up GUI
  60. self.recreateWidget(firstInit = True)
  61. # --------------------------------------------------------------------------------------------------------
  62. def close(self):
  63. if self.fWidget is None:
  64. return
  65. widget = self.fWidget
  66. self.fWidget = None
  67. self.fParent.customClearSelection()
  68. self.fParent.setItemWidget(self, None)
  69. widget.fEditDialog.close()
  70. widget.fEditDialog.setParent(None)
  71. widget.fEditDialog.deleteLater()
  72. widget.fEditDialog = None
  73. widget.close()
  74. widget.setParent(None)
  75. widget.deleteLater()
  76. del widget
  77. def getEditDialog(self):
  78. if self.fWidget is None:
  79. return None
  80. return self.fWidget.fEditDialog
  81. def getPluginId(self):
  82. return self.fPluginId
  83. def getWidget(self):
  84. return self.fWidget
  85. def isCompacted(self):
  86. return self.fOptions['compact']
  87. def isGuiShown(self):
  88. if self.fWidget is None or self.fWidget.b_gui is not None:
  89. return None
  90. return self.fWidget.b_gui.isChecked()
  91. # --------------------------------------------------------------------------------------------------------
  92. def setPluginId(self, pluginId):
  93. self.fPluginId = pluginId
  94. if self.fWidget is not None:
  95. self.fWidget.setPluginId(pluginId)
  96. def setSelected(self, select):
  97. if self.fWidget is not None:
  98. self.fWidget.setSelected(select)
  99. QListWidgetItem.setSelected(self, select)
  100. # --------------------------------------------------------------------------------------------------------
  101. def setCompacted(self, compact):
  102. self.fOptions['compact'] = compact
  103. # --------------------------------------------------------------------------------------------------------
  104. def compact(self):
  105. if self.fOptions['compact']:
  106. return
  107. self.recreateWidget(True)
  108. def expand(self):
  109. if not self.fOptions['compact']:
  110. return
  111. self.recreateWidget(True)
  112. def recreateWidget(self, invertCompactOption = False, firstInit = False, newColor = None, newSkin = None):
  113. if invertCompactOption:
  114. self.fOptions['compact'] = not self.fOptions['compact']
  115. if newColor is not None:
  116. self.fOptions['color'] = newColor
  117. if newSkin is not None:
  118. self.fOptions['skin'] = newSkin
  119. wasGuiShown = None
  120. if self.fWidget is not None and self.fWidget.b_gui is not None:
  121. wasGuiShown = self.fWidget.b_gui.isChecked()
  122. self.close()
  123. self.fWidget = createPluginSlot(self.fParent, self.host, self.fPluginId, self.fOptions)
  124. self.fWidget.setFixedHeight(self.fWidget.getFixedHeight())
  125. if wasGuiShown and self.fWidget.b_gui is not None:
  126. self.fWidget.b_gui.setChecked(True)
  127. self.setSizeHint(QSize(self.kMinimumWidth, self.fWidget.getFixedHeight()))
  128. self.fParent.setItemWidget(self, self.fWidget)
  129. if not firstInit:
  130. self.host.set_custom_data(self.fPluginId, CUSTOM_DATA_TYPE_PROPERTY,
  131. "CarlaSkinIsCompacted", "true" if self.fOptions['compact'] else "false")
  132. def recreateWidget2(self, wasCompacted, wasGuiShown):
  133. self.fOptions['compact'] = wasCompacted
  134. self.close()
  135. self.fWidget = createPluginSlot(self.fParent, self.host, self.fPluginId, self.fOptions)
  136. self.fWidget.setFixedHeight(self.fWidget.getFixedHeight())
  137. if wasGuiShown and self.fWidget.b_gui is not None:
  138. self.fWidget.b_gui.setChecked(True)
  139. self.setSizeHint(QSize(self.kMinimumWidth, self.fWidget.getFixedHeight()))
  140. self.fParent.setItemWidget(self, self.fWidget)
  141. self.host.set_custom_data(self.fPluginId, CUSTOM_DATA_TYPE_PROPERTY,
  142. "CarlaSkinIsCompacted", "true" if wasCompacted else "false")
  143. # ------------------------------------------------------------------------------------------------------------
  144. # Rack Widget
  145. class RackListWidget(QListWidget):
  146. def __init__(self, parent):
  147. QListWidget.__init__(self, parent)
  148. self.host = None
  149. self.fParent = None
  150. exts = gCarla.utils.get_supported_file_extensions()
  151. self.fSupportedExtensions = tuple(("." + i) for i in exts)
  152. self.fLastSelectedItem = None
  153. self.fWasLastDragValid = False
  154. self.fPixmapL = QPixmap(":/bitmaps/rack_interior_left.png")
  155. self.fPixmapR = QPixmap(":/bitmaps/rack_interior_right.png")
  156. self.fPixmapWidth = self.fPixmapL.width()
  157. self.setMinimumWidth(RackListItem.kMinimumWidth)
  158. self.setSelectionMode(QAbstractItemView.SingleSelection)
  159. self.setSortingEnabled(False)
  160. self.setDragEnabled(True)
  161. self.setDragDropMode(QAbstractItemView.DropOnly)
  162. self.setDropIndicatorShown(True)
  163. self.viewport().setAcceptDrops(True)
  164. self._updateStyle()
  165. # --------------------------------------------------------------------------------------------------------
  166. def createItem(self, pluginId, useClassicSkin):
  167. return RackListItem(self, pluginId, useClassicSkin)
  168. def getPluginCount(self):
  169. return self.fParent.getPluginCount()
  170. def setHostAndParent(self, host, parent):
  171. self.host = host
  172. self.fParent = parent
  173. # --------------------------------------------------------------------------------------------------------
  174. def customClearSelection(self):
  175. self.setCurrentRow(-1)
  176. self.clearSelection()
  177. self.clearFocus()
  178. def isDragUrlValid(self, filename):
  179. if not filename:
  180. return False
  181. if filename[-1] == '/':
  182. filename = filename[:-1]
  183. lfilename = filename.lower()
  184. if os.path.isdir(filename):
  185. #if os.path.exists(os.path.join(filename, "manifest.ttl")):
  186. #return True
  187. if MACOS and lfilename.endswith(".vst"):
  188. return True
  189. if lfilename.endswith(".vst3") and ".vst3" in self.fSupportedExtensions:
  190. return True
  191. elif os.path.isfile(filename):
  192. if lfilename.endswith(self.fSupportedExtensions):
  193. return True
  194. return False
  195. # --------------------------------------------------------------------------------------------------------
  196. def dragEnterEvent(self, event):
  197. urls = event.mimeData().urls()
  198. for url in urls:
  199. if self.isDragUrlValid(url.toLocalFile()):
  200. self.fWasLastDragValid = True
  201. event.acceptProposedAction()
  202. return
  203. self.fWasLastDragValid = False
  204. QListWidget.dragEnterEvent(self, event)
  205. def dragMoveEvent(self, event):
  206. if not self.fWasLastDragValid:
  207. QListWidget.dragMoveEvent(self, event)
  208. return
  209. event.acceptProposedAction()
  210. tryItem = self.itemAt(event.pos())
  211. if tryItem is not None:
  212. self.setCurrentRow(tryItem.getPluginId())
  213. else:
  214. self.setCurrentRow(-1)
  215. def dragLeaveEvent(self, event):
  216. if self.fWasLastDragValid:
  217. self.fWasLastDragValid = False
  218. QListWidget.dragLeaveEvent(self, event)
  219. # --------------------------------------------------------------------------------------------------------
  220. # FIXME: this needs some attention
  221. # if dropping project file over 1 plugin, load it in rack or patchbay
  222. # if dropping regular files over 1 plugin, keep replacing plugins
  223. def dropEvent(self, event):
  224. event.acceptProposedAction()
  225. urls = event.mimeData().urls()
  226. if not urls:
  227. return
  228. tryItem = self.itemAt(event.pos())
  229. if tryItem is not None:
  230. pluginId = tryItem.getPluginId()
  231. else:
  232. pluginId = -1
  233. for url in urls:
  234. if pluginId >= 0:
  235. self.host.replace_plugin(pluginId)
  236. pluginId += 1
  237. if pluginId > self.host.get_current_plugin_count():
  238. pluginId = -1
  239. filename = url.toLocalFile()
  240. if not filename:
  241. continue
  242. if filename[-1] == '/':
  243. filename = filename[:-1]
  244. if not self.host.load_file(filename):
  245. CustomMessageBox(self, QMessageBox.Critical, self.tr("Error"),
  246. self.tr("Failed to load file"),
  247. self.host.get_last_error(), QMessageBox.Ok, QMessageBox.Ok)
  248. continue
  249. if filename.endswith(".carxp"):
  250. gCarla.gui.loadExternalCanvasGroupPositionsIfNeeded(filename)
  251. if tryItem is not None:
  252. self.host.replace_plugin(self.host.get_max_plugin_number())
  253. #tryItem.widget.setActive(True, True, True)
  254. # --------------------------------------------------------------------------------------------------------
  255. def mousePressEvent(self, event):
  256. if self.itemAt(event.pos()) is None and self.currentRow() != -1:
  257. event.accept()
  258. self.customClearSelection()
  259. return
  260. QListWidget.mousePressEvent(self, event)
  261. def changeEvent(self, event):
  262. if event.type() in (QEvent.StyleChange, QEvent.PaletteChange):
  263. self._updateStyle()
  264. QListWidget.changeEvent(self, event)
  265. def paintEvent(self, event):
  266. painter = QPainter(self.viewport())
  267. width = self.width()
  268. height = self.height()
  269. imgL_rect = QRect(0, 0, self.fPixmapWidth, height)
  270. imgR_rect = QRect(width-self.fPixmapWidth, 0, self.fPixmapWidth, height)
  271. painter.setBrush(self.rail_col)
  272. painter.setPen(Qt.NoPen)
  273. painter.drawRects(imgL_rect, imgR_rect)
  274. painter.setCompositionMode(QPainter.CompositionMode_Multiply)
  275. painter.drawTiledPixmap(imgL_rect, self.fPixmapL)
  276. painter.drawTiledPixmap(imgR_rect, self.fPixmapR)
  277. painter.setCompositionMode(QPainter.CompositionMode_Plus)
  278. painter.drawTiledPixmap(imgL_rect, self.fPixmapL)
  279. painter.drawTiledPixmap(imgR_rect, self.fPixmapR)
  280. painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
  281. painter.setPen(self.edge_col)
  282. painter.setBrush(Qt.NoBrush)
  283. painter.drawRect(self.fPixmapWidth, 0, width-self.fPixmapWidth*2, height)
  284. QListWidget.paintEvent(self, event)
  285. def selectionChanged(self, selected, deselected):
  286. for index in deselected.indexes():
  287. item = self.itemFromIndex(index)
  288. if item is not None:
  289. item.setSelected(False)
  290. for index in selected.indexes():
  291. item = self.itemFromIndex(index)
  292. if item is not None:
  293. item.setSelected(True)
  294. QListWidget.selectionChanged(self, selected, deselected)
  295. # --------------------------------------------------------------------------------------------------------
  296. def _updateStyle(self):
  297. palette = self.palette()
  298. bg_color = palette.window().color()
  299. base_color = palette.base().color()
  300. text_color = palette.text().color()
  301. r0,g0,b0,_ = bg_color.getRgb()
  302. r1,g1,b1,_ = text_color.getRgb()
  303. self.rail_col = QColor(int((r0*3+r1)/4), int((g0*3+g1)/4), int((b0*3+b1)/4))
  304. self.edge_col = (self.rail_col if self.rail_col.blackF() > base_color.blackF() else base_color).darker(115)
  305. # ------------------------------------------------------------------------------------------------------------