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.

415 lines
14KB

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