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.

586 lines
18KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Carla rack widget code
  4. # Copyright (C) 2011-2014 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 (Config)
  19. from carla_config import *
  20. # ------------------------------------------------------------------------------------------------------------
  21. # Imports (Global)
  22. if config_UseQt5:
  23. from PyQt5.QtCore import Qt, QSize, QTimer
  24. from PyQt5.QtGui import QPixmap
  25. from PyQt5.QtWidgets import QAbstractItemView, QApplication, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QScrollBar
  26. else:
  27. from PyQt4.QtCore import Qt, QSize, QTimer
  28. from PyQt4.QtGui import QAbstractItemView, QApplication, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPixmap, QScrollBar
  29. # ------------------------------------------------------------------------------------------------------------
  30. # Imports (Custom Stuff)
  31. from carla_host import *
  32. from carla_skin import *
  33. # ------------------------------------------------------------------------------------------------------------
  34. # Rack widget item
  35. class CarlaRackItem(QListWidgetItem):
  36. kRackItemType = QListWidgetItem.UserType + 1
  37. def __init__(self, parent, pluginId, useSkins):
  38. QListWidgetItem.__init__(self, parent, self.kRackItemType)
  39. # ----------------------------------------------------------------------------------------------------
  40. # Internal stuff
  41. self.fParent = parent
  42. self.fPluginId = pluginId
  43. self.fUseSkins = useSkins
  44. self.fWidget = None
  45. self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
  46. #self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled|Qt.ItemIsDragEnabled|Qt.ItemIsDropEnabled)
  47. # ----------------------------------------------------------------------------------------------------
  48. # Set-up GUI
  49. self.recreateWidget()
  50. if False:
  51. self.fWidget = AbstractPluginSlot(parent, parent.host, pluginId)
  52. # --------------------------------------------------------------------------------------------------------
  53. def setPluginId(self, pluginId):
  54. self.fPluginId = pluginId
  55. self.fWidget.setPluginId(pluginId)
  56. # --------------------------------------------------------------------------------------------------------
  57. def getEditDialog(self):
  58. return self.fWidget.fEditDialog
  59. def closeEditDialog(self):
  60. self.fWidget.fEditDialog.close()
  61. # --------------------------------------------------------------------------------------------------------
  62. def getWidget(self):
  63. return self.fWidget
  64. def recreateWidget(self):
  65. if self.fWidget is not None:
  66. #self.fWidget.fEditDialog.close()
  67. del self.fWidget
  68. self.fWidget = createPluginSlot(self.fParent, self.fParent.host, self.fPluginId, self.fUseSkins)
  69. self.fWidget.setFixedHeight(self.fWidget.getFixedHeight())
  70. self.setSizeHint(QSize(640, self.fWidget.getFixedHeight()))
  71. self.fParent.setItemWidget(self, self.fWidget)
  72. # ------------------------------------------------------------------------------------------------------------
  73. # Rack widget list
  74. class CarlaRackList(QListWidget):
  75. def __init__(self, parent, host):
  76. QListWidget.__init__(self, parent)
  77. self.host = host
  78. if False:
  79. # kdevelop likes this :)
  80. host = CarlaHostMeta()
  81. self.host = host
  82. # -------------------------------------------------------------
  83. exts = host.get_supported_file_extensions().split(";")
  84. # plugin files
  85. exts.append("dll")
  86. if MACOS:
  87. exts.append("dylib")
  88. if not WINDOWS:
  89. exts.append("so")
  90. self.fSupportedExtensions = tuple(i.replace("*.","") for i in exts)
  91. self.fWasLastDragValid = False
  92. self.setMinimumWidth(640+20) # required by zita, 591 was old value
  93. self.setSelectionMode(QAbstractItemView.SingleSelection)
  94. self.setSortingEnabled(False)
  95. #self.setSortingEnabled(True)
  96. self.setDragEnabled(True)
  97. self.setDragDropMode(QAbstractItemView.DropOnly)
  98. self.setDropIndicatorShown(True)
  99. self.viewport().setAcceptDrops(True)
  100. self.setFrameShape(QFrame.NoFrame)
  101. self.setFrameShadow(QFrame.Plain)
  102. self.fPixmapL = QPixmap(":/bitmaps/rack_interior_left.png")
  103. self.fPixmapR = QPixmap(":/bitmaps/rack_interior_right.png")
  104. self.fPixmapWidth = self.fPixmapL.width()
  105. def isDragEventValid(self, urls):
  106. for url in urls:
  107. filename = url.toLocalFile()
  108. if os.path.isdir(filename):
  109. if os.path.exists(os.path.join(filename, "manifest.ttl")):
  110. return True
  111. elif os.path.isfile(filename):
  112. if filename.lower().endswith(self.fSupportedExtensions):
  113. return True
  114. return False
  115. def dragEnterEvent(self, event):
  116. if self.isDragEventValid(event.mimeData().urls()):
  117. self.fWasLastDragValid = True
  118. event.acceptProposedAction()
  119. return
  120. self.fWasLastDragValid = False
  121. QListWidget.dragEnterEvent(self, event)
  122. def dragMoveEvent(self, event):
  123. if self.fWasLastDragValid:
  124. event.acceptProposedAction()
  125. tryItem = self.itemAt(event.pos())
  126. if tryItem is not None:
  127. self.setCurrentRow(tryItem.widget.getPluginId())
  128. else:
  129. self.setCurrentRow(-1)
  130. return
  131. QListWidget.dragMoveEvent(self, event)
  132. #def dragLeaveEvent(self, event):
  133. #self.fWasLastDragValid = False
  134. #QListWidget.dragLeaveEvent(self, event)
  135. def dropEvent(self, event):
  136. event.acceptProposedAction()
  137. urls = event.mimeData().urls()
  138. if len(urls) == 0:
  139. return
  140. tryItem = self.itemAt(event.pos())
  141. if tryItem is not None:
  142. pluginId = tryItem.widget.getPluginId()
  143. self.host.replace_plugin(pluginId)
  144. for url in urls:
  145. filename = url.toLocalFile()
  146. if not self.host.load_file(filename):
  147. CustomMessageBox(self, QMessageBox.Critical, self.tr("Error"),
  148. self.tr("Failed to load file"),
  149. self.host.get_last_error(), QMessageBox.Ok, QMessageBox.Ok)
  150. if tryItem is not None:
  151. self.host.replace_plugin(self.parent().fPluginCount)
  152. #tryItem.widget.setActive(True, True, True)
  153. def mousePressEvent(self, event):
  154. if self.itemAt(event.pos()) is None:
  155. event.accept()
  156. self.setCurrentRow(-1)
  157. return
  158. QListWidget.mousePressEvent(self, event)
  159. def paintEvent(self, event):
  160. painter = QPainter(self.viewport())
  161. painter.drawTiledPixmap(0, 0, self.fPixmapWidth, self.height(), self.fPixmapL)
  162. painter.drawTiledPixmap(self.width()-self.fPixmapWidth-2, 0, self.fPixmapWidth, self.height(), self.fPixmapR)
  163. QListWidget.paintEvent(self, event)
  164. # ------------------------------------------------------------------------------------------------------------
  165. # Rack widget
  166. class CarlaRackW(QFrame, HostWidgetMeta):
  167. #class CarlaRackW(QFrame, HostWidgetMeta, metaclass=PyQtMetaClass):
  168. def __init__(self, parent, host, doSetup = True):
  169. QFrame.__init__(self, parent)
  170. self.host = host
  171. if False:
  172. # kdevelop likes this :)
  173. host = CarlaHostMeta()
  174. self.host = host
  175. # -------------------------------------------------------------
  176. self.fLayout = QHBoxLayout(self)
  177. self.fLayout.setContentsMargins(0, 0, 0, 0)
  178. self.fLayout.setSpacing(0)
  179. self.setLayout(self.fLayout)
  180. self.fPadLeft = QLabel(self)
  181. self.fPadLeft.setFixedWidth(25)
  182. self.fPadLeft.setObjectName("PadLeft")
  183. self.fPadLeft.setText("")
  184. self.fPadRight = QLabel(self)
  185. self.fPadRight.setFixedWidth(25)
  186. self.fPadRight.setObjectName("PadRight")
  187. self.fPadRight.setText("")
  188. self.fRack = CarlaRackList(self, host)
  189. self.fRack.setObjectName("CarlaRackList")
  190. self.fRack.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  191. self.fRack.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  192. self.fRack.currentRowChanged.connect(self.slot_currentRowChanged)
  193. sb = self.fRack.verticalScrollBar()
  194. self.fScrollBar = QScrollBar(Qt.Vertical, self)
  195. self.fScrollBar.setMinimum(sb.minimum())
  196. self.fScrollBar.setMaximum(sb.maximum())
  197. self.fScrollBar.setValue(sb.value())
  198. #sb.actionTriggered.connect(self.fScrollBar.triggerAction)
  199. #sb.sliderMoved.connect(self.fScrollBar.)
  200. #sb.sliderPressed.connect(self.fScrollBar.)
  201. #sb.sliderReleased.connect(self.fScrollBar.)
  202. sb.rangeChanged.connect(self.fScrollBar.setRange)
  203. sb.valueChanged.connect(self.fScrollBar.setValue)
  204. self.fScrollBar.rangeChanged.connect(sb.setRange)
  205. self.fScrollBar.valueChanged.connect(sb.setValue)
  206. self.fLayout.addWidget(self.fPadLeft)
  207. self.fLayout.addWidget(self.fRack)
  208. self.fLayout.addWidget(self.fPadRight)
  209. self.fLayout.addWidget(self.fScrollBar)
  210. # -------------------------------------------------------------
  211. # Internal stuff
  212. self.fParent = parent
  213. self.fPluginCount = 0
  214. self.fPluginList = []
  215. self.fCurrentRow = -1
  216. self.fLastSelectedItem = None
  217. # -------------------------------------------------------------
  218. # Set-up GUI stuff
  219. #app = QApplication.instance()
  220. #pal1 = app.palette().base().color()
  221. #pal2 = app.palette().button().color()
  222. #col1 = "stop:0 rgb(%i, %i, %i)" % (pal1.red(), pal1.green(), pal1.blue())
  223. #col2 = "stop:1 rgb(%i, %i, %i)" % (pal2.red(), pal2.green(), pal2.blue())
  224. self.setStyleSheet("""
  225. QLabel#PadLeft {
  226. background-image: url(:/bitmaps/rack_padding_left.png);
  227. background-repeat: repeat-y;
  228. }
  229. QLabel#PadRight {
  230. background-image: url(:/bitmaps/rack_padding_right.png);
  231. background-repeat: repeat-y;
  232. }
  233. CarlaRackList#CarlaRackList {
  234. background-color: black;
  235. }
  236. """)
  237. # -------------------------------------------------------------
  238. # Connect actions to functions
  239. host.PluginAddedCallback.connect(self.slot_handlePluginAddedCallback)
  240. host.PluginRemovedCallback.connect(self.slot_handlePluginRemovedCallback)
  241. host.ReloadAllCallback.connect(self.slot_handleReloadAllCallback)
  242. if not doSetup: return
  243. parent.ui.menu_Canvas.hide()
  244. parent.ui.act_plugins_enable.triggered.connect(self.slot_pluginsEnable)
  245. parent.ui.act_plugins_disable.triggered.connect(self.slot_pluginsDisable)
  246. parent.ui.act_plugins_volume100.triggered.connect(self.slot_pluginsVolume100)
  247. parent.ui.act_plugins_mute.triggered.connect(self.slot_pluginsMute)
  248. parent.ui.act_plugins_wet100.triggered.connect(self.slot_pluginsWet100)
  249. parent.ui.act_plugins_bypass.triggered.connect(self.slot_pluginsBypass)
  250. parent.ui.act_plugins_center.triggered.connect(self.slot_pluginsCenter)
  251. parent.ui.act_plugins_panic.triggered.connect(self.slot_pluginsDisable)
  252. parent.ui.act_settings_configure.triggered.connect(self.slot_configureCarla)
  253. # -----------------------------------------------------------------
  254. @pyqtSlot(int, str)
  255. def slot_handlePluginAddedCallback(self, pluginId, pluginName):
  256. pitem = CarlaRackItem(self.fRack, pluginId, self.fParent.getSavedSettings()[CARLA_KEY_MAIN_USE_CUSTOM_SKINS])
  257. self.fPluginList.append(pitem)
  258. self.fPluginCount += 1
  259. if not self.fParent.isProjectLoading():
  260. pitem.getWidget().setActive(True, True, True)
  261. @pyqtSlot(int)
  262. def slot_handlePluginRemovedCallback(self, pluginId):
  263. pitem = self.getPluginItem(pluginId)
  264. self.fPluginCount -= 1
  265. self.fPluginList.pop(pluginId)
  266. self.fRack.takeItem(pluginId)
  267. if pitem is not None:
  268. pitem.closeEditDialog()
  269. del pitem
  270. # push all plugins 1 slot back
  271. for i in range(pluginId, self.fPluginCount):
  272. pitem = self.fPluginList[i]
  273. pitem.setPluginId(i)
  274. # -----------------------------------------------------------------
  275. # HostWidgetMeta methods
  276. def removeAllPlugins(self):
  277. while self.fRack.takeItem(0):
  278. pass
  279. for pitem in self.fPluginList:
  280. if pitem is None:
  281. break
  282. pitem.closeEditDialog()
  283. del pitem
  284. self.fPluginCount = 0
  285. self.fPluginList = []
  286. def engineStarted(self):
  287. pass
  288. def engineStopped(self):
  289. pass
  290. def idleFast(self):
  291. for pitem in self.fPluginList:
  292. if pitem is None:
  293. break
  294. pitem.getWidget().idleFast()
  295. def idleSlow(self):
  296. for pitem in self.fPluginList:
  297. if pitem is None:
  298. break
  299. pitem.getWidget().idleSlow()
  300. def projectLoadingStarted(self):
  301. self.fRack.setEnabled(False)
  302. def projectLoadingFinished(self):
  303. self.fRack.setEnabled(True)
  304. def saveSettings(self, settings):
  305. pass
  306. def showEditDialog(self, pluginId):
  307. dialog = self.getPluginEditDialog(pluginId)
  308. if dialog is None:
  309. return
  310. dialog.show()
  311. # -----------------------------------------------------------------
  312. @pyqtSlot()
  313. def slot_pluginsEnable(self):
  314. if not self.host.is_engine_running():
  315. return
  316. for pitem in self.fPluginList:
  317. if pitem is None:
  318. break
  319. pitem.getWidget().setActive(True, True, True)
  320. @pyqtSlot()
  321. def slot_pluginsDisable(self):
  322. if not self.host.is_engine_running():
  323. return
  324. for pitem in self.fPluginList:
  325. if pitem is None:
  326. break
  327. pitem.getWidget().setActive(False, True, True)
  328. @pyqtSlot()
  329. def slot_pluginsVolume100(self):
  330. if not self.host.is_engine_running():
  331. return
  332. for pitem in self.fPluginList:
  333. if pitem is None:
  334. break
  335. pitem.getWidget().setInternalParameter(PLUGIN_CAN_VOLUME, 1.0)
  336. @pyqtSlot()
  337. def slot_pluginsMute(self):
  338. if not self.host.is_engine_running():
  339. return
  340. for pitem in self.fPluginList:
  341. if pitem is None:
  342. break
  343. pitem.getWidget().setInternalParameter(PLUGIN_CAN_VOLUME, 0.0)
  344. @pyqtSlot()
  345. def slot_pluginsWet100(self):
  346. if not self.host.is_engine_running():
  347. return
  348. for pitem in self.fPluginList:
  349. if pitem is None:
  350. break
  351. pitem.getWidget().setInternalParameter(PLUGIN_CAN_DRYWET, 1.0)
  352. @pyqtSlot()
  353. def slot_pluginsBypass(self):
  354. if not self.host.is_engine_running():
  355. return
  356. for pitem in self.fPluginList:
  357. if pitem is None:
  358. break
  359. pitem.getWidget().setInternalParameter(PLUGIN_CAN_DRYWET, 0.0)
  360. @pyqtSlot()
  361. def slot_pluginsCenter(self):
  362. if not self.host.is_engine_running():
  363. return
  364. for pitem in self.fPluginList:
  365. if pitem is None:
  366. break
  367. pitem.getWidget().setInternalParameter(PARAMETER_BALANCE_LEFT, -1.0)
  368. pitem.getWidget().setInternalParameter(PARAMETER_BALANCE_RIGHT, 1.0)
  369. pitem.getWidget().setInternalParameter(PARAMETER_PANNING, 0.0)
  370. # -----------------------------------------------------------------
  371. @pyqtSlot()
  372. def slot_configureCarla(self):
  373. dialog = CarlaSettingsW(self, self.host, False, False)
  374. if not dialog.exec_():
  375. return
  376. self.fParent.loadSettings(False)
  377. # -----------------------------------------------------------------
  378. @pyqtSlot(int)
  379. def slot_handleReloadAllCallback(self, pluginId):
  380. if pluginId >= self.fPluginCount:
  381. return
  382. pitem = self.fPluginList[pluginId]
  383. if pitem is None:
  384. return
  385. self.fRack.setCurrentRow(-1)
  386. self.fCurrentRow = -1
  387. self.fLastSelectedItem = None
  388. pitem.recreateWidget()
  389. # -----------------------------------------------------------------
  390. @pyqtSlot(int)
  391. def slot_currentRowChanged(self, row):
  392. self.fCurrentRow = row
  393. if self.fLastSelectedItem is not None:
  394. self.fLastSelectedItem.setSelected(False)
  395. if row < 0 or row >= self.fPluginCount or self.fPluginList[row] is None:
  396. self.fLastSelectedItem = None
  397. return
  398. pitem = self.fPluginList[row]
  399. pitem.getWidget().setSelected(True)
  400. self.fLastSelectedItem = pitem.getWidget()
  401. # -----------------------------------------------------------------
  402. def getPluginItem(self, pluginId):
  403. if pluginId >= self.fPluginCount:
  404. return None
  405. pitem = self.fPluginList[pluginId]
  406. if pitem is None:
  407. return None
  408. if False:
  409. pitem = CarlaRackItem(self, 0, False)
  410. return pitem
  411. def getPluginEditDialog(self, pluginId):
  412. if pluginId >= self.fPluginCount:
  413. return None
  414. pitem = self.fPluginList[pluginId]
  415. if pitem is None:
  416. return None
  417. if False:
  418. pitem = CarlaRackItem(self, 0, False)
  419. return pitem.getEditDialog()
  420. def getPluginSlotWidget(self, pluginId):
  421. if pluginId >= self.fPluginCount:
  422. return None
  423. pitem = self.fPluginList[pluginId]
  424. if pitem is None:
  425. return None
  426. if False:
  427. pitem = CarlaRackItem(self, 0, False)
  428. return pitem.getWidget()
  429. # -----------------------------------------------------------------