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.

432 lines
15KB

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