1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Carla host code
5# Copyright (C) 2011-2021 Filipe Coelho <falktx@falktx.com>
6#
7# This program is free software; you can redistribute it and/or
8# modify it under the terms of the GNU General Public License as
9# published by the Free Software Foundation; either version 2 of
10# the License, or any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# For a full copy of the GNU General Public License see the doc/GPL.txt file.
18
19# ------------------------------------------------------------------------------------------------------------
20# Imports (Global)
21
22import json
23
24# ------------------------------------------------------------------------------------------------------------
25# Imports (ctypes)
26
27from ctypes import (
28    byref, pointer
29)
30
31# ------------------------------------------------------------------------------------------------------------
32# Imports (PyQt5)
33
34# This fails in some configurations, assume >= 5.6.0 in that case
35try:
36    from PyQt5.Qt import PYQT_VERSION
37except ImportError:
38    PYQT_VERSION = 0x50600
39
40from PyQt5.QtCore import (
41    QT_VERSION, qCritical, QBuffer, QEventLoop, QFileInfo, QIODevice, QMimeData, QModelIndex, QPointF, QTimer, QEvent
42)
43from PyQt5.QtGui import (
44    QImage, QImageWriter, QPainter, QPalette, QBrush
45)
46from PyQt5.QtWidgets import (
47    QAction, QApplication, QInputDialog, QFileSystemModel, QListWidgetItem, QGraphicsView, QMainWindow
48)
49
50# ------------------------------------------------------------------------------------------------------------
51# Imports (Custom)
52
53import ui_carla_host
54
55from carla_app import *
56from carla_backend_qt import CarlaHostQtDLL, CarlaHostQtNull
57from carla_database import *
58from carla_settings import *
59from carla_utils import *
60from carla_widgets import *
61
62from patchcanvas import patchcanvas
63from widgets.digitalpeakmeter import DigitalPeakMeter
64from widgets.pixmapkeyboard import PixmapKeyboardHArea
65
66# ------------------------------------------------------------------------------------------------------------
67# Try Import OpenGL
68
69try:
70    from PyQt5.QtOpenGL import QGLWidget
71    hasGL = True
72except:
73    hasGL = False
74
75# ------------------------------------------------------------------------------------------------------------
76# Safe exception hook, needed for PyQt5
77
78def sys_excepthook(typ, value, tback):
79    return sys.__excepthook__(typ, value, tback)
80
81# ------------------------------------------------------------------------------------------------------------
82# Session Management support
83
84CARLA_CLIENT_NAME = os.getenv("CARLA_CLIENT_NAME")
85LADISH_APP_NAME   = os.getenv("LADISH_APP_NAME")
86NSM_URL           = os.getenv("NSM_URL")
87
88# ------------------------------------------------------------------------------------------------------------
89# Small print helper
90
91def processMode2Str(processMode):
92    if processMode == ENGINE_PROCESS_MODE_SINGLE_CLIENT:
93        return "Single client"
94    if processMode == ENGINE_PROCESS_MODE_MULTIPLE_CLIENTS:
95        return "Multi client"
96    if processMode == ENGINE_PROCESS_MODE_CONTINUOUS_RACK:
97        return "Continuous Rack"
98    if processMode == ENGINE_PROCESS_MODE_PATCHBAY:
99        return "Patchbay"
100    if processMode == ENGINE_PROCESS_MODE_BRIDGE:
101        return "Bridge"
102    return "Unknown"
103
104# ------------------------------------------------------------------------------------------------------------
105# Carla Print class
106
107class CarlaPrint:
108    def __init__(self, err):
109        self.err = err
110
111    def flush(self):
112        gCarla.utils.fflush(self.err)
113
114    def write(self, string):
115        gCarla.utils.fputs(self.err, string)
116
117# ------------------------------------------------------------------------------------------------------------
118# Host Window
119
120class HostWindow(QMainWindow):
121#class HostWindow(QMainWindow, PluginEditParentMeta, metaclass=PyQtMetaClass):
122    # signals
123    SIGTERM = pyqtSignal()
124    SIGUSR1 = pyqtSignal()
125
126    # CustomActions
127    CUSTOM_ACTION_NONE         = 0
128    CUSTOM_ACTION_APP_CLOSE    = 1
129    CUSTOM_ACTION_PROJECT_LOAD = 2
130
131    # --------------------------------------------------------------------------------------------------------
132
133    def __init__(self, host, withCanvas, parent=None):
134        QMainWindow.__init__(self, parent)
135        self.host = host
136        self.ui = ui_carla_host.Ui_CarlaHostW()
137        self.ui.setupUi(self)
138        gCarla.gui = self
139
140        if False:
141            # kdevelop likes this :)
142            host = CarlaHostNull()
143            self.host = host
144
145        self._true = c_char_p("true".encode("utf-8"))
146
147        self.fParentOrSelf = parent or self
148
149        # ----------------------------------------------------------------------------------------------------
150        # Internal stuff
151
152        self.fIdleTimerNull = self.startTimer(1000) # keep application signals alive
153        self.fIdleTimerFast = 0
154        self.fIdleTimerSlow = 0
155
156        self.fLadspaRdfNeedsUpdate = True
157        self.fLadspaRdfList = []
158
159        self.fPluginCount = 0
160        self.fPluginList  = []
161
162        self.fPluginDatabaseDialog = None
163        self.fFavoritePlugins = []
164
165        self.fProjectFilename  = ""
166        self.fIsProjectLoading = False
167        self.fCurrentlyRemovingAllPlugins = False
168
169        self.fLastTransportBPM   = 0.0
170        self.fLastTransportFrame = 0
171        self.fLastTransportState = False
172        self.fBufferSize         = 0
173        self.fSampleRate         = 0.0
174        self.fOscAddressTCP      = ""
175        self.fOscAddressUDP      = ""
176
177        if MACOS:
178            self.fMacClosingHelper = True
179
180        # CancelableActionCallback Box
181        self.fCancelableActionBox = None
182
183        # run a custom action after engine is properly closed
184        self.fCustomStopAction = self.CUSTOM_ACTION_NONE
185
186        # first attempt of auto-start engine doesn't show an error
187        self.fFirstEngineInit = True
188
189        # to be filled with key-value pairs of current settings
190        self.fSavedSettings = {}
191
192        # true if NSM server handles our window management
193        self.fWindowCloseHideGui = False
194
195        if host.isControl:
196            self.fClientName         = "Carla-Control"
197            self.fSessionManagerName = "Control"
198        elif host.isPlugin:
199            self.fClientName         = "Carla-Plugin"
200            self.fSessionManagerName = "Plugin"
201        elif LADISH_APP_NAME:
202            self.fClientName         = LADISH_APP_NAME
203            self.fSessionManagerName = "LADISH"
204        elif NSM_URL and host.nsmOK:
205            self.fClientName         = "Carla.tmp"
206            self.fSessionManagerName = "Non Session Manager TMP"
207            self.fWindowCloseHideGui = True
208        else:
209            self.fClientName         = CARLA_CLIENT_NAME or "Carla"
210            self.fSessionManagerName = ""
211
212        # ----------------------------------------------------------------------------------------------------
213        # Internal stuff (patchbay)
214
215        self.fPeaksCleared = True
216
217        self.fExternalPatchbay = False
218        self.fSelectedPlugins  = []
219
220        self.fCanvasWidth  = 0
221        self.fCanvasHeight = 0
222        self.fMiniCanvasUpdateTimeout = 0
223
224        self.fWithCanvas = withCanvas
225
226        # ----------------------------------------------------------------------------------------------------
227        # Internal stuff (logs)
228
229        self.autoscrollOnNewLog = True
230        self.lastLogSliderPos = 0
231
232        # ----------------------------------------------------------------------------------------------------
233        # Set up GUI (engine stopped)
234
235        if self.host.isPlugin or self.host.isControl:
236            self.ui.act_file_save.setVisible(False)
237            self.ui.act_engine_start.setEnabled(False)
238            self.ui.act_engine_start.setVisible(False)
239            self.ui.act_engine_stop.setEnabled(False)
240            self.ui.act_engine_stop.setVisible(False)
241            self.ui.menu_Engine.setEnabled(False)
242            self.ui.menu_Engine.setVisible(False)
243            self.ui.menu_Engine.menuAction().setVisible(False)
244            self.ui.tabWidget.removeTab(2)
245
246            if self.host.isControl:
247                self.ui.act_file_new.setVisible(False)
248                self.ui.act_file_open.setVisible(False)
249                self.ui.act_file_save_as.setVisible(False)
250                self.ui.tabUtils.removeTab(0)
251            else:
252                self.ui.act_file_save_as.setText(self.tr("Export as..."))
253
254                if not withCanvas:
255                    self.ui.tabWidget.tabBar().hide()
256
257        else:
258            self.ui.act_engine_start.setEnabled(True)
259
260            if WINDOWS:
261                self.ui.tabWidget.removeTab(2)
262
263        if self.host.isControl:
264            self.ui.act_file_refresh.setEnabled(False)
265        else:
266            self.ui.act_file_connect.setEnabled(False)
267            self.ui.act_file_connect.setVisible(False)
268            self.ui.act_file_refresh.setEnabled(False)
269            self.ui.act_file_refresh.setVisible(False)
270
271        if self.fSessionManagerName and not self.host.isPlugin:
272            self.ui.act_file_new.setEnabled(False)
273
274        self.ui.act_file_open.setEnabled(False)
275        self.ui.act_file_save.setEnabled(False)
276        self.ui.act_file_save_as.setEnabled(False)
277        self.ui.act_engine_stop.setEnabled(False)
278        self.ui.act_plugin_remove_all.setEnabled(False)
279
280        self.ui.act_canvas_show_internal.setChecked(False)
281        self.ui.act_canvas_show_internal.setVisible(False)
282        self.ui.act_canvas_show_external.setChecked(False)
283        self.ui.act_canvas_show_external.setVisible(False)
284
285        self.ui.menu_PluginMacros.setEnabled(False)
286        self.ui.menu_Canvas.setEnabled(False)
287
288        self.ui.dockWidgetTitleBar = QWidget(self)
289        self.ui.dockWidget.setTitleBarWidget(self.ui.dockWidgetTitleBar)
290
291        if not withCanvas:
292            self.ui.act_canvas_show_internal.setVisible(False)
293            self.ui.act_canvas_show_external.setVisible(False)
294            self.ui.act_canvas_arrange.setVisible(False)
295            self.ui.act_canvas_refresh.setVisible(False)
296            self.ui.act_canvas_save_image.setVisible(False)
297            self.ui.act_canvas_zoom_100.setVisible(False)
298            self.ui.act_canvas_zoom_fit.setVisible(False)
299            self.ui.act_canvas_zoom_in.setVisible(False)
300            self.ui.act_canvas_zoom_out.setVisible(False)
301            self.ui.act_settings_show_meters.setVisible(False)
302            self.ui.act_settings_show_keyboard.setVisible(False)
303            self.ui.menu_Canvas_Zoom.setEnabled(False)
304            self.ui.menu_Canvas_Zoom.setVisible(False)
305            self.ui.menu_Canvas_Zoom.menuAction().setVisible(False)
306            self.ui.menu_Canvas.setEnabled(False)
307            self.ui.menu_Canvas.setVisible(False)
308            self.ui.menu_Canvas.menuAction().setVisible(False)
309            self.ui.tw_miniCanvas.hide()
310            self.ui.tabWidget.removeTab(1)
311            if WINDOWS:
312                self.ui.tabWidget.tabBar().hide()
313
314        # ----------------------------------------------------------------------------------------------------
315        # Set up GUI (disk)
316
317        exts = gCarla.utils.get_supported_file_extensions()
318
319        self.fDirModel = QFileSystemModel(self)
320        self.fDirModel.setRootPath(HOME)
321        self.fDirModel.setNameFilters(tuple(("*." + i) for i in exts))
322
323        self.ui.fileTreeView.setModel(self.fDirModel)
324        self.ui.fileTreeView.setRootIndex(self.fDirModel.index(HOME))
325        self.ui.fileTreeView.setColumnHidden(1, True)
326        self.ui.fileTreeView.setColumnHidden(2, True)
327        self.ui.fileTreeView.setColumnHidden(3, True)
328        self.ui.fileTreeView.setHeaderHidden(True)
329
330        # ----------------------------------------------------------------------------------------------------
331        # Set up GUI (transport)
332
333        fontMetrics   = self.ui.l_transport_bbt.fontMetrics()
334        minValueWidth = fontMetricsHorizontalAdvance(fontMetrics, "000|00|0000")
335        minLabelWidth = fontMetricsHorizontalAdvance(fontMetrics, self.ui.label_transport_frame.text())
336
337        labelTimeWidth = fontMetricsHorizontalAdvance(fontMetrics, self.ui.label_transport_time.text())
338        labelBBTWidth  = fontMetricsHorizontalAdvance(fontMetrics, self.ui.label_transport_bbt.text())
339
340        if minLabelWidth < labelTimeWidth:
341            minLabelWidth = labelTimeWidth
342        if minLabelWidth < labelBBTWidth:
343            minLabelWidth = labelBBTWidth
344
345        self.ui.label_transport_frame.setMinimumWidth(minLabelWidth + 3)
346        self.ui.label_transport_time.setMinimumWidth(minLabelWidth + 3)
347        self.ui.label_transport_bbt.setMinimumWidth(minLabelWidth + 3)
348
349        self.ui.l_transport_bbt.setMinimumWidth(minValueWidth + 3)
350        self.ui.l_transport_frame.setMinimumWidth(minValueWidth + 3)
351        self.ui.l_transport_time.setMinimumWidth(minValueWidth + 3)
352
353        if host.isPlugin:
354            self.ui.b_transport_play.setEnabled(False)
355            self.ui.b_transport_stop.setEnabled(False)
356            self.ui.b_transport_backwards.setEnabled(False)
357            self.ui.b_transport_forwards.setEnabled(False)
358            self.ui.group_transport_controls.setEnabled(False)
359            self.ui.group_transport_controls.setVisible(False)
360            self.ui.cb_transport_link.setEnabled(False)
361            self.ui.cb_transport_link.setVisible(False)
362            self.ui.cb_transport_jack.setEnabled(False)
363            self.ui.cb_transport_jack.setVisible(False)
364            self.ui.dsb_transport_bpm.setEnabled(False)
365            self.ui.dsb_transport_bpm.setReadOnly(True)
366
367        self.ui.w_transport.setEnabled(False)
368
369        # ----------------------------------------------------------------------------------------------------
370        # Set up GUI (rack)
371
372        self.ui.listWidget.setHostAndParent(self.host, self)
373
374        sb = self.ui.listWidget.verticalScrollBar()
375        self.ui.rackScrollBar.setMinimum(sb.minimum())
376        self.ui.rackScrollBar.setMaximum(sb.maximum())
377        self.ui.rackScrollBar.setValue(sb.value())
378
379        sb.rangeChanged.connect(self.ui.rackScrollBar.setRange)
380        sb.valueChanged.connect(self.ui.rackScrollBar.setValue)
381        self.ui.rackScrollBar.rangeChanged.connect(sb.setRange)
382        self.ui.rackScrollBar.valueChanged.connect(sb.setValue)
383
384        self.updateStyle()
385
386        self.ui.rack.setStyleSheet("""
387          CarlaRackList#CarlaRackList {
388            background-color: black;
389          }
390        """)
391
392        # ----------------------------------------------------------------------------------------------------
393        # Set up GUI (patchbay)
394
395        self.ui.peak_in.setChannelCount(2)
396        self.ui.peak_in.setMeterColor(DigitalPeakMeter.COLOR_BLUE)
397        self.ui.peak_in.setMeterOrientation(DigitalPeakMeter.VERTICAL)
398        self.ui.peak_in.setFixedWidth(25)
399
400        self.ui.peak_out.setChannelCount(2)
401        self.ui.peak_out.setMeterColor(DigitalPeakMeter.COLOR_GREEN)
402        self.ui.peak_out.setMeterOrientation(DigitalPeakMeter.VERTICAL)
403        self.ui.peak_out.setFixedWidth(25)
404
405        self.ui.scrollArea = PixmapKeyboardHArea(self.ui.patchbay)
406        self.ui.keyboard   = self.ui.scrollArea.keyboard
407        self.ui.patchbay.layout().addWidget(self.ui.scrollArea, 1, 0, 1, 0)
408
409        self.ui.scrollArea.setEnabled(False)
410
411        self.ui.miniCanvasPreview.setRealParent(self)
412        self.ui.tw_miniCanvas.tabBar().hide()
413
414        # ----------------------------------------------------------------------------------------------------
415        # Set up GUI (logs)
416
417        self.ui.text_logs.textChanged.connect(self.slot_logButtonsState)
418        self.ui.logs_clear.clicked.connect(self.slot_logClear)
419        self.ui.logs_save.clicked.connect(self.slot_logSave)
420        self.ui.logs_autoscroll.stateChanged.connect(self.slot_toggleLogAutoscroll)
421        self.ui.text_logs.verticalScrollBar().valueChanged.connect(self.slot_logSliderMoved)
422
423        # ----------------------------------------------------------------------------------------------------
424        # Set up GUI (special stuff for Mac OS)
425
426        if MACOS:
427            self.ui.act_file_quit.setMenuRole(QAction.QuitRole)
428            self.ui.act_settings_configure.setMenuRole(QAction.PreferencesRole)
429            self.ui.act_help_about.setMenuRole(QAction.AboutRole)
430            self.ui.act_help_about_juce.setMenuRole(QAction.ApplicationSpecificRole)
431            self.ui.act_help_about_qt.setMenuRole(QAction.AboutQtRole)
432            self.ui.menu_Settings.setTitle("Panels")
433
434        # ----------------------------------------------------------------------------------------------------
435        # Load Settings
436
437        self.loadSettings(True)
438
439        # ----------------------------------------------------------------------------------------------------
440        # Set-up Canvas
441
442        if withCanvas:
443            self.scene = patchcanvas.PatchScene(self, self.ui.graphicsView)
444            self.ui.graphicsView.setScene(self.scene)
445
446            if self.fSavedSettings[CARLA_KEY_CANVAS_USE_OPENGL] and hasGL:
447                self.ui.glView = QGLWidget(self)
448                self.ui.graphicsView.setViewport(self.ui.glView)
449
450            self.setupCanvas()
451
452        # ----------------------------------------------------------------------------------------------------
453        # Set-up Icons
454
455        if self.fSavedSettings[CARLA_KEY_MAIN_SYSTEM_ICONS]:
456            self.ui.act_file_connect.setIcon(getIcon('network-connect', 16, 'svgz'))
457            self.ui.act_file_refresh.setIcon(getIcon('view-refresh', 16, 'svgz'))
458            self.ui.act_file_new.setIcon(getIcon('document-new', 16, 'svgz'))
459            self.ui.act_file_open.setIcon(getIcon('document-open', 16, 'svgz'))
460            self.ui.act_file_save.setIcon(getIcon('document-save', 16, 'svgz'))
461            self.ui.act_file_save_as.setIcon(getIcon('document-save-as', 16, 'svgz'))
462            self.ui.act_file_quit.setIcon(getIcon('application-exit', 16, 'svgz'))
463            self.ui.act_engine_start.setIcon(getIcon('media-playback-start', 16, 'svgz'))
464            self.ui.act_engine_stop.setIcon(getIcon('media-playback-stop', 16, 'svgz'))
465            self.ui.act_engine_panic.setIcon(getIcon('dialog-warning', 16, 'svgz'))
466            self.ui.act_engine_config.setIcon(getIcon('configure', 16, 'svgz'))
467            self.ui.act_plugin_add.setIcon(getIcon('list-add', 16, 'svgz'))
468            self.ui.act_plugin_add_jack.setIcon(getIcon('list-add', 16, 'svgz'))
469            self.ui.act_plugin_remove_all.setIcon(getIcon('edit-delete', 16, 'svgz'))
470            self.ui.act_canvas_arrange.setIcon(getIcon('view-sort-ascending', 16, 'svgz'))
471            self.ui.act_canvas_refresh.setIcon(getIcon('view-refresh', 16, 'svgz'))
472            self.ui.act_canvas_zoom_fit.setIcon(getIcon('zoom-fit-best', 16, 'svgz'))
473            self.ui.act_canvas_zoom_in.setIcon(getIcon('zoom-in', 16, 'svgz'))
474            self.ui.act_canvas_zoom_out.setIcon(getIcon('zoom-out', 16, 'svgz'))
475            self.ui.act_canvas_zoom_100.setIcon(getIcon('zoom-original', 16, 'svgz'))
476            self.ui.act_settings_configure.setIcon(getIcon('configure', 16, 'svgz'))
477            self.ui.b_disk_add.setIcon(getIcon('list-add', 16, 'svgz'))
478            self.ui.b_disk_remove.setIcon(getIcon('list-remove', 16, 'svgz'))
479            self.ui.b_transport_play.setIcon(getIcon('media-playback-start', 16, 'svgz'))
480            self.ui.b_transport_stop.setIcon(getIcon('media-playback-stop', 16, 'svgz'))
481            self.ui.b_transport_backwards.setIcon(getIcon('media-seek-backward', 16, 'svgz'))
482            self.ui.b_transport_forwards.setIcon(getIcon('media-seek-forward', 16, 'svgz'))
483            self.ui.logs_clear.setIcon(getIcon('edit-clear', 16, 'svgz'))
484            self.ui.logs_save.setIcon(getIcon('document-save', 16, 'svgz'))
485
486        # ----------------------------------------------------------------------------------------------------
487        # Connect actions to functions
488
489        self.ui.act_file_new.triggered.connect(self.slot_fileNew)
490        self.ui.act_file_open.triggered.connect(self.slot_fileOpen)
491        self.ui.act_file_save.triggered.connect(self.slot_fileSave)
492        self.ui.act_file_save_as.triggered.connect(self.slot_fileSaveAs)
493
494        self.ui.act_engine_start.triggered.connect(self.slot_engineStart)
495        self.ui.act_engine_stop.triggered.connect(self.slot_engineStop)
496        self.ui.act_engine_panic.triggered.connect(self.slot_pluginsDisable)
497        self.ui.act_engine_config.triggered.connect(self.slot_engineConfig)
498
499        self.ui.act_plugin_add.triggered.connect(self.slot_pluginAdd)
500        self.ui.act_plugin_add_jack.triggered.connect(self.slot_jackAppAdd)
501        self.ui.act_plugin_remove_all.triggered.connect(self.slot_confirmRemoveAll)
502
503        self.ui.act_plugins_enable.triggered.connect(self.slot_pluginsEnable)
504        self.ui.act_plugins_disable.triggered.connect(self.slot_pluginsDisable)
505        self.ui.act_plugins_volume100.triggered.connect(self.slot_pluginsVolume100)
506        self.ui.act_plugins_mute.triggered.connect(self.slot_pluginsMute)
507        self.ui.act_plugins_wet100.triggered.connect(self.slot_pluginsWet100)
508        self.ui.act_plugins_bypass.triggered.connect(self.slot_pluginsBypass)
509        self.ui.act_plugins_center.triggered.connect(self.slot_pluginsCenter)
510        self.ui.act_plugins_compact.triggered.connect(self.slot_pluginsCompact)
511        self.ui.act_plugins_expand.triggered.connect(self.slot_pluginsExpand)
512
513        self.ui.act_settings_show_toolbar.toggled.connect(self.slot_showToolbar)
514        self.ui.act_settings_show_meters.toggled.connect(self.slot_showCanvasMeters)
515        self.ui.act_settings_show_keyboard.toggled.connect(self.slot_showCanvasKeyboard)
516        self.ui.act_settings_show_side_panel.toggled.connect(self.slot_showSidePanel)
517        self.ui.act_settings_configure.triggered.connect(self.slot_configureCarla)
518
519        self.ui.act_help_about.triggered.connect(self.slot_aboutCarla)
520        self.ui.act_help_about_juce.triggered.connect(self.slot_aboutJuce)
521        self.ui.act_help_about_qt.triggered.connect(self.slot_aboutQt)
522
523        self.ui.cb_disk.currentIndexChanged.connect(self.slot_diskFolderChanged)
524        self.ui.b_disk_add.clicked.connect(self.slot_diskFolderAdd)
525        self.ui.b_disk_remove.clicked.connect(self.slot_diskFolderRemove)
526        self.ui.fileTreeView.doubleClicked.connect(self.slot_fileTreeDoubleClicked)
527
528        self.ui.b_transport_play.clicked.connect(self.slot_transportPlayPause)
529        self.ui.b_transport_stop.clicked.connect(self.slot_transportStop)
530        self.ui.b_transport_backwards.clicked.connect(self.slot_transportBackwards)
531        self.ui.b_transport_forwards.clicked.connect(self.slot_transportForwards)
532        self.ui.dsb_transport_bpm.valueChanged.connect(self.slot_transportBpmChanged)
533        self.ui.cb_transport_jack.clicked.connect(self.slot_transportJackEnabled)
534        self.ui.cb_transport_link.clicked.connect(self.slot_transportLinkEnabled)
535
536        self.ui.b_xruns.clicked.connect(self.slot_xrunClear)
537
538        self.ui.listWidget.customContextMenuRequested.connect(self.slot_showPluginActionsMenu)
539
540        self.ui.keyboard.noteOn.connect(self.slot_noteOn)
541        self.ui.keyboard.noteOff.connect(self.slot_noteOff)
542
543        self.ui.tabWidget.currentChanged.connect(self.slot_tabChanged)
544
545        if withCanvas:
546            self.ui.act_canvas_show_internal.triggered.connect(self.slot_canvasShowInternal)
547            self.ui.act_canvas_show_external.triggered.connect(self.slot_canvasShowExternal)
548            self.ui.act_canvas_arrange.triggered.connect(self.slot_canvasArrange)
549            self.ui.act_canvas_refresh.triggered.connect(self.slot_canvasRefresh)
550            self.ui.act_canvas_zoom_fit.triggered.connect(self.slot_canvasZoomFit)
551            self.ui.act_canvas_zoom_in.triggered.connect(self.slot_canvasZoomIn)
552            self.ui.act_canvas_zoom_out.triggered.connect(self.slot_canvasZoomOut)
553            self.ui.act_canvas_zoom_100.triggered.connect(self.slot_canvasZoomReset)
554            self.ui.act_canvas_save_image.triggered.connect(self.slot_canvasSaveImage)
555            self.ui.act_canvas_save_image_2x.triggered.connect(self.slot_canvasSaveImage)
556            self.ui.act_canvas_save_image_4x.triggered.connect(self.slot_canvasSaveImage)
557            self.ui.act_canvas_copy_clipboard.triggered.connect(self.slot_canvasCopyToClipboard)
558            self.ui.act_canvas_arrange.setEnabled(False) # TODO, later
559            self.ui.graphicsView.horizontalScrollBar().valueChanged.connect(self.slot_horizontalScrollBarChanged)
560            self.ui.graphicsView.verticalScrollBar().valueChanged.connect(self.slot_verticalScrollBarChanged)
561            self.ui.miniCanvasPreview.miniCanvasMoved.connect(self.slot_miniCanvasMoved)
562            self.scene.scaleChanged.connect(self.slot_canvasScaleChanged)
563            self.scene.pluginSelected.connect(self.slot_canvasPluginSelected)
564            self.scene.selectionChanged.connect(self.slot_canvasSelectionChanged)
565
566        self.SIGUSR1.connect(self.slot_handleSIGUSR1)
567        self.SIGTERM.connect(self.slot_handleSIGTERM)
568
569        host.EngineStartedCallback.connect(self.slot_handleEngineStartedCallback)
570        host.EngineStoppedCallback.connect(self.slot_handleEngineStoppedCallback)
571        host.TransportModeChangedCallback.connect(self.slot_handleTransportModeChangedCallback)
572        host.BufferSizeChangedCallback.connect(self.slot_handleBufferSizeChangedCallback)
573        host.SampleRateChangedCallback.connect(self.slot_handleSampleRateChangedCallback)
574        host.CancelableActionCallback.connect(self.slot_handleCancelableActionCallback)
575        host.ProjectLoadFinishedCallback.connect(self.slot_handleProjectLoadFinishedCallback)
576
577        host.PluginAddedCallback.connect(self.slot_handlePluginAddedCallback)
578        host.PluginRemovedCallback.connect(self.slot_handlePluginRemovedCallback)
579        host.ReloadAllCallback.connect(self.slot_handleReloadAllCallback)
580
581        host.NoteOnCallback.connect(self.slot_handleNoteOnCallback)
582        host.NoteOffCallback.connect(self.slot_handleNoteOffCallback)
583
584        host.UpdateCallback.connect(self.slot_handleUpdateCallback)
585
586        if withCanvas:
587            host.PatchbayClientAddedCallback.connect(self.slot_handlePatchbayClientAddedCallback)
588            host.PatchbayClientRemovedCallback.connect(self.slot_handlePatchbayClientRemovedCallback)
589            host.PatchbayClientRenamedCallback.connect(self.slot_handlePatchbayClientRenamedCallback)
590            host.PatchbayClientDataChangedCallback.connect(self.slot_handlePatchbayClientDataChangedCallback)
591            host.PatchbayClientPositionChangedCallback.connect(self.slot_handlePatchbayClientPositionChangedCallback)
592            host.PatchbayPortAddedCallback.connect(self.slot_handlePatchbayPortAddedCallback)
593            host.PatchbayPortRemovedCallback.connect(self.slot_handlePatchbayPortRemovedCallback)
594            host.PatchbayPortChangedCallback.connect(self.slot_handlePatchbayPortChangedCallback)
595            host.PatchbayPortGroupAddedCallback.connect(self.slot_handlePatchbayPortGroupAddedCallback)
596            host.PatchbayPortGroupRemovedCallback.connect(self.slot_handlePatchbayPortGroupRemovedCallback)
597            host.PatchbayPortGroupChangedCallback.connect(self.slot_handlePatchbayPortGroupChangedCallback)
598            host.PatchbayConnectionAddedCallback.connect(self.slot_handlePatchbayConnectionAddedCallback)
599            host.PatchbayConnectionRemovedCallback.connect(self.slot_handlePatchbayConnectionRemovedCallback)
600
601        host.NSMCallback.connect(self.slot_handleNSMCallback)
602
603        host.DebugCallback.connect(self.slot_handleDebugCallback)
604        host.InfoCallback.connect(self.slot_handleInfoCallback)
605        host.ErrorCallback.connect(self.slot_handleErrorCallback)
606        host.QuitCallback.connect(self.slot_handleQuitCallback)
607        host.InlineDisplayRedrawCallback.connect(self.slot_handleInlineDisplayRedrawCallback)
608
609        # ----------------------------------------------------------------------------------------------------
610        # Final setup
611
612        self.ui.text_logs.clear()
613        self.slot_logButtonsState(False)
614        self.setProperWindowTitle()
615
616        # Disable non-supported features
617        features = gCarla.utils.get_supported_features()
618
619        if "link" not in features:
620            self.ui.cb_transport_link.setEnabled(False)
621            self.ui.cb_transport_link.setVisible(False)
622
623        if "juce" not in features:
624            self.ui.act_help_about_juce.setEnabled(False)
625            self.ui.act_help_about_juce.setVisible(False)
626
627        # Plugin needs to have timers always running so it receives messages
628        if self.host.isPlugin or self.host.isRemote:
629            self.startTimers()
630
631        # Qt needs this so it properly creates & resizes the canvas
632        self.ui.tabWidget.blockSignals(True)
633        self.ui.tabWidget.setCurrentIndex(1)
634        self.ui.tabWidget.setCurrentIndex(0)
635        self.ui.tabWidget.blockSignals(False)
636
637        # Start in patchbay tab if using forced patchbay mode
638        if host.processModeForced and host.processMode == ENGINE_PROCESS_MODE_PATCHBAY:
639            self.ui.tabWidget.setCurrentIndex(1)
640
641        # Load initial project file if set
642        if not (self.host.isControl or self.host.isPlugin):
643            projectFile = getInitialProjectFile(QApplication.instance())
644
645            if projectFile:
646                self.loadProjectLater(projectFile)
647
648        # For NSM we wait for the open message
649        if NSM_URL and host.nsmOK:
650            host.nsm_ready(NSM_CALLBACK_INIT)
651            return
652
653        if not host.isControl:
654            QTimer.singleShot(0, self.slot_engineStart)
655
656    # --------------------------------------------------------------------------------------------------------
657    # Manage visibility state, needed for NSM
658
659    def hideForNSM(self):
660        for pitem in reversed(self.fPluginList):
661            if pitem is None:
662                continue
663            pitem.getWidget().hideCustomUI()
664        self.hide()
665
666    def showIfNeeded(self):
667        if self.host.nsmOK:
668            self.ui.act_file_quit.setText(self.tr("Hide"))
669            QApplication.instance().setQuitOnLastWindowClosed(False)
670        else:
671            self.show()
672
673    # --------------------------------------------------------------------------------------------------------
674    # Setup
675
676    def compactPlugin(self, pluginId):
677        if pluginId > self.fPluginCount:
678            return
679
680        pitem = self.fPluginList[pluginId]
681
682        if pitem is None:
683            return
684
685        pitem.recreateWidget(True)
686
687    def changePluginColor(self, pluginId, color, colorStr):
688        if pluginId > self.fPluginCount:
689            return
690
691        pitem = self.fPluginList[pluginId]
692
693        if pitem is None:
694            return
695
696        self.host.set_custom_data(pluginId, CUSTOM_DATA_TYPE_PROPERTY, "CarlaColor", colorStr)
697        pitem.recreateWidget(newColor = color)
698
699    def changePluginSkin(self, pluginId, skin):
700        if pluginId > self.fPluginCount:
701            return
702
703        pitem = self.fPluginList[pluginId]
704
705        if pitem is None:
706            return
707
708        self.host.set_custom_data(pluginId, CUSTOM_DATA_TYPE_PROPERTY, "CarlaSkin", skin)
709        if skin not in ("default","rncbc","presets","mpresets"):
710            pitem.recreateWidget(newSkin = skin, newColor = (255,255,255))
711        else:
712            pitem.recreateWidget(newSkin = skin)
713
714    def findPluginInPatchbay(self, pluginId):
715        if pluginId > self.fPluginCount:
716            return
717
718        if self.fExternalPatchbay:
719            self.slot_canvasShowInternal()
720
721        if not patchcanvas.focusGroupUsingPluginId(pluginId):
722            name = self.host.get_plugin_info(pluginId)['name']
723            if not patchcanvas.focusGroupUsingGroupName(name):
724                return
725
726        self.ui.tabWidget.setCurrentIndex(1)
727
728    def switchPlugins(self, pluginIdA, pluginIdB):
729        if pluginIdA == pluginIdB:
730            return
731        if pluginIdA < 0 or pluginIdB < 0:
732            return
733        if pluginIdA >= self.fPluginCount or pluginIdB >= self.fPluginCount:
734            return
735
736        self.host.switch_plugins(pluginIdA, pluginIdB)
737
738        itemA = self.fPluginList[pluginIdA]
739        compactA = itemA.isCompacted()
740        guiShownA = itemA.isGuiShown()
741
742        itemB = self.fPluginList[pluginIdB]
743        compactB = itemB.isCompacted()
744        guiShownB = itemB.isGuiShown()
745
746        itemA.setPluginId(pluginIdA)
747        itemA.recreateWidget2(compactB, guiShownB)
748
749        itemB.setPluginId(pluginIdB)
750        itemB.recreateWidget2(compactA, guiShownA)
751
752        if self.fWithCanvas:
753            self.slot_canvasRefresh()
754
755    def setLoadRDFsNeeded(self):
756        self.fLadspaRdfNeedsUpdate = True
757
758    def setProperWindowTitle(self):
759        title = self.fClientName
760
761        if self.fProjectFilename and not self.host.nsmOK:
762            title += " - %s" % os.path.basename(self.fProjectFilename)
763        if self.fSessionManagerName:
764            title += " (%s)" % self.fSessionManagerName
765
766        self.setWindowTitle(title)
767
768    def updateBufferSize(self, newBufferSize):
769        if self.fBufferSize == newBufferSize:
770            return
771        self.fBufferSize = newBufferSize
772        self.ui.cb_buffer_size.clear()
773        self.ui.cb_buffer_size.addItem(str(newBufferSize))
774        self.ui.cb_buffer_size.setCurrentIndex(0)
775
776    def updateSampleRate(self, newSampleRate):
777        if self.fSampleRate == newSampleRate:
778            return
779        self.fSampleRate = newSampleRate
780        self.ui.cb_sample_rate.clear()
781        self.ui.cb_sample_rate.addItem(str(newSampleRate))
782        self.ui.cb_sample_rate.setCurrentIndex(0)
783        self.refreshTransport(True)
784
785    # --------------------------------------------------------------------------------------------------------
786    # Files
787
788    def loadProjectNow(self):
789        if not self.fProjectFilename:
790            return qCritical("ERROR: loading project without filename set")
791        if self.host.nsmOK and not os.path.exists(self.fProjectFilename):
792            return
793
794        self.projectLoadingStarted()
795        self.fIsProjectLoading = True
796
797        if not self.host.load_project(self.fProjectFilename):
798            self.fIsProjectLoading = False
799            self.projectLoadingFinished(True)
800
801            CustomMessageBox(self, QMessageBox.Critical, self.tr("Error"), self.tr("Failed to load project"),
802                             self.host.get_last_error(),
803                             QMessageBox.Ok, QMessageBox.Ok)
804
805    def loadProjectLater(self, filename):
806        self.fProjectFilename = QFileInfo(filename).absoluteFilePath()
807        self.setProperWindowTitle()
808        QTimer.singleShot(1, self.slot_loadProjectNow)
809
810    def saveProjectNow(self):
811        if not self.fProjectFilename:
812            return qCritical("ERROR: saving project without filename set")
813
814        if not self.host.save_project(self.fProjectFilename):
815            CustomMessageBox(self, QMessageBox.Critical, self.tr("Error"), self.tr("Failed to save project"),
816                             self.host.get_last_error(),
817                             QMessageBox.Ok, QMessageBox.Ok)
818            return
819
820    def projectLoadingStarted(self):
821        self.ui.rack.setEnabled(False)
822        self.ui.graphicsView.setEnabled(False)
823
824    def projectLoadingFinished(self, refreshCanvas):
825        self.ui.rack.setEnabled(True)
826        self.ui.graphicsView.setEnabled(True)
827
828        if self.fCustomStopAction == self.CUSTOM_ACTION_APP_CLOSE or not self.fWithCanvas:
829            return
830
831        if refreshCanvas and not self.loadExternalCanvasGroupPositionsIfNeeded(self.fProjectFilename):
832            QTimer.singleShot(1, self.slot_canvasRefresh)
833
834    def loadExternalCanvasGroupPositionsIfNeeded(self, filename):
835        extrafile = filename.rsplit(".",1)[0]+".json"
836        if not os.path.exists(extrafile):
837            return False
838
839        with open(filename, "r") as fh:
840            if "".join(fh.readlines(90)).find("<CARLA-PROJECT VERSION='2.0'>") < 0:
841                return False
842
843        with open(extrafile, "r") as fh:
844            try:
845                canvasdata = json.load(fh)['canvas']
846            except:
847                return False
848
849        print("NOTICE: loading old-style canvas group positions via legacy json file")
850        patchcanvas.restoreGroupPositions(canvasdata)
851        QTimer.singleShot(1, self.slot_canvasRefresh)
852        return True
853
854    # --------------------------------------------------------------------------------------------------------
855    # Files (menu actions)
856
857    @pyqtSlot()
858    def slot_fileNew(self):
859        if self.fPluginCount > 0 and QMessageBox.question(self, self.tr("New File"),
860                                                                self.tr("Plugins that are currently loaded will be removed. Are you sure?"),
861                                                                QMessageBox.Yes|QMessageBox.No) == QMessageBox.No:
862            return
863
864        self.pluginRemoveAll()
865        self.fProjectFilename = ""
866        self.setProperWindowTitle()
867        self.host.clear_project_filename()
868
869    @pyqtSlot()
870    def slot_fileOpen(self):
871        fileFilter = self.tr("Carla Project File (*.carxp);;Carla Preset File (*.carxs)")
872        filename, ok = QFileDialog.getOpenFileName(self, self.tr("Open Carla Project File"), self.fSavedSettings[CARLA_KEY_MAIN_PROJECT_FOLDER], filter=fileFilter)
873
874        # FIXME use ok value, test if it works as expected
875        if not filename:
876            return
877
878        newFile = True
879
880        if self.fPluginCount > 0:
881            ask = QMessageBox.question(self, self.tr("Question"), self.tr("There are some plugins loaded, do you want to remove them now?"),
882                                                                          QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
883            newFile = (ask == QMessageBox.Yes)
884
885        if newFile:
886            self.pluginRemoveAll()
887            self.fProjectFilename = filename
888            self.setProperWindowTitle()
889            self.loadProjectNow()
890        else:
891            filenameOld = self.fProjectFilename
892            self.fProjectFilename = filename
893            self.loadProjectNow()
894            self.fProjectFilename = filenameOld
895
896    @pyqtSlot()
897    def slot_fileSave(self, saveAs=False):
898        if self.fProjectFilename and not saveAs:
899            return self.saveProjectNow()
900
901        fileFilter = self.tr("Carla Project File (*.carxp)")
902        filename, ok = QFileDialog.getSaveFileName(self, self.tr("Save Carla Project File"), self.fSavedSettings[CARLA_KEY_MAIN_PROJECT_FOLDER], filter=fileFilter)
903
904        # FIXME use ok value, test if it works as expected
905        if not filename:
906            return
907
908        if not filename.lower().endswith(".carxp"):
909            filename += ".carxp"
910
911        if self.fProjectFilename != filename:
912            self.fProjectFilename = filename
913            self.setProperWindowTitle()
914
915        self.saveProjectNow()
916
917    @pyqtSlot()
918    def slot_fileSaveAs(self):
919        self.slot_fileSave(True)
920
921    @pyqtSlot()
922    def slot_loadProjectNow(self):
923        self.loadProjectNow()
924
925    # --------------------------------------------------------------------------------------------------------
926    # Engine (menu actions)
927
928    @pyqtSlot()
929    def slot_engineStart(self):
930        audioDriver = setEngineSettings(self.host)
931        firstInit   = self.fFirstEngineInit
932
933        self.fFirstEngineInit = False
934        self.ui.text_logs.appendPlainText("======= Starting engine =======")
935
936        if self.host.engine_init(audioDriver, self.fClientName):
937            if firstInit and not (self.host.isControl or self.host.isPlugin):
938                settings = QSafeSettings()
939                lastBpm  = settings.value("LastBPM", 120.0, float)
940                del settings
941                if lastBpm >= 20.0:
942                    self.host.transport_bpm(lastBpm)
943            return
944
945        elif firstInit:
946            self.ui.text_logs.appendPlainText("Failed to start engine on first try, ignored")
947            return
948
949        audioError = self.host.get_last_error()
950
951        if audioError:
952            QMessageBox.critical(self, self.tr("Error"), self.tr("Could not connect to Audio backend '%s', possible reasons:\n%s" % (audioDriver, audioError)))
953        else:
954            QMessageBox.critical(self, self.tr("Error"), self.tr("Could not connect to Audio backend '%s'" % audioDriver))
955
956    @pyqtSlot()
957    def slot_engineStop(self, forced = False):
958        self.ui.text_logs.appendPlainText("======= Stopping engine =======")
959
960        if self.fPluginCount == 0 or not self.host.is_engine_running():
961            self.engineStopFinal()
962            return True
963
964        if not forced:
965            ask = QMessageBox.question(self, self.tr("Warning"), self.tr("There are still some plugins loaded, you need to remove them to stop the engine.\n"
966                                                                         "Do you want to do this now?"),
967                                                                         QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
968            if ask != QMessageBox.Yes:
969                return False
970
971        return self.slot_engineStopTryAgain()
972
973    @pyqtSlot()
974    def slot_engineConfig(self):
975        dialog = RuntimeDriverSettingsW(self.fParentOrSelf, self.host)
976
977        if not dialog.exec_():
978            return
979
980        audioDevice, bufferSize, sampleRate = dialog.getValues()
981
982        if self.host.is_engine_running():
983            self.host.set_engine_buffer_size_and_sample_rate(bufferSize, sampleRate)
984        else:
985            self.host.set_engine_option(ENGINE_OPTION_AUDIO_DEVICE, 0, audioDevice)
986            self.host.set_engine_option(ENGINE_OPTION_AUDIO_BUFFER_SIZE, bufferSize, "")
987            self.host.set_engine_option(ENGINE_OPTION_AUDIO_SAMPLE_RATE, sampleRate, "")
988
989    @pyqtSlot()
990    def slot_engineStopTryAgain(self):
991        if self.host.is_engine_running() and not self.host.set_engine_about_to_close():
992            QTimer.singleShot(0, self.slot_engineStopTryAgain)
993            return False
994
995        self.engineStopFinal()
996        return True
997
998    def engineStopFinal(self):
999        patchcanvas.handleAllPluginsRemoved()
1000        self.killTimers()
1001
1002        if self.fCustomStopAction == self.CUSTOM_ACTION_PROJECT_LOAD:
1003            self.removeAllPlugins()
1004        else:
1005            self.fProjectFilename = ""
1006            self.setProperWindowTitle()
1007            if self.fPluginCount != 0:
1008                self.fCurrentlyRemovingAllPlugins = True
1009                self.projectLoadingStarted()
1010
1011        if self.host.is_engine_running() and not self.host.remove_all_plugins():
1012            self.ui.text_logs.appendPlainText("Failed to remove all plugins, error was:")
1013            self.ui.text_logs.appendPlainText(self.host.get_last_error())
1014
1015        if not self.host.engine_close():
1016            self.ui.text_logs.appendPlainText("Failed to stop engine, error was:")
1017            self.ui.text_logs.appendPlainText(self.host.get_last_error())
1018
1019        if self.fCustomStopAction == self.CUSTOM_ACTION_APP_CLOSE:
1020            self.close()
1021        elif self.fCustomStopAction == self.CUSTOM_ACTION_PROJECT_LOAD:
1022            self.slot_engineStart()
1023            self.loadProjectNow()
1024            self.host.nsm_ready(NSM_CALLBACK_OPEN)
1025
1026        self.fCustomStopAction = self.CUSTOM_ACTION_NONE
1027
1028    # --------------------------------------------------------------------------------------------------------
1029    # Engine (host callbacks)
1030
1031    @pyqtSlot(int, int, int, int, float, str)
1032    def slot_handleEngineStartedCallback(self, pluginCount, processMode, transportMode, bufferSize, sampleRate, driverName):
1033        self.ui.menu_PluginMacros.setEnabled(True)
1034        self.ui.menu_Canvas.setEnabled(True)
1035        self.ui.w_transport.setEnabled(True)
1036
1037        self.ui.act_canvas_show_internal.blockSignals(True)
1038        self.ui.act_canvas_show_external.blockSignals(True)
1039
1040        if processMode == ENGINE_PROCESS_MODE_PATCHBAY and not self.host.isPlugin:
1041            self.ui.act_canvas_show_internal.setChecked(True)
1042            self.ui.act_canvas_show_internal.setVisible(True)
1043            self.ui.act_canvas_show_external.setChecked(False)
1044            self.ui.act_canvas_show_external.setVisible(True)
1045            self.fExternalPatchbay = False
1046        else:
1047            self.ui.act_canvas_show_internal.setChecked(False)
1048            self.ui.act_canvas_show_internal.setVisible(False)
1049            self.ui.act_canvas_show_external.setChecked(True)
1050            self.ui.act_canvas_show_external.setVisible(False)
1051            self.fExternalPatchbay = not self.host.isPlugin
1052
1053        self.ui.act_canvas_show_internal.blockSignals(False)
1054        self.ui.act_canvas_show_external.blockSignals(False)
1055
1056        if not (self.host.isControl or self.host.isPlugin):
1057            canSave = (self.fProjectFilename and os.path.exists(self.fProjectFilename)) or not self.fSessionManagerName
1058            self.ui.act_file_save.setEnabled(canSave)
1059            self.ui.act_engine_start.setEnabled(False)
1060            self.ui.act_engine_stop.setEnabled(True)
1061
1062        if not self.host.isPlugin:
1063            self.enableTransport(transportMode != ENGINE_TRANSPORT_MODE_DISABLED)
1064
1065        if self.host.isPlugin or not self.fSessionManagerName:
1066            self.ui.act_file_open.setEnabled(True)
1067            self.ui.act_file_save_as.setEnabled(True)
1068
1069        self.ui.cb_transport_jack.setChecked(transportMode == ENGINE_TRANSPORT_MODE_JACK)
1070        self.ui.cb_transport_jack.setEnabled(driverName == "JACK" and processMode != ENGINE_PROCESS_MODE_MULTIPLE_CLIENTS)
1071
1072        if self.ui.cb_transport_link.isEnabled():
1073            self.ui.cb_transport_link.setChecked(":link:" in self.host.transportExtra)
1074
1075        self.updateBufferSize(bufferSize)
1076        self.updateSampleRate(int(sampleRate))
1077        self.refreshRuntimeInfo(0.0, 0)
1078        self.startTimers()
1079
1080        self.ui.text_logs.appendPlainText("======= Engine started ========")
1081        self.ui.text_logs.appendPlainText("Carla engine started, details:")
1082        self.ui.text_logs.appendPlainText("  Driver name:  %s" % driverName)
1083        self.ui.text_logs.appendPlainText("  Sample rate:  %i" % int(sampleRate))
1084        self.ui.text_logs.appendPlainText("  Process mode: %s" % processMode2Str(processMode))
1085
1086    @pyqtSlot()
1087    def slot_handleEngineStoppedCallback(self):
1088        self.ui.text_logs.appendPlainText("======= Engine stopped ========")
1089
1090        if self.fWithCanvas:
1091            patchcanvas.clear()
1092
1093        self.killTimers()
1094
1095        # just in case
1096        self.removeAllPlugins()
1097        self.refreshRuntimeInfo(0.0, 0)
1098
1099        self.ui.menu_PluginMacros.setEnabled(False)
1100        self.ui.menu_Canvas.setEnabled(False)
1101        self.ui.w_transport.setEnabled(False)
1102
1103        if not (self.host.isControl or self.host.isPlugin):
1104            self.ui.act_file_save.setEnabled(False)
1105            self.ui.act_engine_start.setEnabled(True)
1106            self.ui.act_engine_stop.setEnabled(False)
1107
1108        if self.host.isPlugin or not self.fSessionManagerName:
1109            self.ui.act_file_open.setEnabled(False)
1110            self.ui.act_file_save_as.setEnabled(False)
1111
1112    @pyqtSlot(int, str)
1113    def slot_handleTransportModeChangedCallback(self, transportMode, transportExtra):
1114        self.enableTransport(transportMode != ENGINE_TRANSPORT_MODE_DISABLED)
1115
1116        self.ui.cb_transport_jack.setChecked(transportMode == ENGINE_TRANSPORT_MODE_JACK)
1117        self.ui.cb_transport_link.setChecked(":link:" in transportExtra)
1118
1119    @pyqtSlot(int)
1120    def slot_handleBufferSizeChangedCallback(self, newBufferSize):
1121        self.updateBufferSize(newBufferSize)
1122
1123    @pyqtSlot(float)
1124    def slot_handleSampleRateChangedCallback(self, newSampleRate):
1125        self.updateSampleRate(int(newSampleRate))
1126
1127    @pyqtSlot(int, bool, str)
1128    def slot_handleCancelableActionCallback(self, pluginId, started, action):
1129        if self.fCancelableActionBox is not None:
1130            self.fCancelableActionBox.close()
1131
1132        if started:
1133            self.fCancelableActionBox = QMessageBox(self)
1134            self.fCancelableActionBox.setIcon(QMessageBox.Information)
1135            self.fCancelableActionBox.setWindowTitle(self.tr("Action in progress"))
1136            self.fCancelableActionBox.setText(action)
1137            self.fCancelableActionBox.setInformativeText(self.tr("An action is in progress, please wait..."))
1138            self.fCancelableActionBox.setStandardButtons(QMessageBox.Cancel)
1139            self.fCancelableActionBox.setDefaultButton(QMessageBox.Cancel)
1140            self.fCancelableActionBox.buttonClicked.connect(self.slot_canlableActionBoxClicked)
1141            self.fCancelableActionBox.show()
1142
1143        else:
1144            self.fCancelableActionBox = None
1145
1146    @pyqtSlot()
1147    def slot_canlableActionBoxClicked(self):
1148        self.host.cancel_engine_action()
1149
1150    @pyqtSlot()
1151    def slot_handleProjectLoadFinishedCallback(self):
1152        self.fIsProjectLoading = False
1153        self.projectLoadingFinished(False)
1154
1155    # --------------------------------------------------------------------------------------------------------
1156    # Plugins
1157
1158    def removeAllPlugins(self):
1159        self.ui.act_plugin_remove_all.setEnabled(False)
1160        patchcanvas.handleAllPluginsRemoved()
1161
1162        while self.ui.listWidget.takeItem(0):
1163            pass
1164
1165        self.clearSideStuff()
1166
1167        for pitem in self.fPluginList:
1168            if pitem is None:
1169                continue
1170
1171            pitem.close()
1172            del pitem
1173
1174        self.fPluginCount = 0
1175        self.fPluginList  = []
1176
1177    # --------------------------------------------------------------------------------------------------------
1178    # Plugins (menu actions)
1179
1180    def showAddPluginDialog(self):
1181        if self.fPluginDatabaseDialog is None:
1182            self.fPluginDatabaseDialog = PluginDatabaseW(self.fParentOrSelf, self.host,
1183                                                         self.fSavedSettings[CARLA_KEY_MAIN_SYSTEM_ICONS])
1184        dialog = self.fPluginDatabaseDialog
1185
1186        ret = dialog.exec_()
1187
1188        if dialog.fFavoritePluginsChanged:
1189            self.fFavoritePlugins = dialog.fFavoritePlugins
1190
1191        if not ret:
1192            return
1193
1194        if not self.host.is_engine_running():
1195            QMessageBox.warning(self, self.tr("Warning"), self.tr("Cannot add new plugins while engine is stopped"))
1196            return
1197
1198        btype    = dialog.fRetPlugin['build']
1199        ptype    = dialog.fRetPlugin['type']
1200        filename = dialog.fRetPlugin['filename']
1201        label    = dialog.fRetPlugin['label']
1202        uniqueId = dialog.fRetPlugin['uniqueId']
1203        extraPtr = self.getExtraPtr(dialog.fRetPlugin)
1204
1205        return (btype, ptype, filename, label, uniqueId, extraPtr)
1206
1207    def showAddJackAppDialog(self):
1208        dialog = JackApplicationW(self.fParentOrSelf, self.fProjectFilename)
1209
1210        if not dialog.exec_():
1211            return
1212
1213        if not self.host.is_engine_running():
1214            QMessageBox.warning(self, self.tr("Warning"), self.tr("Cannot add new plugins while engine is stopped"))
1215            return
1216
1217        return dialog.getCommandAndFlags()
1218
1219    @pyqtSlot()
1220    def slot_favoritePluginAdd(self):
1221        plugin = self.sender().data()
1222
1223        if plugin is None:
1224            return
1225
1226        if not self.host.add_plugin(plugin['build'], plugin['type'], plugin['filename'], None,
1227                                    plugin['label'], plugin['uniqueId'], None, PLUGIN_OPTIONS_NULL):
1228            # remove plugin from favorites
1229            try:
1230                self.fFavoritePlugins.remove(plugin)
1231            except ValueError:
1232                pass
1233            else:
1234                settingsDBf = QSafeSettings("falkTX", "CarlaDatabase2")
1235                settingsDBf.setValue("PluginDatabase/Favorites", self.fFavoritePlugins)
1236                settingsDBf.sync()
1237                del settingsDBf
1238
1239            CustomMessageBox(self,
1240                             QMessageBox.Critical,
1241                             self.tr("Error"),
1242                             self.tr("Failed to load plugin"),
1243                             self.host.get_last_error(), QMessageBox.Ok, QMessageBox.Ok)
1244
1245    @pyqtSlot()
1246    def slot_showPluginActionsMenu(self):
1247        menu = QMenu(self)
1248
1249        menu.addSection("Plugins")
1250        menu.addAction(self.ui.act_plugin_add)
1251
1252        if len(self.fFavoritePlugins) != 0:
1253            fmenu = QMenu("Add from favorites", self)
1254            for p in self.fFavoritePlugins:
1255                act = fmenu.addAction(p['name'])
1256                act.setData(p)
1257                act.triggered.connect(self.slot_favoritePluginAdd)
1258            menu.addMenu(fmenu)
1259
1260        menu.addAction(self.ui.act_plugin_remove_all)
1261
1262        menu.addSection("All plugins (macros)")
1263        menu.addAction(self.ui.act_plugins_enable)
1264        menu.addAction(self.ui.act_plugins_disable)
1265        menu.addSeparator()
1266        menu.addAction(self.ui.act_plugins_volume100)
1267        menu.addAction(self.ui.act_plugins_mute)
1268        menu.addSeparator()
1269        menu.addAction(self.ui.act_plugins_wet100)
1270        menu.addAction(self.ui.act_plugins_bypass)
1271        menu.addSeparator()
1272        menu.addAction(self.ui.act_plugins_center)
1273        menu.addSeparator()
1274        menu.addAction(self.ui.act_plugins_compact)
1275        menu.addAction(self.ui.act_plugins_expand)
1276
1277        menu.exec_(QCursor.pos())
1278
1279    @pyqtSlot()
1280    def slot_pluginAdd(self):
1281        data = self.showAddPluginDialog()
1282
1283        if data is None:
1284            return
1285
1286        btype, ptype, filename, label, uniqueId, extraPtr = data
1287
1288        if not self.host.add_plugin(btype, ptype, filename, None, label, uniqueId, extraPtr, PLUGIN_OPTIONS_NULL):
1289            CustomMessageBox(self, QMessageBox.Critical, self.tr("Error"), self.tr("Failed to load plugin"),
1290                             self.host.get_last_error(), QMessageBox.Ok, QMessageBox.Ok)
1291
1292    @pyqtSlot()
1293    def slot_confirmRemoveAll(self):
1294        if self.fPluginCount == 0:
1295            return
1296
1297        if QMessageBox.question(self, self.tr("Remove All"),
1298                                      self.tr("Are you sure you want to remove all plugins?"),
1299                                      QMessageBox.Yes|QMessageBox.No) == QMessageBox.No:
1300            return
1301
1302        self.pluginRemoveAll()
1303
1304    def pluginRemoveAll(self):
1305        if self.fPluginCount == 0:
1306            return
1307
1308        self.fCurrentlyRemovingAllPlugins = True
1309        self.projectLoadingStarted()
1310
1311        if not self.host.remove_all_plugins():
1312            self.projectLoadingFinished(True)
1313            self.fCurrentlyRemovingAllPlugins = False
1314            CustomMessageBox(self, QMessageBox.Warning, self.tr("Error"), self.tr("Operation failed"),
1315                                   self.host.get_last_error(), QMessageBox.Ok, QMessageBox.Ok)
1316
1317    @pyqtSlot()
1318    def slot_jackAppAdd(self):
1319        data = self.showAddJackAppDialog()
1320
1321        if data is None:
1322            return
1323
1324        filename, name, label = data
1325
1326        if not filename:
1327            CustomMessageBox(self, QMessageBox.Critical, self.tr("Error"), self.tr("Cannot add jack application"),
1328                                   self.tr("command is empty"), QMessageBox.Ok, QMessageBox.Ok)
1329            return
1330
1331        if not self.host.add_plugin(BINARY_NATIVE, PLUGIN_JACK, filename, name, label, 0, None, PLUGIN_OPTIONS_NULL):
1332            CustomMessageBox(self, QMessageBox.Critical, self.tr("Error"), self.tr("Failed to load plugin"),
1333                                   self.host.get_last_error(), QMessageBox.Ok, QMessageBox.Ok)
1334
1335    # --------------------------------------------------------------------------------------------------------
1336    # Plugins (macros)
1337
1338    @pyqtSlot()
1339    def slot_pluginsEnable(self):
1340        if not self.host.is_engine_running():
1341            return
1342
1343        for pitem in self.fPluginList:
1344            if pitem is None:
1345                break
1346
1347            pitem.getWidget().setActive(True, True, True)
1348
1349    @pyqtSlot()
1350    def slot_pluginsDisable(self):
1351        if not self.host.is_engine_running():
1352            return
1353
1354        for pitem in self.fPluginList:
1355            if pitem is None:
1356                break
1357
1358            pitem.getWidget().setActive(False, True, True)
1359
1360    @pyqtSlot()
1361    def slot_pluginsVolume100(self):
1362        if not self.host.is_engine_running():
1363            return
1364
1365        for pitem in self.fPluginList:
1366            if pitem is None:
1367                break
1368
1369            pitem.getWidget().setInternalParameter(PLUGIN_CAN_VOLUME, 1.0)
1370
1371    @pyqtSlot()
1372    def slot_pluginsMute(self):
1373        if not self.host.is_engine_running():
1374            return
1375
1376        for pitem in self.fPluginList:
1377            if pitem is None:
1378                break
1379
1380            pitem.getWidget().setInternalParameter(PLUGIN_CAN_VOLUME, 0.0)
1381
1382    @pyqtSlot()
1383    def slot_pluginsWet100(self):
1384        if not self.host.is_engine_running():
1385            return
1386
1387        for pitem in self.fPluginList:
1388            if pitem is None:
1389                break
1390
1391            pitem.getWidget().setInternalParameter(PLUGIN_CAN_DRYWET, 1.0)
1392
1393    @pyqtSlot()
1394    def slot_pluginsBypass(self):
1395        if not self.host.is_engine_running():
1396            return
1397
1398        for pitem in self.fPluginList:
1399            if pitem is None:
1400                break
1401
1402            pitem.getWidget().setInternalParameter(PLUGIN_CAN_DRYWET, 0.0)
1403
1404    @pyqtSlot()
1405    def slot_pluginsCenter(self):
1406        if not self.host.is_engine_running():
1407            return
1408
1409        for pitem in self.fPluginList:
1410            if pitem is None:
1411                break
1412
1413            pitem.getWidget().setInternalParameter(PARAMETER_BALANCE_LEFT, -1.0)
1414            pitem.getWidget().setInternalParameter(PARAMETER_BALANCE_RIGHT, 1.0)
1415            pitem.getWidget().setInternalParameter(PARAMETER_PANNING, 0.0)
1416
1417    @pyqtSlot()
1418    def slot_pluginsCompact(self):
1419        for pitem in self.fPluginList:
1420            if pitem is None:
1421                break
1422            pitem.compact()
1423
1424    @pyqtSlot()
1425    def slot_pluginsExpand(self):
1426        for pitem in self.fPluginList:
1427            if pitem is None:
1428                break
1429            pitem.expand()
1430
1431    # --------------------------------------------------------------------------------------------------------
1432    # Plugins (host callbacks)
1433
1434    @pyqtSlot(int, str)
1435    def slot_handlePluginAddedCallback(self, pluginId, pluginName):
1436        if pluginId != self.fPluginCount:
1437            print("ERROR: pluginAdded mismatch Id:", pluginId, self.fPluginCount)
1438            pitem = self.getPluginItem(pluginId)
1439            pitem.recreateWidget()
1440            return
1441
1442        pitem = self.ui.listWidget.createItem(pluginId, self.fSavedSettings[CARLA_KEY_MAIN_CLASSIC_SKIN])
1443        self.fPluginList.append(pitem)
1444        self.fPluginCount += 1
1445
1446        self.ui.act_plugin_remove_all.setEnabled(self.fPluginCount > 0)
1447
1448    @pyqtSlot(int)
1449    def slot_handlePluginRemovedCallback(self, pluginId):
1450        if self.fWithCanvas:
1451            patchcanvas.handlePluginRemoved(pluginId)
1452
1453        if pluginId in self.fSelectedPlugins:
1454            self.clearSideStuff()
1455
1456        if self.fPluginCount == 0:
1457            return
1458
1459        pitem = self.getPluginItem(pluginId)
1460
1461        self.fPluginCount -= 1
1462        self.fPluginList.pop(pluginId)
1463        self.ui.listWidget.takeItem(pluginId)
1464
1465        if pitem is not None:
1466            pitem.close()
1467            del pitem
1468
1469        if self.fPluginCount == 0:
1470            self.ui.act_plugin_remove_all.setEnabled(False)
1471            if self.fCurrentlyRemovingAllPlugins:
1472                self.fCurrentlyRemovingAllPlugins = False
1473                self.projectLoadingFinished(False)
1474            return
1475
1476        # push all plugins 1 slot back
1477        for i in range(pluginId, self.fPluginCount):
1478            pitem = self.fPluginList[i]
1479            pitem.setPluginId(i)
1480
1481        self.ui.act_plugin_remove_all.setEnabled(True)
1482
1483    # --------------------------------------------------------------------------------------------------------
1484    # Canvas
1485
1486    def clearSideStuff(self):
1487        if self.fWithCanvas:
1488            self.scene.clearSelection()
1489
1490        self.fSelectedPlugins = []
1491
1492        self.ui.keyboard.allNotesOff(False)
1493        self.ui.scrollArea.setEnabled(False)
1494
1495        self.fPeaksCleared = True
1496        self.ui.peak_in.displayMeter(1, 0.0, True)
1497        self.ui.peak_in.displayMeter(2, 0.0, True)
1498        self.ui.peak_out.displayMeter(1, 0.0, True)
1499        self.ui.peak_out.displayMeter(2, 0.0, True)
1500
1501    def setupCanvas(self):
1502        pOptions = patchcanvas.options_t()
1503        pOptions.theme_name        = self.fSavedSettings[CARLA_KEY_CANVAS_THEME]
1504        pOptions.auto_hide_groups  = self.fSavedSettings[CARLA_KEY_CANVAS_AUTO_HIDE_GROUPS]
1505        pOptions.auto_select_items = self.fSavedSettings[CARLA_KEY_CANVAS_AUTO_SELECT_ITEMS]
1506        pOptions.use_bezier_lines  = self.fSavedSettings[CARLA_KEY_CANVAS_USE_BEZIER_LINES]
1507        pOptions.antialiasing      = self.fSavedSettings[CARLA_KEY_CANVAS_ANTIALIASING]
1508        pOptions.inline_displays   = self.fSavedSettings[CARLA_KEY_CANVAS_INLINE_DISPLAYS]
1509
1510        if self.fSavedSettings[CARLA_KEY_CANVAS_FANCY_EYE_CANDY]:
1511            pOptions.eyecandy = patchcanvas.EYECANDY_FULL
1512        elif self.fSavedSettings[CARLA_KEY_CANVAS_EYE_CANDY]:
1513            pOptions.eyecandy = patchcanvas.EYECANDY_SMALL
1514        else:
1515            pOptions.eyecandy = patchcanvas.EYECANDY_NONE
1516
1517        pFeatures = patchcanvas.features_t()
1518        pFeatures.group_info   = False
1519        pFeatures.group_rename = False
1520        pFeatures.port_info    = False
1521        pFeatures.port_rename  = False
1522        pFeatures.handle_group_pos = False
1523
1524        patchcanvas.setOptions(pOptions)
1525        patchcanvas.setFeatures(pFeatures)
1526        patchcanvas.init("Carla2", self.scene, canvasCallback, False)
1527
1528        tryCanvasSize = self.fSavedSettings[CARLA_KEY_CANVAS_SIZE].split("x")
1529
1530        if len(tryCanvasSize) == 2 and tryCanvasSize[0].isdigit() and tryCanvasSize[1].isdigit():
1531            self.fCanvasWidth  = int(tryCanvasSize[0])
1532            self.fCanvasHeight = int(tryCanvasSize[1])
1533        else:
1534            self.fCanvasWidth  = CARLA_DEFAULT_CANVAS_SIZE_WIDTH
1535            self.fCanvasHeight = CARLA_DEFAULT_CANVAS_SIZE_HEIGHT
1536
1537        patchcanvas.setCanvasSize(0, 0, self.fCanvasWidth, self.fCanvasHeight)
1538        patchcanvas.setInitialPos(self.fCanvasWidth / 2, self.fCanvasHeight / 2)
1539        self.ui.graphicsView.setSceneRect(0, 0, self.fCanvasWidth, self.fCanvasHeight)
1540
1541        self.ui.miniCanvasPreview.setViewTheme(patchcanvas.canvas.theme.canvas_bg, patchcanvas.canvas.theme.rubberband_brush, patchcanvas.canvas.theme.rubberband_pen.color())
1542        self.ui.miniCanvasPreview.init(self.scene, self.fCanvasWidth, self.fCanvasHeight, self.fSavedSettings[CARLA_KEY_CUSTOM_PAINTING])
1543
1544        if self.fSavedSettings[CARLA_KEY_CANVAS_ANTIALIASING] != patchcanvas.ANTIALIASING_NONE:
1545            self.ui.graphicsView.setRenderHint(QPainter.Antialiasing, True)
1546
1547            fullAA = self.fSavedSettings[CARLA_KEY_CANVAS_ANTIALIASING] == patchcanvas.ANTIALIASING_FULL
1548            self.ui.graphicsView.setRenderHint(QPainter.SmoothPixmapTransform, fullAA)
1549            self.ui.graphicsView.setRenderHint(QPainter.TextAntialiasing, fullAA)
1550
1551            if self.fSavedSettings[CARLA_KEY_CANVAS_USE_OPENGL] and hasGL:
1552                self.ui.graphicsView.setRenderHint(QPainter.HighQualityAntialiasing, self.fSavedSettings[CARLA_KEY_CANVAS_HQ_ANTIALIASING])
1553
1554        else:
1555            self.ui.graphicsView.setRenderHint(QPainter.Antialiasing, False)
1556
1557        if self.fSavedSettings[CARLA_KEY_CANVAS_FULL_REPAINTS]:
1558            self.ui.graphicsView.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
1559        else:
1560            self.ui.graphicsView.setViewportUpdateMode(QGraphicsView.MinimalViewportUpdate)
1561
1562    def updateCanvasInitialPos(self):
1563        x = self.ui.graphicsView.horizontalScrollBar().value() + self.width()/4
1564        y = self.ui.graphicsView.verticalScrollBar().value() + self.height()/4
1565        patchcanvas.setInitialPos(x, y)
1566
1567    def updateMiniCanvasLater(self):
1568        QTimer.singleShot(self.fMiniCanvasUpdateTimeout, self.ui.miniCanvasPreview.update)
1569
1570    # --------------------------------------------------------------------------------------------------------
1571    # Canvas (menu actions)
1572
1573    @pyqtSlot()
1574    def slot_canvasShowInternal(self):
1575        self.fExternalPatchbay = False
1576        self.ui.act_canvas_show_internal.blockSignals(True)
1577        self.ui.act_canvas_show_external.blockSignals(True)
1578        self.ui.act_canvas_show_internal.setChecked(True)
1579        self.ui.act_canvas_show_external.setChecked(False)
1580        self.ui.act_canvas_show_internal.blockSignals(False)
1581        self.ui.act_canvas_show_external.blockSignals(False)
1582        self.slot_canvasRefresh()
1583
1584    @pyqtSlot()
1585    def slot_canvasShowExternal(self):
1586        self.fExternalPatchbay = True
1587        self.ui.act_canvas_show_internal.blockSignals(True)
1588        self.ui.act_canvas_show_external.blockSignals(True)
1589        self.ui.act_canvas_show_internal.setChecked(False)
1590        self.ui.act_canvas_show_external.setChecked(True)
1591        self.ui.act_canvas_show_internal.blockSignals(False)
1592        self.ui.act_canvas_show_external.blockSignals(False)
1593        self.slot_canvasRefresh()
1594
1595    @pyqtSlot()
1596    def slot_canvasArrange(self):
1597        patchcanvas.arrange()
1598
1599    @pyqtSlot()
1600    def slot_canvasRefresh(self):
1601        patchcanvas.clear()
1602
1603        if self.host.processMode == ENGINE_PROCESS_MODE_CONTINUOUS_RACK and self.host.isPlugin:
1604            return
1605
1606        if self.host.is_engine_running():
1607            self.host.patchbay_refresh(self.fExternalPatchbay)
1608
1609        self.updateMiniCanvasLater()
1610
1611    @pyqtSlot()
1612    def slot_canvasZoomFit(self):
1613        self.scene.zoom_fit()
1614
1615    @pyqtSlot()
1616    def slot_canvasZoomIn(self):
1617        self.scene.zoom_in()
1618
1619    @pyqtSlot()
1620    def slot_canvasZoomOut(self):
1621        self.scene.zoom_out()
1622
1623    @pyqtSlot()
1624    def slot_canvasZoomReset(self):
1625        self.scene.zoom_reset()
1626
1627    def _canvasImageRender(self, zoom = 1.0):
1628        image   = QImage(self.scene.width()*zoom, self.scene.height()*zoom, QImage.Format_RGB32)
1629        painter = QPainter(image)
1630        painter.save()
1631        painter.setRenderHints(painter.renderHints() | QPainter.Antialiasing | QPainter.TextAntialiasing)
1632        self.scene.clearSelection()
1633        self.scene.render(painter)
1634        painter.restore()
1635        del painter
1636        return image
1637
1638    def _canvasImageWrite(self, iw: QImageWriter, imgFormat: bytes, image: QImage):
1639        iw.setFormat(imgFormat)
1640        iw.setCompression(-1)
1641        if QT_VERSION >= 0x50500:
1642            iw.setOptimizedWrite(True)
1643        iw.write(image)
1644
1645    @pyqtSlot()
1646    def slot_canvasSaveImage(self):
1647        if self.fProjectFilename:
1648            dir = QFileInfo(self.fProjectFilename).absoluteDir().absolutePath()
1649        else:
1650            dir = self.fSavedSettings[CARLA_KEY_MAIN_PROJECT_FOLDER]
1651
1652        fileDialog = QFileDialog(self)
1653        fileDialog.setAcceptMode(QFileDialog.AcceptSave)
1654        fileDialog.setDirectory(dir)
1655        fileDialog.setFileMode(QFileDialog.AnyFile)
1656        fileDialog.setMimeTypeFilters(("image/png", "image/jpeg"))
1657        fileDialog.setNameFilter(self.tr("Images (*.png *.jpg)"))
1658        fileDialog.setOptions(QFileDialog.DontUseCustomDirectoryIcons)
1659        fileDialog.setWindowTitle(self.tr("Save Image"))
1660
1661        ok = fileDialog.exec_()
1662
1663        if not ok:
1664            return
1665
1666        newPath = fileDialog.selectedFiles()
1667
1668        if len(newPath) != 1:
1669            return
1670
1671        newPath = newPath[0]
1672
1673        if QT_VERSION >= 0x50900:
1674            if fileDialog.selectedMimeTypeFilter() == "image/jpeg":
1675                imgFormat = b"JPG"
1676            else:
1677                imgFormat = b"PNG"
1678        else:
1679            if newPath.lower().endswith((".jpg", ".jpeg")):
1680                imgFormat = b"JPG"
1681            else:
1682                imgFormat = b"PNG"
1683
1684        sender = self.sender()
1685        if sender == self.ui.act_canvas_save_image_2x:
1686            zoom = 2.0
1687        elif sender == self.ui.act_canvas_save_image_4x:
1688            zoom = 4.0
1689        else:
1690            zoom = 1.0
1691
1692        image = self._canvasImageRender(zoom)
1693        iw = QImageWriter(newPath)
1694        self._canvasImageWrite(iw, imgFormat, image)
1695
1696    @pyqtSlot()
1697    def slot_canvasCopyToClipboard(self):
1698        buffer = QBuffer()
1699        buffer.open(QIODevice.WriteOnly)
1700
1701        image = self._canvasImageRender()
1702        iw = QImageWriter(buffer, b"PNG")
1703        self._canvasImageWrite(iw, b"PNG", image)
1704
1705        buffer.close()
1706
1707        mimeData = QMimeData()
1708        mimeData.setData("image/png", buffer.buffer());
1709
1710        QApplication.clipboard().setMimeData(mimeData)
1711
1712    # --------------------------------------------------------------------------------------------------------
1713    # Canvas (canvas callbacks)
1714
1715    @pyqtSlot()
1716    def slot_canvasSelectionChanged(self):
1717        self.updateMiniCanvasLater()
1718
1719    @pyqtSlot(float)
1720    def slot_canvasScaleChanged(self, scale):
1721        self.ui.miniCanvasPreview.setViewScale(scale)
1722
1723    @pyqtSlot(list)
1724    def slot_canvasPluginSelected(self, pluginList):
1725        self.ui.keyboard.allNotesOff(False)
1726        self.ui.scrollArea.setEnabled(len(pluginList) != 0) # and self.fPluginCount > 0
1727        self.fSelectedPlugins = pluginList
1728
1729    # --------------------------------------------------------------------------------------------------------
1730    # Canvas (host callbacks)
1731
1732    @pyqtSlot(int, int, int, str)
1733    def slot_handlePatchbayClientAddedCallback(self, clientId, clientIcon, pluginId, clientName):
1734        pcSplit = patchcanvas.SPLIT_UNDEF
1735        pcIcon  = patchcanvas.ICON_APPLICATION
1736
1737        if clientIcon == PATCHBAY_ICON_PLUGIN:
1738            pcIcon = patchcanvas.ICON_PLUGIN
1739        if clientIcon == PATCHBAY_ICON_HARDWARE:
1740            pcIcon = patchcanvas.ICON_HARDWARE
1741        elif clientIcon == PATCHBAY_ICON_CARLA:
1742            pass
1743        elif clientIcon == PATCHBAY_ICON_DISTRHO:
1744            pcIcon = patchcanvas.ICON_DISTRHO
1745        elif clientIcon == PATCHBAY_ICON_FILE:
1746            pcIcon = patchcanvas.ICON_FILE
1747
1748        patchcanvas.addGroup(clientId, clientName, pcSplit, pcIcon)
1749
1750        self.updateMiniCanvasLater()
1751
1752        if pluginId < 0:
1753            return
1754        if pluginId >= self.fPluginCount and pluginId != MAIN_CARLA_PLUGIN_ID:
1755            print("Error mapping plugin to canvas client:", clientName)
1756            return
1757
1758        if pluginId == MAIN_CARLA_PLUGIN_ID:
1759            hasCustomUI = False
1760            hasInlineDisplay = False
1761        else:
1762            hints = self.host.get_plugin_info(pluginId)['hints']
1763            hasCustomUI = bool(hints & PLUGIN_HAS_CUSTOM_UI)
1764            hasInlineDisplay = bool(hints & PLUGIN_HAS_INLINE_DISPLAY)
1765
1766        patchcanvas.setGroupAsPlugin(clientId, pluginId, hasCustomUI, hasInlineDisplay)
1767
1768    @pyqtSlot(int)
1769    def slot_handlePatchbayClientRemovedCallback(self, clientId):
1770        patchcanvas.removeGroup(clientId)
1771        self.updateMiniCanvasLater()
1772
1773    @pyqtSlot(int, str)
1774    def slot_handlePatchbayClientRenamedCallback(self, clientId, newClientName):
1775        patchcanvas.renameGroup(clientId, newClientName)
1776        self.updateMiniCanvasLater()
1777
1778    @pyqtSlot(int, int, int)
1779    def slot_handlePatchbayClientDataChangedCallback(self, clientId, clientIcon, pluginId):
1780        pcIcon = patchcanvas.ICON_APPLICATION
1781
1782        if clientIcon == PATCHBAY_ICON_PLUGIN:
1783            pcIcon = patchcanvas.ICON_PLUGIN
1784        if clientIcon == PATCHBAY_ICON_HARDWARE:
1785            pcIcon = patchcanvas.ICON_HARDWARE
1786        elif clientIcon == PATCHBAY_ICON_CARLA:
1787            pass
1788        elif clientIcon == PATCHBAY_ICON_DISTRHO:
1789            pcIcon = patchcanvas.ICON_DISTRHO
1790        elif clientIcon == PATCHBAY_ICON_FILE:
1791            pcIcon = patchcanvas.ICON_FILE
1792
1793        patchcanvas.setGroupIcon(clientId, pcIcon)
1794        self.updateMiniCanvasLater()
1795
1796        if pluginId < 0:
1797            return
1798        if pluginId >= self.fPluginCount and pluginId != MAIN_CARLA_PLUGIN_ID:
1799            print("sorry, can't map this plugin to canvas client", pluginId, self.fPluginCount)
1800            return
1801
1802        if pluginId == MAIN_CARLA_PLUGIN_ID:
1803            hasCustomUI = False
1804            hasInlineDisplay = False
1805        else:
1806            hints = self.host.get_plugin_info(pluginId)['hints']
1807            hasCustomUI = bool(hints & PLUGIN_HAS_CUSTOM_UI)
1808            hasInlineDisplay = bool(hints & PLUGIN_HAS_INLINE_DISPLAY)
1809
1810        patchcanvas.setGroupAsPlugin(clientId, pluginId, hasCustomUI, hasInlineDisplay)
1811
1812    @pyqtSlot(int, int, int, int, int)
1813    def slot_handlePatchbayClientPositionChangedCallback(self, clientId, x1, y1, x2, y2):
1814        if (x1 != 0 and x2 != 0) or (y1 != 0 and y2 != 0):
1815            patchcanvas.splitGroup(clientId)
1816        else:
1817            patchcanvas.joinGroup(clientId)
1818        patchcanvas.setGroupPosFull(clientId, x1, y1, x2, y2)
1819        self.updateMiniCanvasLater()
1820
1821    @pyqtSlot(int, int, int, int, str)
1822    def slot_handlePatchbayPortAddedCallback(self, clientId, portId, portFlags, portGroupId, portName):
1823        if portFlags & PATCHBAY_PORT_IS_INPUT:
1824            portMode = patchcanvas.PORT_MODE_INPUT
1825        else:
1826            portMode = patchcanvas.PORT_MODE_OUTPUT
1827
1828        if portFlags & PATCHBAY_PORT_TYPE_AUDIO:
1829            portType    = patchcanvas.PORT_TYPE_AUDIO_JACK
1830            isAlternate = False
1831        elif portFlags & PATCHBAY_PORT_TYPE_CV:
1832            portType    = patchcanvas.PORT_TYPE_PARAMETER
1833            isAlternate = False
1834        elif portFlags & PATCHBAY_PORT_TYPE_MIDI:
1835            portType    = patchcanvas.PORT_TYPE_MIDI_JACK
1836            isAlternate = False
1837        else:
1838            portType    = patchcanvas.PORT_TYPE_NULL
1839            isAlternate = False
1840
1841        patchcanvas.addPort(clientId, portId, portName, portMode, portType, isAlternate)
1842        self.updateMiniCanvasLater()
1843
1844    @pyqtSlot(int, int)
1845    def slot_handlePatchbayPortRemovedCallback(self, groupId, portId):
1846        patchcanvas.removePort(groupId, portId)
1847        self.updateMiniCanvasLater()
1848
1849    @pyqtSlot(int, int, int, int, str)
1850    def slot_handlePatchbayPortChangedCallback(self, groupId, portId, portFlags, portGroupId, newPortName):
1851        patchcanvas.renamePort(groupId, portId, newPortName)
1852        self.updateMiniCanvasLater()
1853
1854    @pyqtSlot(int, int, int, str)
1855    def slot_handlePatchbayPortGroupAddedCallback(self, groupId, portId, portGroupId, newPortName):
1856        # TODO
1857        pass
1858
1859    @pyqtSlot(int, int)
1860    def slot_handlePatchbayPortGroupRemovedCallback(self, groupId, portId):
1861        # TODO
1862        pass
1863
1864    @pyqtSlot(int, int, int, str)
1865    def slot_handlePatchbayPortGroupChangedCallback(self, groupId, portId, portGroupId, newPortName):
1866        # TODO
1867        pass
1868
1869    @pyqtSlot(int, int, int, int, int)
1870    def slot_handlePatchbayConnectionAddedCallback(self, connectionId, groupOutId, portOutId, groupInId, portInId):
1871        patchcanvas.connectPorts(connectionId, groupOutId, portOutId, groupInId, portInId)
1872        self.updateMiniCanvasLater()
1873
1874    @pyqtSlot(int, int, int)
1875    def slot_handlePatchbayConnectionRemovedCallback(self, connectionId, portOutId, portInId):
1876        patchcanvas.disconnectPorts(connectionId)
1877        self.updateMiniCanvasLater()
1878
1879    # --------------------------------------------------------------------------------------------------------
1880    # Settings
1881
1882    def saveSettings(self):
1883        settings = QSafeSettings()
1884
1885        settings.setValue("Geometry", self.saveGeometry())
1886        settings.setValue("ShowToolbar", self.ui.toolBar.isEnabled())
1887        settings.setValue("ShowSidePanel", self.ui.dockWidget.isEnabled())
1888
1889        diskFolders = []
1890
1891        for i in range(self.ui.cb_disk.count()):
1892            diskFolders.append(self.ui.cb_disk.itemData(i))
1893
1894        settings.setValue("DiskFolders", diskFolders)
1895        settings.setValue("LastBPM", self.fLastTransportBPM)
1896
1897        settings.setValue("ShowMeters", self.ui.act_settings_show_meters.isChecked())
1898        settings.setValue("ShowKeyboard", self.ui.act_settings_show_keyboard.isChecked())
1899        settings.setValue("HorizontalScrollBarValue", self.ui.graphicsView.horizontalScrollBar().value())
1900        settings.setValue("VerticalScrollBarValue", self.ui.graphicsView.verticalScrollBar().value())
1901
1902        settings.setValue(CARLA_KEY_ENGINE_TRANSPORT_MODE,  self.host.transportMode)
1903        settings.setValue(CARLA_KEY_ENGINE_TRANSPORT_EXTRA, self.host.transportExtra)
1904
1905        return settings
1906
1907    def loadSettings(self, firstTime):
1908        settings = QSafeSettings()
1909
1910        if firstTime:
1911            geometry = settings.value("Geometry", QByteArray(), QByteArray)
1912            if not geometry.isNull():
1913                self.restoreGeometry(geometry)
1914
1915            showToolbar = settings.value("ShowToolbar", True, bool)
1916            self.ui.act_settings_show_toolbar.setChecked(showToolbar)
1917            self.ui.toolBar.setEnabled(showToolbar)
1918            self.ui.toolBar.setVisible(showToolbar)
1919
1920            #if settings.contains("SplitterState"):
1921                #self.ui.splitter.restoreState(settings.value("SplitterState", b""))
1922            #else:
1923                #self.ui.splitter.setSizes([210, 99999])
1924
1925            showSidePanel = settings.value("ShowSidePanel", True, bool)
1926            self.ui.act_settings_show_side_panel.setChecked(showSidePanel)
1927            self.slot_showSidePanel(showSidePanel)
1928
1929            diskFolders = settings.value("DiskFolders", [HOME], list)
1930
1931            self.ui.cb_disk.setItemData(0, HOME)
1932
1933            for i in range(len(diskFolders)):
1934                if i == 0: continue
1935                folder = diskFolders[i]
1936                self.ui.cb_disk.addItem(os.path.basename(folder), folder)
1937
1938            #if MACOS and not settings.value(CARLA_KEY_MAIN_USE_PRO_THEME, True, bool):
1939            #    self.setUnifiedTitleAndToolBarOnMac(True)
1940
1941            showMeters = settings.value("ShowMeters", True, bool)
1942            self.ui.act_settings_show_meters.setChecked(showMeters)
1943            self.ui.peak_in.setVisible(showMeters)
1944            self.ui.peak_out.setVisible(showMeters)
1945
1946            showKeyboard = settings.value("ShowKeyboard", True, bool)
1947            self.ui.act_settings_show_keyboard.setChecked(showKeyboard)
1948            self.ui.scrollArea.setVisible(showKeyboard)
1949
1950            settingsDBf = QSafeSettings("falkTX", "CarlaDatabase2")
1951            self.fFavoritePlugins = settingsDBf.value("PluginDatabase/Favorites", [], list)
1952
1953            QTimer.singleShot(100, self.slot_restoreCanvasScrollbarValues)
1954
1955        # TODO - complete this
1956        oldSettings = self.fSavedSettings
1957
1958        self.fSavedSettings = {
1959            CARLA_KEY_MAIN_PROJECT_FOLDER:      settings.value(CARLA_KEY_MAIN_PROJECT_FOLDER,      CARLA_DEFAULT_MAIN_PROJECT_FOLDER,      str),
1960            CARLA_KEY_MAIN_CONFIRM_EXIT:        settings.value(CARLA_KEY_MAIN_CONFIRM_EXIT,        CARLA_DEFAULT_MAIN_CONFIRM_EXIT,        bool),
1961            CARLA_KEY_MAIN_CLASSIC_SKIN:        settings.value(CARLA_KEY_MAIN_CLASSIC_SKIN,        CARLA_DEFAULT_MAIN_CLASSIC_SKIN,        bool),
1962            CARLA_KEY_MAIN_REFRESH_INTERVAL:    settings.value(CARLA_KEY_MAIN_REFRESH_INTERVAL,    CARLA_DEFAULT_MAIN_REFRESH_INTERVAL,    int),
1963            CARLA_KEY_MAIN_SYSTEM_ICONS:        settings.value(CARLA_KEY_MAIN_SYSTEM_ICONS,        CARLA_DEFAULT_MAIN_SYSTEM_ICONS,        bool),
1964            CARLA_KEY_MAIN_EXPERIMENTAL:        settings.value(CARLA_KEY_MAIN_EXPERIMENTAL,        CARLA_DEFAULT_MAIN_EXPERIMENTAL,        bool),
1965            CARLA_KEY_CANVAS_THEME:             settings.value(CARLA_KEY_CANVAS_THEME,             CARLA_DEFAULT_CANVAS_THEME,             str),
1966            CARLA_KEY_CANVAS_SIZE:              settings.value(CARLA_KEY_CANVAS_SIZE,              CARLA_DEFAULT_CANVAS_SIZE,              str),
1967            CARLA_KEY_CANVAS_AUTO_HIDE_GROUPS:  settings.value(CARLA_KEY_CANVAS_AUTO_HIDE_GROUPS,  CARLA_DEFAULT_CANVAS_AUTO_HIDE_GROUPS,  bool),
1968            CARLA_KEY_CANVAS_AUTO_SELECT_ITEMS: settings.value(CARLA_KEY_CANVAS_AUTO_SELECT_ITEMS, CARLA_DEFAULT_CANVAS_AUTO_SELECT_ITEMS, bool),
1969            CARLA_KEY_CANVAS_USE_BEZIER_LINES:  settings.value(CARLA_KEY_CANVAS_USE_BEZIER_LINES,  CARLA_DEFAULT_CANVAS_USE_BEZIER_LINES,  bool),
1970            CARLA_KEY_CANVAS_EYE_CANDY:         settings.value(CARLA_KEY_CANVAS_EYE_CANDY,         CARLA_DEFAULT_CANVAS_EYE_CANDY,         bool),
1971            CARLA_KEY_CANVAS_FANCY_EYE_CANDY:   settings.value(CARLA_KEY_CANVAS_FANCY_EYE_CANDY,   CARLA_DEFAULT_CANVAS_FANCY_EYE_CANDY,   bool),
1972            CARLA_KEY_CANVAS_USE_OPENGL:        settings.value(CARLA_KEY_CANVAS_USE_OPENGL,        CARLA_DEFAULT_CANVAS_USE_OPENGL,        bool),
1973            CARLA_KEY_CANVAS_ANTIALIASING:      settings.value(CARLA_KEY_CANVAS_ANTIALIASING,      CARLA_DEFAULT_CANVAS_ANTIALIASING,      int),
1974            CARLA_KEY_CANVAS_HQ_ANTIALIASING:   settings.value(CARLA_KEY_CANVAS_HQ_ANTIALIASING,   CARLA_DEFAULT_CANVAS_HQ_ANTIALIASING,   bool),
1975            CARLA_KEY_CANVAS_FULL_REPAINTS:     settings.value(CARLA_KEY_CANVAS_FULL_REPAINTS,     CARLA_DEFAULT_CANVAS_FULL_REPAINTS,     bool),
1976            CARLA_KEY_CUSTOM_PAINTING:         (settings.value(CARLA_KEY_MAIN_USE_PRO_THEME,    True,   bool) and
1977                                                settings.value(CARLA_KEY_MAIN_PRO_THEME_COLOR, "Black", str).lower() == "black"),
1978        }
1979
1980        if not self.host.isControl:
1981            self.fSavedSettings[CARLA_KEY_CANVAS_INLINE_DISPLAYS] = settings.value(CARLA_KEY_CANVAS_INLINE_DISPLAYS, CARLA_DEFAULT_CANVAS_INLINE_DISPLAYS, bool)
1982        else:
1983            self.fSavedSettings[CARLA_KEY_CANVAS_INLINE_DISPLAYS] = False
1984
1985        settings2 = QSafeSettings("falkTX", "Carla2")
1986
1987        if self.host.experimental:
1988            visible = settings2.value(CARLA_KEY_EXPERIMENTAL_JACK_APPS, CARLA_DEFAULT_EXPERIMENTAL_JACK_APPS, bool)
1989            self.ui.act_plugin_add_jack.setVisible(visible)
1990        else:
1991            self.ui.act_plugin_add_jack.setVisible(False)
1992
1993        self.fMiniCanvasUpdateTimeout = 1000 if self.fSavedSettings[CARLA_KEY_CANVAS_FANCY_EYE_CANDY] else 0
1994
1995        setEngineSettings(self.host)
1996        self.restartTimersIfNeeded()
1997
1998        if oldSettings.get(CARLA_KEY_MAIN_CLASSIC_SKIN, None) not in (self.fSavedSettings[CARLA_KEY_MAIN_CLASSIC_SKIN], None):
1999            newSkin = "classic" if self.fSavedSettings[CARLA_KEY_MAIN_CLASSIC_SKIN] else None
2000
2001            for pitem in self.fPluginList:
2002                if pitem is None:
2003                    continue
2004                pitem.recreateWidget(newSkin = newSkin)
2005
2006        return settings
2007
2008    # --------------------------------------------------------------------------------------------------------
2009    # Settings (helpers)
2010
2011    def enableTransport(self, enabled):
2012        self.ui.group_transport_controls.setEnabled(enabled)
2013        self.ui.group_transport_settings.setEnabled(enabled)
2014
2015    @pyqtSlot()
2016    def slot_restoreCanvasScrollbarValues(self):
2017        settings = QSafeSettings()
2018        horiz = settings.value("HorizontalScrollBarValue", int(self.ui.graphicsView.horizontalScrollBar().maximum()/2), int)
2019        vertc = settings.value("VerticalScrollBarValue", int(self.ui.graphicsView.verticalScrollBar().maximum()/2), int)
2020        self.ui.graphicsView.horizontalScrollBar().setValue(horiz)
2021        self.ui.graphicsView.verticalScrollBar().setValue(vertc)
2022
2023    # --------------------------------------------------------------------------------------------------------
2024    # Settings (menu actions)
2025
2026    @pyqtSlot(bool)
2027    def slot_showSidePanel(self, yesNo):
2028        self.ui.dockWidget.setEnabled(yesNo)
2029        self.ui.dockWidget.setVisible(yesNo)
2030
2031    @pyqtSlot(bool)
2032    def slot_showToolbar(self, yesNo):
2033        self.ui.toolBar.setEnabled(yesNo)
2034        self.ui.toolBar.setVisible(yesNo)
2035
2036    @pyqtSlot(bool)
2037    def slot_showCanvasMeters(self, yesNo):
2038        self.ui.peak_in.setVisible(yesNo)
2039        self.ui.peak_out.setVisible(yesNo)
2040        QTimer.singleShot(0, self.slot_miniCanvasCheckAll)
2041
2042    @pyqtSlot(bool)
2043    def slot_showCanvasKeyboard(self, yesNo):
2044        self.ui.scrollArea.setVisible(yesNo)
2045        QTimer.singleShot(0, self.slot_miniCanvasCheckAll)
2046
2047    @pyqtSlot()
2048    def slot_configureCarla(self):
2049        dialog = CarlaSettingsW(self.fParentOrSelf, self.host, True, hasGL)
2050        if not dialog.exec_():
2051            return
2052
2053        self.loadSettings(False)
2054
2055        if self.fWithCanvas:
2056            patchcanvas.clear()
2057            self.setupCanvas()
2058            self.slot_miniCanvasCheckAll()
2059
2060        if self.host.processMode == ENGINE_PROCESS_MODE_CONTINUOUS_RACK and self.host.isPlugin:
2061            pass
2062        elif self.host.is_engine_running():
2063            self.host.patchbay_refresh(self.fExternalPatchbay)
2064
2065    # --------------------------------------------------------------------------------------------------------
2066    # About (menu actions)
2067
2068    @pyqtSlot()
2069    def slot_aboutCarla(self):
2070        CarlaAboutW(self.fParentOrSelf, self.host).exec_()
2071
2072    @pyqtSlot()
2073    def slot_aboutJuce(self):
2074        JuceAboutW(self.fParentOrSelf).exec_()
2075
2076    @pyqtSlot()
2077    def slot_aboutQt(self):
2078        QApplication.instance().aboutQt()
2079
2080    # --------------------------------------------------------------------------------------------------------
2081    # Disk (menu actions)
2082
2083    @pyqtSlot(int)
2084    def slot_diskFolderChanged(self, index):
2085        if index < 0:
2086            return
2087        elif index == 0:
2088            filename = HOME
2089            self.ui.b_disk_remove.setEnabled(False)
2090        else:
2091            filename = self.ui.cb_disk.itemData(index)
2092            self.ui.b_disk_remove.setEnabled(True)
2093
2094        self.fDirModel.setRootPath(filename)
2095        self.ui.fileTreeView.setRootIndex(self.fDirModel.index(filename))
2096
2097    @pyqtSlot()
2098    def slot_diskFolderAdd(self):
2099        newPath = QFileDialog.getExistingDirectory(self, self.tr("New Folder"), "", QFileDialog.ShowDirsOnly)
2100
2101        if newPath:
2102            if newPath[-1] == os.sep:
2103                newPath = newPath[:-1]
2104            self.ui.cb_disk.addItem(os.path.basename(newPath), newPath)
2105            self.ui.cb_disk.setCurrentIndex(self.ui.cb_disk.count()-1)
2106            self.ui.b_disk_remove.setEnabled(True)
2107
2108    @pyqtSlot()
2109    def slot_diskFolderRemove(self):
2110        index = self.ui.cb_disk.currentIndex()
2111
2112        if index <= 0:
2113            return
2114
2115        self.ui.cb_disk.removeItem(index)
2116
2117        if self.ui.cb_disk.currentIndex() == 0:
2118            self.ui.b_disk_remove.setEnabled(False)
2119
2120    @pyqtSlot(QModelIndex)
2121    def slot_fileTreeDoubleClicked(self, modelIndex):
2122        filename = self.fDirModel.filePath(modelIndex)
2123
2124        if not self.ui.listWidget.isDragUrlValid(filename):
2125            return
2126
2127        if not self.host.load_file(filename):
2128            CustomMessageBox(self, QMessageBox.Critical, self.tr("Error"),
2129                             self.tr("Failed to load file"),
2130                             self.host.get_last_error(), QMessageBox.Ok, QMessageBox.Ok)
2131            return
2132
2133        if filename.endswith(".carxp"):
2134            self.loadExternalCanvasGroupPositionsIfNeeded(filename)
2135
2136    # --------------------------------------------------------------------------------------------------------
2137    # Transport
2138
2139    def refreshTransport(self, forced = False):
2140        if not self.ui.l_transport_time.isVisible():
2141            return
2142        if self.fSampleRate == 0.0 or not self.host.is_engine_running():
2143            return
2144
2145        timeInfo = self.host.get_transport_info()
2146        playing  = timeInfo['playing']
2147        frame    = timeInfo['frame']
2148        bpm      = timeInfo['bpm']
2149
2150        if playing != self.fLastTransportState or forced:
2151            if playing:
2152                if self.fSavedSettings[CARLA_KEY_MAIN_SYSTEM_ICONS]:
2153                    icon = getIcon('media-playback-pause', 16, 'svgz')
2154                else:
2155                    icon = QIcon(":/16x16/media-playback-pause.svgz")
2156                self.ui.b_transport_play.setChecked(True)
2157                self.ui.b_transport_play.setIcon(icon)
2158                #self.ui.b_transport_play.setText(self.tr("&Pause"))
2159            else:
2160                if self.fSavedSettings[CARLA_KEY_MAIN_SYSTEM_ICONS]:
2161                    icon = getIcon('media-playback-start', 16, 'svgz')
2162                else:
2163                    icon = QIcon(":/16x16/media-playback-start.svgz")
2164                self.ui.b_transport_play.setChecked(False)
2165                self.ui.b_transport_play.setIcon(icon)
2166                #self.ui.b_play.setText(self.tr("&Play"))
2167
2168            self.fLastTransportState = playing
2169
2170        if frame != self.fLastTransportFrame or forced:
2171            self.fLastTransportFrame = frame
2172
2173            time = frame / self.fSampleRate
2174            secs =  time % 60
2175            mins = (time / 60) % 60
2176            hrs  = (time / 3600) % 60
2177            self.ui.l_transport_time.setText("%02i:%02i:%02i" % (hrs, mins, secs))
2178
2179            frame1 =  frame % 1000
2180            frame2 = (frame / 1000) % 1000
2181            frame3 = (frame / 1000000) % 1000
2182            self.ui.l_transport_frame.setText("%03i'%03i'%03i" % (frame3, frame2, frame1))
2183
2184            bar  = timeInfo['bar']
2185            beat = timeInfo['beat']
2186            tick = timeInfo['tick']
2187            self.ui.l_transport_bbt.setText("%03i|%02i|%04i" % (bar, beat, tick))
2188
2189        if bpm != self.fLastTransportBPM or forced:
2190            self.fLastTransportBPM = bpm
2191
2192            if bpm > 0.0:
2193                self.ui.dsb_transport_bpm.blockSignals(True)
2194                self.ui.dsb_transport_bpm.setValue(bpm)
2195                self.ui.dsb_transport_bpm.blockSignals(False)
2196                self.ui.dsb_transport_bpm.setStyleSheet("")
2197            else:
2198                self.ui.dsb_transport_bpm.setStyleSheet("QDoubleSpinBox { color: palette(mid); }")
2199
2200    # --------------------------------------------------------------------------------------------------------
2201    # Transport (menu actions)
2202
2203    @pyqtSlot(bool)
2204    def slot_transportPlayPause(self, toggled):
2205        if self.host.isPlugin or not self.host.is_engine_running():
2206            return
2207
2208        if toggled:
2209            self.host.transport_play()
2210        else:
2211            self.host.transport_pause()
2212
2213        self.refreshTransport()
2214
2215    @pyqtSlot()
2216    def slot_transportStop(self):
2217        if self.host.isPlugin or not self.host.is_engine_running():
2218            return
2219
2220        self.host.transport_pause()
2221        self.host.transport_relocate(0)
2222
2223        self.refreshTransport()
2224
2225    @pyqtSlot()
2226    def slot_transportBackwards(self):
2227        if self.host.isPlugin or not self.host.is_engine_running():
2228            return
2229
2230        newFrame = self.host.get_current_transport_frame() - 100000
2231
2232        if newFrame < 0:
2233            newFrame = 0
2234
2235        self.host.transport_relocate(newFrame)
2236
2237    @pyqtSlot(float)
2238    def slot_transportBpmChanged(self, newValue):
2239        self.host.transport_bpm(newValue)
2240
2241    @pyqtSlot()
2242    def slot_transportForwards(self):
2243        if self.fSampleRate == 0.0 or self.host.isPlugin or not self.host.is_engine_running():
2244            return
2245
2246        newFrame = self.host.get_current_transport_frame() + int(self.fSampleRate*2.5)
2247        self.host.transport_relocate(newFrame)
2248
2249    @pyqtSlot(bool)
2250    def slot_transportJackEnabled(self, clicked):
2251        if not self.host.is_engine_running():
2252            return
2253        self.host.transportMode = ENGINE_TRANSPORT_MODE_JACK if clicked else ENGINE_TRANSPORT_MODE_INTERNAL
2254        self.host.set_engine_option(ENGINE_OPTION_TRANSPORT_MODE,
2255                                    self.host.transportMode,
2256                                    self.host.transportExtra)
2257
2258    @pyqtSlot(bool)
2259    def slot_transportLinkEnabled(self, clicked):
2260        if not self.host.is_engine_running():
2261            return
2262        extra = ":link:" if clicked else ""
2263        self.host.transportExtra = extra
2264        self.host.set_engine_option(ENGINE_OPTION_TRANSPORT_MODE,
2265                                    self.host.transportMode,
2266                                    self.host.transportExtra)
2267
2268    # --------------------------------------------------------------------------------------------------------
2269    # Other
2270
2271    @pyqtSlot(bool)
2272    def slot_xrunClear(self):
2273        self.host.clear_engine_xruns()
2274
2275    # --------------------------------------------------------------------------------------------------------
2276    # Canvas scrollbars
2277
2278    @pyqtSlot(int)
2279    def slot_horizontalScrollBarChanged(self, value):
2280        maximum = self.ui.graphicsView.horizontalScrollBar().maximum()
2281        if maximum == 0:
2282            xp = 0
2283        else:
2284            xp = float(value) / maximum
2285        self.ui.miniCanvasPreview.setViewPosX(xp)
2286        self.updateCanvasInitialPos()
2287
2288    @pyqtSlot(int)
2289    def slot_verticalScrollBarChanged(self, value):
2290        maximum = self.ui.graphicsView.verticalScrollBar().maximum()
2291        if maximum == 0:
2292            yp = 0
2293        else:
2294            yp = float(value) / maximum
2295        self.ui.miniCanvasPreview.setViewPosY(yp)
2296        self.updateCanvasInitialPos()
2297
2298    # --------------------------------------------------------------------------------------------------------
2299    # Canvas keyboard
2300
2301    @pyqtSlot(int)
2302    def slot_noteOn(self, note):
2303        if self.fPluginCount == 0:
2304            return
2305
2306        for pluginId in self.fSelectedPlugins:
2307            self.host.send_midi_note(pluginId, 0, note, 100)
2308
2309            pedit = self.getPluginEditDialog(pluginId)
2310            pedit.noteOn(0, note, 100)
2311
2312    @pyqtSlot(int)
2313    def slot_noteOff(self, note):
2314        if self.fPluginCount == 0:
2315            return
2316
2317        for pluginId in self.fSelectedPlugins:
2318            self.host.send_midi_note(pluginId, 0, note, 0)
2319
2320            pedit = self.getPluginEditDialog(pluginId)
2321            pedit.noteOff(0, note)
2322
2323    # --------------------------------------------------------------------------------------------------------
2324    # Canvas keyboard (host callbacks)
2325
2326    @pyqtSlot(int, int, int, int)
2327    def slot_handleNoteOnCallback(self, pluginId, channel, note, velocity):
2328        if pluginId in self.fSelectedPlugins:
2329            self.ui.keyboard.sendNoteOn(note, False)
2330
2331    @pyqtSlot(int, int, int)
2332    def slot_handleNoteOffCallback(self, pluginId, channel, note):
2333        if pluginId in self.fSelectedPlugins:
2334            self.ui.keyboard.sendNoteOff(note, False)
2335
2336    # --------------------------------------------------------------------------------------------------------
2337
2338    @pyqtSlot(int)
2339    def slot_handleUpdateCallback(self, pluginId):
2340        pitem = self.getPluginItem(pluginId)
2341
2342        if pitem is None:
2343            return
2344
2345        wasCompacted = pitem.isCompacted()
2346        isCompacted  = wasCompacted
2347
2348        check = self.host.get_custom_data_value(pluginId, CUSTOM_DATA_TYPE_PROPERTY, "CarlaSkinIsCompacted")
2349        if not check:
2350            return
2351        isCompacted = bool(check == "true")
2352
2353        if wasCompacted == isCompacted:
2354            return
2355
2356        pitem.recreateWidget(True)
2357
2358    # --------------------------------------------------------------------------------------------------------
2359    # MiniCanvas stuff
2360
2361    @pyqtSlot()
2362    def slot_miniCanvasCheckAll(self):
2363        self.slot_miniCanvasCheckSize()
2364        self.slot_horizontalScrollBarChanged(self.ui.graphicsView.horizontalScrollBar().value())
2365        self.slot_verticalScrollBarChanged(self.ui.graphicsView.verticalScrollBar().value())
2366
2367    @pyqtSlot()
2368    def slot_miniCanvasCheckSize(self):
2369        if self.fCanvasWidth == 0 or self.fCanvasHeight == 0:
2370            return
2371
2372        currentIndex = self.ui.tabWidget.currentIndex()
2373
2374        if currentIndex == 1:
2375            width  = self.ui.graphicsView.width()
2376            height = self.ui.graphicsView.height()
2377        else:
2378            self.ui.tabWidget.blockSignals(True)
2379            self.ui.tabWidget.setCurrentIndex(1)
2380            width  = self.ui.graphicsView.width()
2381            height = self.ui.graphicsView.height()
2382            self.ui.tabWidget.setCurrentIndex(currentIndex)
2383            self.ui.tabWidget.blockSignals(False)
2384
2385        self.scene.updateLimits()
2386
2387        self.ui.miniCanvasPreview.setViewSize(float(width)/self.fCanvasWidth, float(height)/self.fCanvasHeight)
2388
2389    @pyqtSlot(float, float)
2390    def slot_miniCanvasMoved(self, xp, yp):
2391        hsb = self.ui.graphicsView.horizontalScrollBar()
2392        vsb = self.ui.graphicsView.verticalScrollBar()
2393        hsb.setValue(xp * hsb.maximum())
2394        vsb.setValue(yp * vsb.maximum())
2395        self.updateCanvasInitialPos()
2396
2397    # --------------------------------------------------------------------------------------------------------
2398    # Logs autoscroll, save and clear
2399
2400    @pyqtSlot(int)
2401    def slot_toggleLogAutoscroll(self, checkState):
2402        self.autoscrollOnNewLog = checkState == Qt.Checked
2403        if self.autoscrollOnNewLog:
2404            self.ui.text_logs.verticalScrollBar().setValue(self.ui.text_logs.verticalScrollBar().maximum())
2405
2406    @pyqtSlot(int)
2407    def slot_logSliderMoved(self, slider_pos):
2408        if self.ui.text_logs.verticalScrollBar().hasTracking() or self.autoscrollOnNewLog:
2409            self.lastLogSliderPos = slider_pos
2410        else:
2411            self.ui.text_logs.verticalScrollBar().setValue(self.lastLogSliderPos)
2412
2413    @pyqtSlot()
2414    def slot_logButtonsState(self, enabled=True):
2415        self.ui.logs_clear.setEnabled(enabled)
2416        self.ui.logs_save.setEnabled(enabled)
2417
2418    @pyqtSlot()
2419    def slot_logSave(self):
2420        filename = os.path.join(self.fSavedSettings[CARLA_KEY_MAIN_PROJECT_FOLDER], 'carla_log.txt')
2421        filename, _ = QFileDialog.getSaveFileName(self, self.tr("Save Logs"), filename)
2422
2423        if not filename:
2424            return
2425
2426        try:
2427            with open(filename, "w") as logfile:
2428                logfile.write(self.ui.text_logs.toPlainText())
2429                logfile.close()
2430        except:
2431            return
2432
2433    @pyqtSlot()
2434    def slot_logClear(self):
2435        self.ui.text_logs.clear()
2436        self.ui.text_logs.appendPlainText("======= Logs cleared ========")
2437        self.slot_logButtonsState(False)
2438
2439    # --------------------------------------------------------------------------------------------------------
2440    # Timers
2441
2442    def startTimers(self):
2443        if self.fIdleTimerFast == 0:
2444            self.fIdleTimerFast = self.startTimer(self.fSavedSettings[CARLA_KEY_MAIN_REFRESH_INTERVAL])
2445
2446        if self.fIdleTimerSlow == 0:
2447            self.fIdleTimerSlow = self.startTimer(self.fSavedSettings[CARLA_KEY_MAIN_REFRESH_INTERVAL]*4)
2448
2449    def restartTimersIfNeeded(self):
2450        if self.fIdleTimerFast != 0:
2451            self.killTimer(self.fIdleTimerFast)
2452            self.fIdleTimerFast = self.startTimer(self.fSavedSettings[CARLA_KEY_MAIN_REFRESH_INTERVAL])
2453
2454        if self.fIdleTimerSlow != 0:
2455            self.killTimer(self.fIdleTimerSlow)
2456            self.fIdleTimerSlow = self.startTimer(self.fSavedSettings[CARLA_KEY_MAIN_REFRESH_INTERVAL]*4)
2457
2458    def killTimers(self):
2459        if self.fIdleTimerFast != 0:
2460            self.killTimer(self.fIdleTimerFast)
2461            self.fIdleTimerFast = 0
2462
2463        if self.fIdleTimerSlow != 0:
2464            self.killTimer(self.fIdleTimerSlow)
2465            self.fIdleTimerSlow = 0
2466
2467    # --------------------------------------------------------------------------------------------------------
2468    # Misc
2469
2470    @pyqtSlot(int)
2471    def slot_tabChanged(self, index):
2472        if index != 1:
2473            return
2474
2475        self.ui.graphicsView.setFocus()
2476
2477    @pyqtSlot(int)
2478    def slot_handleReloadAllCallback(self, pluginId):
2479        if pluginId >= self.fPluginCount:
2480            return
2481
2482        pitem = self.fPluginList[pluginId]
2483        if pitem is None:
2484            return
2485
2486        pitem.recreateWidget()
2487
2488    # --------------------------------------------------------------------------------------------------------
2489
2490    @pyqtSlot(int, int, str)
2491    def slot_handleNSMCallback(self, opcode, valueInt, valueStr):
2492        if opcode == NSM_CALLBACK_INIT:
2493            return
2494
2495        # Error
2496        elif opcode == NSM_CALLBACK_ERROR:
2497            pass
2498
2499        # Reply
2500        elif opcode == NSM_CALLBACK_ANNOUNCE:
2501            self.fFirstEngineInit    = False
2502            self.fSessionManagerName = valueStr
2503            self.setProperWindowTitle()
2504
2505            # If NSM server does not support optional-gui, revert our initial assumptions that it does
2506            if (valueInt & (1 << 1)) == 0x0:
2507                self.fWindowCloseHideGui = False
2508                self.ui.act_file_quit.setText(self.tr("&Quit"))
2509                QApplication.instance().setQuitOnLastWindowClosed(True)
2510                self.show()
2511
2512        # Open
2513        elif opcode == NSM_CALLBACK_OPEN:
2514            self.fProjectFilename = QFileInfo(valueStr+".carxp").absoluteFilePath()
2515            self.setProperWindowTitle()
2516
2517            self.fCustomStopAction = self.CUSTOM_ACTION_PROJECT_LOAD
2518            self.slot_engineStop(True)
2519            return
2520
2521        # Save
2522        elif opcode == NSM_CALLBACK_SAVE:
2523            self.saveProjectNow()
2524
2525        # Session is Loaded
2526        elif opcode == NSM_CALLBACK_SESSION_IS_LOADED:
2527            pass
2528
2529        # Show Optional Gui
2530        elif opcode == NSM_CALLBACK_SHOW_OPTIONAL_GUI:
2531            self.show()
2532
2533        # Hide Optional Gui
2534        elif opcode == NSM_CALLBACK_HIDE_OPTIONAL_GUI:
2535            self.hideForNSM()
2536
2537        # Set client name
2538        elif opcode == NSM_CALLBACK_SET_CLIENT_NAME_ID:
2539            self.fClientName = valueStr
2540            return
2541
2542        self.host.nsm_ready(opcode)
2543
2544    # --------------------------------------------------------------------------------------------------------
2545
2546    def fixLogText(self, text):
2547        return text.replace("\x1b[30;1m", "").replace("\x1b[31m", "").replace("\x1b[0m", "")
2548
2549    @pyqtSlot(int, int, int, int, float, str)
2550    def slot_handleDebugCallback(self, pluginId, value1, value2, value3, valuef, valueStr):
2551        self.ui.text_logs.appendPlainText(self.fixLogText(valueStr))
2552
2553    @pyqtSlot(str)
2554    def slot_handleInfoCallback(self, info):
2555        QMessageBox.information(self, "Information", info)
2556
2557    @pyqtSlot(str)
2558    def slot_handleErrorCallback(self, error):
2559        QMessageBox.critical(self, "Error", error)
2560
2561    @pyqtSlot()
2562    def slot_handleQuitCallback(self):
2563        self.fIsProjectLoading = False
2564        self.killTimers()
2565        self.removeAllPlugins()
2566        self.projectLoadingFinished(False)
2567
2568    @pyqtSlot(int)
2569    def slot_handleInlineDisplayRedrawCallback(self, pluginId):
2570        # FIXME
2571        if self.fIdleTimerSlow != 0 and self.fIdleTimerFast != 0 and pluginId < self.fPluginCount and not self.fIsProjectLoading:
2572            patchcanvas.redrawPluginGroup(pluginId)
2573
2574    # --------------------------------------------------------------------------------------------------------
2575
2576    @pyqtSlot()
2577    def slot_handleSIGUSR1(self):
2578        print("Got SIGUSR1 -> Saving project now")
2579        self.slot_fileSave()
2580
2581    @pyqtSlot()
2582    def slot_handleSIGTERM(self):
2583        print("Got SIGTERM -> Closing now")
2584        self.fCustomStopAction = self.CUSTOM_ACTION_APP_CLOSE
2585        self.close()
2586
2587    # --------------------------------------------------------------------------------------------------------
2588    # Internal stuff
2589
2590    def getExtraPtr(self, plugin):
2591        ptype = plugin['type']
2592
2593        if ptype == PLUGIN_LADSPA:
2594            uniqueId = plugin['uniqueId']
2595
2596            self.maybeLoadRDFs()
2597
2598            for rdfItem in self.fLadspaRdfList:
2599                if rdfItem.UniqueID == uniqueId:
2600                    return pointer(rdfItem)
2601
2602        elif ptype == PLUGIN_SF2:
2603            if plugin['name'].lower().endswith(" (16 outputs)"):
2604                return self._true
2605
2606        return None
2607
2608    def maybeLoadRDFs(self):
2609        if not self.fLadspaRdfNeedsUpdate:
2610            return
2611
2612        self.fLadspaRdfNeedsUpdate = False
2613        self.fLadspaRdfList = []
2614
2615        if not haveLRDF:
2616            return
2617
2618        settingsDir  = os.path.join(HOME, ".config", "falkTX")
2619        frLadspaFile = os.path.join(settingsDir, "ladspa_rdf.db")
2620
2621        if os.path.exists(frLadspaFile):
2622            frLadspa = open(frLadspaFile, 'r')
2623
2624            try:
2625                self.fLadspaRdfList = ladspa_rdf.get_c_ladspa_rdfs(json.load(frLadspa))
2626            except:
2627                pass
2628
2629            frLadspa.close()
2630
2631    # --------------------------------------------------------------------------------------------------------
2632
2633    def getPluginCount(self):
2634        return self.fPluginCount
2635
2636    def getPluginItem(self, pluginId):
2637        if pluginId >= self.fPluginCount:
2638            return None
2639
2640        pitem = self.fPluginList[pluginId]
2641        if pitem is None:
2642            return None
2643        #if False:
2644            #return CarlaRackItem(self, 0, False)
2645
2646        return pitem
2647
2648    def getPluginEditDialog(self, pluginId):
2649        if pluginId >= self.fPluginCount:
2650            return None
2651
2652        pitem = self.fPluginList[pluginId]
2653        if pitem is None:
2654            return None
2655        if False:
2656            return PluginEdit(self, self.host, 0)
2657
2658        return pitem.getEditDialog()
2659
2660    def getPluginSlotWidget(self, pluginId):
2661        if pluginId >= self.fPluginCount:
2662            return None
2663
2664        pitem = self.fPluginList[pluginId]
2665        if pitem is None:
2666            return None
2667        #if False:
2668            #return AbstractPluginSlot()
2669
2670        return pitem.getWidget()
2671
2672    # --------------------------------------------------------------------------------------------------------
2673
2674    def waitForPendingEvents(self):
2675        pass
2676
2677    # --------------------------------------------------------------------------------------------------------
2678    # show/hide event
2679
2680    def showEvent(self, event):
2681        self.getAndRefreshRuntimeInfo()
2682        self.refreshTransport(True)
2683        QMainWindow.showEvent(self, event)
2684
2685        if QT_VERSION >= 0x50600:
2686            self.host.set_engine_option(ENGINE_OPTION_FRONTEND_UI_SCALE, int(self.devicePixelRatioF() * 1000), "")
2687            print("Frontend pixel ratio is", self.devicePixelRatioF())
2688
2689        # set our gui as parent for all plugins UIs
2690        if self.host.manageUIs and not self.host.isControl:
2691            if MACOS:
2692                nsViewPtr = int(self.winId())
2693                winIdStr  = "%x" % gCarla.utils.cocoa_get_window(nsViewPtr)
2694            else:
2695                winIdStr = "%x" % int(self.winId())
2696            self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, winIdStr)
2697
2698    def hideEvent(self, event):
2699        # disable parent
2700        if not self.host.isControl:
2701            self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, "0")
2702
2703        QMainWindow.hideEvent(self, event)
2704
2705    # --------------------------------------------------------------------------------------------------------
2706    # resize event
2707
2708    def resizeEvent(self, event):
2709        QMainWindow.resizeEvent(self, event)
2710
2711        if self.fWithCanvas:
2712            self.slot_miniCanvasCheckSize()
2713
2714    # --------------------------------------------------------------------------------------------------------
2715    # timer event
2716
2717    def refreshRuntimeInfo(self, load, xruns):
2718        txt1 = str(xruns) if (xruns >= 0) else "--"
2719        txt2 = "" if (xruns == 1) else "s"
2720        self.ui.b_xruns.setText("%s Xrun%s" % (txt1, txt2))
2721        self.ui.pb_dsp_load.setValue(int(load))
2722
2723    def getAndRefreshRuntimeInfo(self):
2724        if not self.ui.pb_dsp_load.isVisible():
2725            return
2726        if not self.host.is_engine_running():
2727            return
2728        info = self.host.get_runtime_engine_info()
2729        self.refreshRuntimeInfo(info['load'], info['xruns'])
2730
2731    def idleFast(self):
2732        self.host.engine_idle()
2733        self.refreshTransport()
2734
2735        if self.fPluginCount == 0 or self.fCurrentlyRemovingAllPlugins:
2736            return
2737
2738        for pitem in self.fPluginList:
2739            if pitem is None:
2740                break
2741
2742            pitem.getWidget().idleFast()
2743
2744        for pluginId in self.fSelectedPlugins:
2745            self.fPeaksCleared = False
2746            if self.ui.peak_in.isVisible():
2747                self.ui.peak_in.displayMeter(1, self.host.get_input_peak_value(pluginId, True))
2748                self.ui.peak_in.displayMeter(2, self.host.get_input_peak_value(pluginId, False))
2749            if self.ui.peak_out.isVisible():
2750                self.ui.peak_out.displayMeter(1, self.host.get_output_peak_value(pluginId, True))
2751                self.ui.peak_out.displayMeter(2, self.host.get_output_peak_value(pluginId, False))
2752            return
2753
2754        if self.fPeaksCleared:
2755            return
2756
2757        self.fPeaksCleared = True
2758        self.ui.peak_in.displayMeter(1, 0.0, True)
2759        self.ui.peak_in.displayMeter(2, 0.0, True)
2760        self.ui.peak_out.displayMeter(1, 0.0, True)
2761        self.ui.peak_out.displayMeter(2, 0.0, True)
2762
2763    def idleSlow(self):
2764        self.getAndRefreshRuntimeInfo()
2765
2766        if self.fPluginCount == 0 or self.fCurrentlyRemovingAllPlugins:
2767            return
2768
2769        for pitem in self.fPluginList:
2770            if pitem is None:
2771                break
2772
2773            pitem.getWidget().idleSlow()
2774
2775    def timerEvent(self, event):
2776        if event.timerId() == self.fIdleTimerFast:
2777            self.idleFast()
2778
2779        elif event.timerId() == self.fIdleTimerSlow:
2780            self.idleSlow()
2781
2782        QMainWindow.timerEvent(self, event)
2783
2784    # --------------------------------------------------------------------------------------------------------
2785    # color/style change event
2786
2787    def changeEvent(self, event):
2788        if event.type() in (QEvent.PaletteChange, QEvent.StyleChange):
2789            self.updateStyle()
2790        QMainWindow.changeEvent(self, event)
2791
2792    def updateStyle(self):
2793        # Rack padding images setup
2794        rack_imgL = QImage(":/bitmaps/rack_padding_left.png")
2795        rack_imgR = QImage(":/bitmaps/rack_padding_right.png")
2796
2797        min_value = 0.07
2798
2799        if PYQT_VERSION >= 0x50600:
2800            value_fix = 1.0/(1.0-rack_imgL.scaled(1, 1, Qt.IgnoreAspectRatio, Qt.SmoothTransformation).pixelColor(0,0).blackF())
2801        else:
2802            value_fix = 1.5
2803
2804        rack_pal = self.ui.rack.palette()
2805        bg_color = rack_pal.window().color()
2806        fg_color = rack_pal.text().color()
2807        bg_value = 1.0 - bg_color.blackF()
2808        if bg_value != 0.0 and bg_value < min_value:
2809            pad_color = bg_color.lighter(int(100*min_value/bg_value*value_fix))
2810        else:
2811            pad_color = QColor.fromHsvF(0.0, 0.0, min_value*value_fix)
2812
2813        painter = QPainter()
2814        fillRect = rack_imgL.rect().adjusted(-1,-1,1,1)
2815
2816        painter.begin(rack_imgL)
2817        painter.setCompositionMode(QPainter.CompositionMode_Multiply)
2818        painter.setBrush(pad_color)
2819        painter.drawRect(fillRect)
2820        painter.end()
2821        rack_pixmapL = QPixmap(rack_imgL)
2822        self.imgL_palette = QPalette()
2823        self.imgL_palette.setBrush(QPalette.Window, QBrush(rack_pixmapL))
2824        self.ui.pad_left.setPalette(self.imgL_palette)
2825        self.ui.pad_left.setAutoFillBackground(True)
2826
2827        painter.begin(rack_imgR)
2828        painter.setCompositionMode(QPainter.CompositionMode_Multiply)
2829        painter.setBrush(pad_color)
2830        painter.drawRect(fillRect)
2831        painter.end()
2832        rack_pixmapR = QPixmap(rack_imgR)
2833        self.imgR_palette = QPalette()
2834        self.imgR_palette.setBrush(QPalette.Window, QBrush(rack_pixmapR))
2835        self.ui.pad_right.setPalette(self.imgR_palette)
2836        self.ui.pad_right.setAutoFillBackground(True)
2837
2838        # qt's rgba is actually argb, so convert that
2839        bg_color_value = bg_color.rgba()
2840        bg_color_value = ((bg_color_value & 0xffffff) << 8) | (bg_color_value >> 24)
2841
2842        fg_color_value = fg_color.rgba()
2843        fg_color_value = ((fg_color_value & 0xffffff) << 8) | (fg_color_value >> 24)
2844
2845        self.host.set_engine_option(ENGINE_OPTION_FRONTEND_BACKGROUND_COLOR, bg_color_value, "")
2846        self.host.set_engine_option(ENGINE_OPTION_FRONTEND_FOREGROUND_COLOR, fg_color_value, "")
2847
2848    # --------------------------------------------------------------------------------------------------------
2849    # paint event
2850
2851    #def paintEvent(self, event):
2852        #QMainWindow.paintEvent(self, event)
2853
2854        #if MACOS or not self.fSavedSettings[CARLA_KEY_CUSTOM_PAINTING]:
2855            #return
2856
2857        #painter = QPainter(self)
2858        #painter.setBrush(QColor(36, 36, 36))
2859        #painter.setPen(QColor(62, 62, 62))
2860        #painter.drawRect(1, self.height()/2, self.width()-3, self.height()-self.height()/2-1)
2861
2862    # --------------------------------------------------------------------------------------------------------
2863    # close event
2864
2865    def shouldIgnoreClose(self):
2866        if self.host.isControl or self.host.isPlugin:
2867            return False
2868        if self.fCustomStopAction == self.CUSTOM_ACTION_APP_CLOSE:
2869            return False
2870        if self.fWindowCloseHideGui:
2871            return False
2872        if self.fSavedSettings[CARLA_KEY_MAIN_CONFIRM_EXIT]:
2873            return QMessageBox.question(self, self.tr("Quit"),
2874                                              self.tr("Are you sure you want to quit Carla?"),
2875                                              QMessageBox.Yes|QMessageBox.No) == QMessageBox.No
2876        return False
2877
2878    def closeEvent(self, event):
2879        if self.shouldIgnoreClose():
2880            event.ignore()
2881            return
2882
2883        if self.fWindowCloseHideGui and self.fCustomStopAction != self.CUSTOM_ACTION_APP_CLOSE:
2884            self.hideForNSM()
2885            self.host.nsm_ready(NSM_CALLBACK_HIDE_OPTIONAL_GUI)
2886            return
2887
2888        patchcanvas.handleAllPluginsRemoved()
2889
2890        if MACOS and self.fMacClosingHelper and not (self.host.isControl or self.host.isPlugin):
2891            self.fCustomStopAction = self.CUSTOM_ACTION_APP_CLOSE
2892            self.fMacClosingHelper = False
2893            event.ignore()
2894
2895            for i in reversed(range(self.fPluginCount)):
2896                self.host.show_custom_ui(i, False)
2897
2898            QTimer.singleShot(100, self.close)
2899            return
2900
2901        self.killTimers()
2902        self.saveSettings()
2903
2904        if self.host.is_engine_running() and not (self.host.isControl or self.host.isPlugin):
2905            if not self.slot_engineStop(True):
2906                self.fCustomStopAction = self.CUSTOM_ACTION_APP_CLOSE
2907                event.ignore()
2908                return
2909
2910        QMainWindow.closeEvent(self, event)
2911
2912        # if we reach this point, fully close ourselves
2913        gCarla.gui = None
2914        QApplication.instance().quit()
2915
2916# ------------------------------------------------------------------------------------------------
2917# Canvas callback
2918
2919def canvasCallback(action, value1, value2, valueStr):
2920    host = gCarla.gui.host
2921
2922    if gCarla.gui.fCustomStopAction == HostWindow.CUSTOM_ACTION_APP_CLOSE:
2923        return
2924
2925    if action == patchcanvas.ACTION_GROUP_INFO:
2926        pass
2927
2928    elif action == patchcanvas.ACTION_GROUP_RENAME:
2929        pass
2930
2931    elif action == patchcanvas.ACTION_GROUP_SPLIT:
2932        groupId = value1
2933        patchcanvas.splitGroup(groupId)
2934        gCarla.gui.updateMiniCanvasLater()
2935
2936    elif action == patchcanvas.ACTION_GROUP_JOIN:
2937        groupId = value1
2938        patchcanvas.joinGroup(groupId)
2939        gCarla.gui.updateMiniCanvasLater()
2940
2941    elif action == patchcanvas.ACTION_GROUP_POSITION:
2942        if gCarla.gui.fIsProjectLoading:
2943            return
2944        if not host.is_engine_running():
2945            return
2946        groupId = value1
2947        x1, y1, x2, y2 = tuple(int(i) for i in valueStr.split(":"))
2948        host.patchbay_set_group_pos(gCarla.gui.fExternalPatchbay, groupId, x1, y1, x2, y2)
2949        gCarla.gui.updateMiniCanvasLater()
2950
2951    elif action == patchcanvas.ACTION_PORT_INFO:
2952        pass
2953
2954    elif action == patchcanvas.ACTION_PORT_RENAME:
2955        pass
2956
2957    elif action == patchcanvas.ACTION_PORTS_CONNECT:
2958        gOut, pOut, gIn, pIn = tuple(int(i) for i in valueStr.split(":"))
2959
2960        if not host.patchbay_connect(gCarla.gui.fExternalPatchbay, gOut, pOut, gIn, pIn):
2961            print("Connection failed:", host.get_last_error())
2962
2963    elif action == patchcanvas.ACTION_PORTS_DISCONNECT:
2964        connectionId = value1
2965
2966        if not host.patchbay_disconnect(gCarla.gui.fExternalPatchbay, connectionId):
2967            print("Disconnect failed:", host.get_last_error())
2968
2969    elif action == patchcanvas.ACTION_PLUGIN_CLONE:
2970        pluginId = value1
2971
2972        if not host.clone_plugin(pluginId):
2973            CustomMessageBox(gCarla.gui, QMessageBox.Warning, gCarla.gui.tr("Error"), gCarla.gui.tr("Operation failed"),
2974                                         host.get_last_error(), QMessageBox.Ok, QMessageBox.Ok)
2975
2976    elif action == patchcanvas.ACTION_PLUGIN_EDIT:
2977        pluginId = value1
2978        pwidget  = gCarla.gui.getPluginSlotWidget(pluginId)
2979
2980        if pwidget is not None:
2981            pwidget.showEditDialog()
2982
2983    elif action == patchcanvas.ACTION_PLUGIN_RENAME:
2984        pluginId = value1
2985        pwidget  = gCarla.gui.getPluginSlotWidget(pluginId)
2986
2987        if pwidget is not None:
2988            pwidget.showRenameDialog()
2989
2990    elif action == patchcanvas.ACTION_PLUGIN_REPLACE:
2991        pluginId = value1
2992        pwidget  = gCarla.gui.getPluginSlotWidget(pluginId)
2993
2994        if pwidget is not None:
2995            pwidget.showReplaceDialog()
2996
2997    elif action == patchcanvas.ACTION_PLUGIN_REMOVE:
2998        pluginId = value1
2999
3000        if not host.remove_plugin(pluginId):
3001            CustomMessageBox(gCarla.gui, QMessageBox.Warning, gCarla.gui.tr("Error"), gCarla.gui.tr("Operation failed"),
3002                                         host.get_last_error(), QMessageBox.Ok, QMessageBox.Ok)
3003
3004    elif action == patchcanvas.ACTION_PLUGIN_SHOW_UI:
3005        pluginId = value1
3006        pwidget  = gCarla.gui.getPluginSlotWidget(pluginId)
3007
3008        if pwidget is not None:
3009            pwidget.showCustomUI()
3010
3011    elif action == patchcanvas.ACTION_BG_RIGHT_CLICK:
3012        gCarla.gui.slot_showPluginActionsMenu()
3013
3014    elif action == patchcanvas.ACTION_INLINE_DISPLAY:
3015        if gCarla.gui.fIsProjectLoading:
3016            return
3017        if not host.is_engine_running():
3018            return
3019        pluginId = value1
3020        width, height = [int(v) for v in valueStr.split(":")]
3021        return host.render_inline_display(pluginId, width, height)
3022
3023# ------------------------------------------------------------------------------------------------------------
3024# Engine callback
3025
3026def engineCallback(host, action, pluginId, value1, value2, value3, valuef, valueStr):
3027    # kdevelop likes this :)
3028    if False: host = CarlaHostNull()
3029
3030    valueStr = charPtrToString(valueStr)
3031
3032    if action == ENGINE_CALLBACK_ENGINE_STARTED:
3033        host.processMode   = value1
3034        host.transportMode = value2
3035    elif action == ENGINE_CALLBACK_PROCESS_MODE_CHANGED:
3036        host.processMode   = value1
3037    elif action == ENGINE_CALLBACK_TRANSPORT_MODE_CHANGED:
3038        host.transportMode  = value1
3039        host.transportExtra = valueStr
3040
3041    if action == ENGINE_CALLBACK_DEBUG:
3042        host.DebugCallback.emit(pluginId, value1, value2, value3, valuef, valueStr)
3043    elif action == ENGINE_CALLBACK_PLUGIN_ADDED:
3044        host.PluginAddedCallback.emit(pluginId, valueStr)
3045    elif action == ENGINE_CALLBACK_PLUGIN_REMOVED:
3046        host.PluginRemovedCallback.emit(pluginId)
3047    elif action == ENGINE_CALLBACK_PLUGIN_RENAMED:
3048        host.PluginRenamedCallback.emit(pluginId, valueStr)
3049    elif action == ENGINE_CALLBACK_PLUGIN_UNAVAILABLE:
3050        host.PluginUnavailableCallback.emit(pluginId, valueStr)
3051    elif action == ENGINE_CALLBACK_PARAMETER_VALUE_CHANGED:
3052        host.ParameterValueChangedCallback.emit(pluginId, value1, valuef)
3053    elif action == ENGINE_CALLBACK_PARAMETER_DEFAULT_CHANGED:
3054        host.ParameterDefaultChangedCallback.emit(pluginId, value1, valuef)
3055    elif action == ENGINE_CALLBACK_PARAMETER_MAPPED_CONTROL_INDEX_CHANGED:
3056        host.ParameterMappedControlIndexChangedCallback.emit(pluginId, value1, value2)
3057    elif action == ENGINE_CALLBACK_PARAMETER_MAPPED_RANGE_CHANGED:
3058        minimum, maximum = (float(v) for v in valueStr.split(":", 2))
3059        host.ParameterMappedRangeChangedCallback.emit(pluginId, value1, minimum, maximum)
3060    elif action == ENGINE_CALLBACK_PARAMETER_MIDI_CHANNEL_CHANGED:
3061        host.ParameterMidiChannelChangedCallback.emit(pluginId, value1, value2)
3062    elif action == ENGINE_CALLBACK_PROGRAM_CHANGED:
3063        host.ProgramChangedCallback.emit(pluginId, value1)
3064    elif action == ENGINE_CALLBACK_MIDI_PROGRAM_CHANGED:
3065        host.MidiProgramChangedCallback.emit(pluginId, value1)
3066    elif action == ENGINE_CALLBACK_OPTION_CHANGED:
3067        host.OptionChangedCallback.emit(pluginId, value1, bool(value2))
3068    elif action == ENGINE_CALLBACK_UI_STATE_CHANGED:
3069        host.UiStateChangedCallback.emit(pluginId, value1)
3070    elif action == ENGINE_CALLBACK_NOTE_ON:
3071        host.NoteOnCallback.emit(pluginId, value1, value2, value3)
3072    elif action == ENGINE_CALLBACK_NOTE_OFF:
3073        host.NoteOffCallback.emit(pluginId, value1, value2)
3074    elif action == ENGINE_CALLBACK_UPDATE:
3075        host.UpdateCallback.emit(pluginId)
3076    elif action == ENGINE_CALLBACK_RELOAD_INFO:
3077        host.ReloadInfoCallback.emit(pluginId)
3078    elif action == ENGINE_CALLBACK_RELOAD_PARAMETERS:
3079        host.ReloadParametersCallback.emit(pluginId)
3080    elif action == ENGINE_CALLBACK_RELOAD_PROGRAMS:
3081        host.ReloadProgramsCallback.emit(pluginId)
3082    elif action == ENGINE_CALLBACK_RELOAD_ALL:
3083        host.ReloadAllCallback.emit(pluginId)
3084    elif action == ENGINE_CALLBACK_PATCHBAY_CLIENT_ADDED:
3085        host.PatchbayClientAddedCallback.emit(pluginId, value1, value2, valueStr)
3086    elif action == ENGINE_CALLBACK_PATCHBAY_CLIENT_REMOVED:
3087        host.PatchbayClientRemovedCallback.emit(pluginId)
3088    elif action == ENGINE_CALLBACK_PATCHBAY_CLIENT_RENAMED:
3089        host.PatchbayClientRenamedCallback.emit(pluginId, valueStr)
3090    elif action == ENGINE_CALLBACK_PATCHBAY_CLIENT_DATA_CHANGED:
3091        host.PatchbayClientDataChangedCallback.emit(pluginId, value1, value2)
3092    elif action == ENGINE_CALLBACK_PATCHBAY_CLIENT_POSITION_CHANGED:
3093        host.PatchbayClientPositionChangedCallback.emit(pluginId, value1, value2, value3, int(round(valuef)))
3094    elif action == ENGINE_CALLBACK_PATCHBAY_PORT_ADDED:
3095        host.PatchbayPortAddedCallback.emit(pluginId, value1, value2, value3, valueStr)
3096    elif action == ENGINE_CALLBACK_PATCHBAY_PORT_REMOVED:
3097        host.PatchbayPortRemovedCallback.emit(pluginId, value1)
3098    elif action == ENGINE_CALLBACK_PATCHBAY_PORT_CHANGED:
3099        host.PatchbayPortChangedCallback.emit(pluginId, value1, value2, value3, valueStr)
3100    elif action == ENGINE_CALLBACK_PATCHBAY_PORT_GROUP_ADDED:
3101        host.PatchbayPortGroupAddedCallback.emit(pluginId, value1, value2, valueStr)
3102    elif action == ENGINE_CALLBACK_PATCHBAY_PORT_GROUP_REMOVED:
3103        host.PatchbayPortGroupRemovedCallback.emit(pluginId, value1)
3104    elif action == ENGINE_CALLBACK_PATCHBAY_PORT_GROUP_CHANGED:
3105        host.PatchbayPortGroupChangedCallback.emit(pluginId, value1, value2, valueStr)
3106    elif action == ENGINE_CALLBACK_PATCHBAY_CONNECTION_ADDED:
3107        gOut, pOut, gIn, pIn = [int(i) for i in valueStr.split(":")] # FIXME
3108        host.PatchbayConnectionAddedCallback.emit(pluginId, gOut, pOut, gIn, pIn)
3109    elif action == ENGINE_CALLBACK_PATCHBAY_CONNECTION_REMOVED:
3110        host.PatchbayConnectionRemovedCallback.emit(pluginId, value1, value2)
3111    elif action == ENGINE_CALLBACK_ENGINE_STARTED:
3112        host.EngineStartedCallback.emit(pluginId, value1, value2, value3, valuef, valueStr)
3113    elif action == ENGINE_CALLBACK_ENGINE_STOPPED:
3114        host.EngineStoppedCallback.emit()
3115    elif action == ENGINE_CALLBACK_PROCESS_MODE_CHANGED:
3116        host.ProcessModeChangedCallback.emit(value1)
3117    elif action == ENGINE_CALLBACK_TRANSPORT_MODE_CHANGED:
3118        host.TransportModeChangedCallback.emit(value1, valueStr)
3119    elif action == ENGINE_CALLBACK_BUFFER_SIZE_CHANGED:
3120        host.BufferSizeChangedCallback.emit(value1)
3121    elif action == ENGINE_CALLBACK_SAMPLE_RATE_CHANGED:
3122        host.SampleRateChangedCallback.emit(valuef)
3123    elif action == ENGINE_CALLBACK_CANCELABLE_ACTION:
3124        host.CancelableActionCallback.emit(pluginId, bool(value1 != 0), valueStr)
3125    elif action == ENGINE_CALLBACK_PROJECT_LOAD_FINISHED:
3126        host.ProjectLoadFinishedCallback.emit()
3127    elif action == ENGINE_CALLBACK_NSM:
3128        host.NSMCallback.emit(value1, value2, valueStr)
3129    elif action == ENGINE_CALLBACK_IDLE:
3130        QApplication.processEvents()
3131    elif action == ENGINE_CALLBACK_INFO:
3132        host.InfoCallback.emit(valueStr)
3133    elif action == ENGINE_CALLBACK_ERROR:
3134        host.ErrorCallback.emit(valueStr)
3135    elif action == ENGINE_CALLBACK_QUIT:
3136        host.QuitCallback.emit()
3137    elif action == ENGINE_CALLBACK_INLINE_DISPLAY_REDRAW:
3138        host.InlineDisplayRedrawCallback.emit(pluginId)
3139    else:
3140        print("unhandled action", action)
3141
3142# ------------------------------------------------------------------------------------------------------------
3143# File callback
3144
3145def fileCallback(ptr, action, isDir, title, filter):
3146    title  = charPtrToString(title)
3147    filter = charPtrToString(filter)
3148
3149    if action == FILE_CALLBACK_OPEN:
3150        ret, ok = QFileDialog.getOpenFileName(gCarla.gui, title, "", filter) #, QFileDialog.ShowDirsOnly if isDir else 0x0)
3151    elif action == FILE_CALLBACK_SAVE:
3152        ret, ok = QFileDialog.getSaveFileName(gCarla.gui, title, "", filter, QFileDialog.ShowDirsOnly if isDir else 0x0)
3153    else:
3154        ret, ok = ("", "")
3155
3156    # FIXME use ok value, test if it works as expected
3157    if not ret:
3158        return None
3159
3160    # FIXME
3161    global fileRet
3162    fileRet = c_char_p(ret.encode("utf-8"))
3163    retval  = cast(byref(fileRet), POINTER(c_uintptr))
3164    return retval.contents.value
3165
3166# ------------------------------------------------------------------------------------------------------------
3167# Init host
3168
3169def initHost(initName, libPrefix, isControl, isPlugin, failError, HostClass = None):
3170    pathBinaries, pathResources = getPaths(libPrefix)
3171
3172    # --------------------------------------------------------------------------------------------------------
3173    # Fail if binary dir is not found
3174
3175    if not os.path.exists(pathBinaries):
3176        if failError:
3177            QMessageBox.critical(None, "Error", "Failed to find the carla binaries, cannot continue")
3178            sys.exit(1)
3179        return
3180
3181    # --------------------------------------------------------------------------------------------------------
3182    # Check if we should open main lib as local or global
3183
3184    settings = QSafeSettings("falkTX", "Carla2")
3185
3186    loadGlobal = settings.value(CARLA_KEY_EXPERIMENTAL_LOAD_LIB_GLOBAL, CARLA_DEFAULT_EXPERIMENTAL_LOAD_LIB_GLOBAL, bool)
3187
3188    # --------------------------------------------------------------------------------------------------------
3189    # Set Carla library name
3190
3191    libname   = "libcarla_%s2.%s" % ("control" if isControl else "standalone", DLL_EXTENSION)
3192    libname   = os.path.join(pathBinaries, libname)
3193    utilsname = os.path.join(pathBinaries, "libcarla_utils.%s" % (DLL_EXTENSION))
3194
3195    # --------------------------------------------------------------------------------------------------------
3196    # Print info
3197
3198    if not (gCarla.nogui and isinstance(gCarla.nogui, int)):
3199        print("Carla %s started, status:" % VERSION)
3200        print("  Python version: %s" % sys.version.split(" ",1)[0])
3201        print("  Qt version:     %s" % QT_VERSION_STR)
3202        print("  PyQt version:   %s" % PYQT_VERSION_STR)
3203        print("  Binary dir:     %s" % pathBinaries)
3204        print("  Resources dir:  %s" % pathResources)
3205
3206    # --------------------------------------------------------------------------------------------------------
3207    # Init host
3208
3209    if failError:
3210        # no try
3211        host = HostClass() if HostClass is not None else CarlaHostQtDLL(libname, loadGlobal)
3212    else:
3213        try:
3214            host = HostClass() if HostClass is not None else CarlaHostQtDLL(libname, loadGlobal)
3215        except:
3216            host = CarlaHostQtNull()
3217
3218    host.isControl = isControl
3219    host.isPlugin  = isPlugin
3220
3221    host.set_engine_callback(lambda h,a,p,v1,v2,v3,vf,vs: engineCallback(host,a,p,v1,v2,v3,vf,vs))
3222    host.set_file_callback(fileCallback)
3223
3224    # If it's a plugin the paths are already set
3225    if not isPlugin:
3226        host.pathBinaries  = pathBinaries
3227        host.pathResources = pathResources
3228        host.set_engine_option(ENGINE_OPTION_PATH_BINARIES,  0, pathBinaries)
3229        host.set_engine_option(ENGINE_OPTION_PATH_RESOURCES, 0, pathResources)
3230
3231        if not isControl:
3232            host.nsmOK = host.nsm_init(os.getpid(), initName)
3233
3234    # --------------------------------------------------------------------------------------------------------
3235    # Init utils
3236
3237    gCarla.utils = CarlaUtils(utilsname)
3238    gCarla.utils.set_process_name(os.path.basename(initName))
3239
3240    try:
3241        sys.stdout.flush()
3242    except:
3243        pass
3244
3245    sys.stdout = CarlaPrint(False)
3246    sys.stderr = CarlaPrint(True)
3247    sys.excepthook = sys_excepthook
3248
3249    # --------------------------------------------------------------------------------------------------------
3250    # Done
3251
3252    return host
3253
3254# ------------------------------------------------------------------------------------------------------------
3255# Load host settings
3256
3257def loadHostSettings(host):
3258    # kdevelop likes this :)
3259    if False: host = CarlaHostNull()
3260
3261    settings = QSafeSettings("falkTX", "Carla2")
3262
3263    host.experimental = settings.value(CARLA_KEY_MAIN_EXPERIMENTAL, CARLA_DEFAULT_MAIN_EXPERIMENTAL, bool)
3264    host.exportLV2 = settings.value(CARLA_KEY_EXPERIMENTAL_EXPORT_LV2, CARLA_DEFAULT_EXPERIMENTAL_LV2_EXPORT, bool)
3265    host.manageUIs = settings.value(CARLA_KEY_ENGINE_MANAGE_UIS, CARLA_DEFAULT_MANAGE_UIS, bool)
3266    host.maxParameters = settings.value(CARLA_KEY_ENGINE_MAX_PARAMETERS, CARLA_DEFAULT_MAX_PARAMETERS, int)
3267    host.resetXruns = settings.value(CARLA_KEY_ENGINE_RESET_XRUNS, CARLA_DEFAULT_RESET_XRUNS, bool)
3268    host.forceStereo = settings.value(CARLA_KEY_ENGINE_FORCE_STEREO, CARLA_DEFAULT_FORCE_STEREO, bool)
3269    host.preferPluginBridges = settings.value(CARLA_KEY_ENGINE_PREFER_PLUGIN_BRIDGES, CARLA_DEFAULT_PREFER_PLUGIN_BRIDGES, bool)
3270    host.preferUIBridges = settings.value(CARLA_KEY_ENGINE_PREFER_UI_BRIDGES, CARLA_DEFAULT_PREFER_UI_BRIDGES, bool)
3271    host.preventBadBehaviour = settings.value(CARLA_KEY_EXPERIMENTAL_PREVENT_BAD_BEHAVIOUR, CARLA_DEFAULT_EXPERIMENTAL_PREVENT_BAD_BEHAVIOUR, bool)
3272    host.showLogs = settings.value(CARLA_KEY_MAIN_SHOW_LOGS, CARLA_DEFAULT_MAIN_SHOW_LOGS, bool) and not WINDOWS
3273    host.showPluginBridges = settings.value(CARLA_KEY_EXPERIMENTAL_PLUGIN_BRIDGES, CARLA_DEFAULT_EXPERIMENTAL_PLUGIN_BRIDGES, bool)
3274    host.showWineBridges = settings.value(CARLA_KEY_EXPERIMENTAL_WINE_BRIDGES, CARLA_DEFAULT_EXPERIMENTAL_WINE_BRIDGES, bool)
3275    host.uiBridgesTimeout = settings.value(CARLA_KEY_ENGINE_UI_BRIDGES_TIMEOUT, CARLA_DEFAULT_UI_BRIDGES_TIMEOUT, int)
3276    host.uisAlwaysOnTop = settings.value(CARLA_KEY_ENGINE_UIS_ALWAYS_ON_TOP, CARLA_DEFAULT_UIS_ALWAYS_ON_TOP, bool)
3277
3278    if host.isPlugin:
3279        return
3280
3281    host.transportExtra = settings.value(CARLA_KEY_ENGINE_TRANSPORT_EXTRA, "", str)
3282
3283    # enums
3284    if host.audioDriverForced is None:
3285        host.transportMode = settings.value(CARLA_KEY_ENGINE_TRANSPORT_MODE, CARLA_DEFAULT_TRANSPORT_MODE, int)
3286
3287    if not host.processModeForced:
3288        host.processMode = settings.value(CARLA_KEY_ENGINE_PROCESS_MODE, CARLA_DEFAULT_PROCESS_MODE, int)
3289
3290    host.nextProcessMode = host.processMode
3291
3292    # --------------------------------------------------------------------------------------------------------
3293    # fix things if needed
3294
3295    if host.processMode == ENGINE_PROCESS_MODE_MULTIPLE_CLIENTS:
3296        if LADISH_APP_NAME:
3297            print("LADISH detected but using multiple clients (not allowed), forcing single client now")
3298            host.nextProcessMode = host.processMode = ENGINE_PROCESS_MODE_SINGLE_CLIENT
3299
3300        else:
3301            host.transportMode = ENGINE_TRANSPORT_MODE_JACK
3302
3303    if gCarla.nogui:
3304        host.showLogs = False
3305
3306    # --------------------------------------------------------------------------------------------------------
3307    # run headless host now if nogui option enabled
3308
3309    if gCarla.nogui:
3310        runHostWithoutUI(host)
3311
3312# ------------------------------------------------------------------------------------------------------------
3313# Set host settings
3314
3315def setHostSettings(host):
3316    # kdevelop likes this :)
3317    if False: host = CarlaHostNull()
3318
3319    host.set_engine_option(ENGINE_OPTION_FORCE_STEREO,          host.forceStereo,         "")
3320    host.set_engine_option(ENGINE_OPTION_MAX_PARAMETERS,        host.maxParameters,       "")
3321    host.set_engine_option(ENGINE_OPTION_RESET_XRUNS,           host.resetXruns,          "")
3322    host.set_engine_option(ENGINE_OPTION_PREFER_PLUGIN_BRIDGES, host.preferPluginBridges, "")
3323    host.set_engine_option(ENGINE_OPTION_PREFER_UI_BRIDGES,     host.preferUIBridges,     "")
3324    host.set_engine_option(ENGINE_OPTION_PREVENT_BAD_BEHAVIOUR, host.preventBadBehaviour, "")
3325    host.set_engine_option(ENGINE_OPTION_UI_BRIDGES_TIMEOUT,    host.uiBridgesTimeout,    "")
3326    host.set_engine_option(ENGINE_OPTION_UIS_ALWAYS_ON_TOP,     host.uisAlwaysOnTop,      "")
3327
3328    if host.isPlugin or host.isRemote or host.is_engine_running():
3329        return
3330
3331    host.set_engine_option(ENGINE_OPTION_PROCESS_MODE,          host.nextProcessMode,     "")
3332    host.set_engine_option(ENGINE_OPTION_TRANSPORT_MODE,        host.transportMode,       host.transportExtra)
3333    host.set_engine_option(ENGINE_OPTION_DEBUG_CONSOLE_OUTPUT,  host.showLogs,            "")
3334
3335    if not (NSM_URL and host.nsmOK):
3336        host.set_engine_option(ENGINE_OPTION_CLIENT_NAME_PREFIX, 0, gCarla.cnprefix)
3337
3338# ------------------------------------------------------------------------------------------------------------
3339# Set Engine settings according to carla preferences. Returns selected audio driver.
3340
3341def setEngineSettings(host, oscPort = None):
3342    # kdevelop likes this :)
3343    if False: host = CarlaHostNull()
3344
3345    # --------------------------------------------------------------------------------------------------------
3346    # do nothing if control
3347
3348    if host.isControl:
3349        return "Control"
3350
3351    # --------------------------------------------------------------------------------------------------------
3352
3353    settings = QSafeSettings("falkTX", "Carla2")
3354
3355    # --------------------------------------------------------------------------------------------------------
3356    # main settings
3357
3358    setHostSettings(host)
3359
3360    # --------------------------------------------------------------------------------------------------------
3361    # file paths
3362
3363    FILE_PATH_AUDIO = settings.value(CARLA_KEY_PATHS_AUDIO, CARLA_DEFAULT_FILE_PATH_AUDIO, list)
3364    FILE_PATH_MIDI  = settings.value(CARLA_KEY_PATHS_MIDI,  CARLA_DEFAULT_FILE_PATH_MIDI, list)
3365
3366    host.set_engine_option(ENGINE_OPTION_FILE_PATH, FILE_AUDIO, splitter.join(FILE_PATH_AUDIO))
3367    host.set_engine_option(ENGINE_OPTION_FILE_PATH, FILE_MIDI,  splitter.join(FILE_PATH_MIDI))
3368
3369    # --------------------------------------------------------------------------------------------------------
3370    # plugin paths
3371
3372    LADSPA_PATH = settings.value(CARLA_KEY_PATHS_LADSPA, CARLA_DEFAULT_LADSPA_PATH, list)
3373    DSSI_PATH   = settings.value(CARLA_KEY_PATHS_DSSI,   CARLA_DEFAULT_DSSI_PATH, list)
3374    LV2_PATH    = settings.value(CARLA_KEY_PATHS_LV2,    CARLA_DEFAULT_LV2_PATH, list)
3375    VST2_PATH   = settings.value(CARLA_KEY_PATHS_VST2,   CARLA_DEFAULT_VST2_PATH, list)
3376    VST3_PATH   = settings.value(CARLA_KEY_PATHS_VST3,   CARLA_DEFAULT_VST3_PATH, list)
3377    SF2_PATH    = settings.value(CARLA_KEY_PATHS_SF2,    CARLA_DEFAULT_SF2_PATH, list)
3378    SFZ_PATH    = settings.value(CARLA_KEY_PATHS_SFZ,    CARLA_DEFAULT_SFZ_PATH, list)
3379
3380    host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_LADSPA, splitter.join(LADSPA_PATH))
3381    host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_DSSI,   splitter.join(DSSI_PATH))
3382    host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_LV2,    splitter.join(LV2_PATH))
3383    host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_VST2,   splitter.join(VST2_PATH))
3384    host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_VST3,   splitter.join(VST3_PATH))
3385    host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_SF2,    splitter.join(SF2_PATH))
3386    host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_SFZ,    splitter.join(SFZ_PATH))
3387
3388    # --------------------------------------------------------------------------------------------------------
3389    # don't continue if plugin
3390
3391    if host.isPlugin:
3392        return "Plugin"
3393
3394    # --------------------------------------------------------------------------------------------------------
3395    # osc settings
3396
3397    if oscPort is not None and isinstance(oscPort, int):
3398        oscEnabled = True
3399        portNumTCP = portNumUDP = oscPort
3400
3401    else:
3402        oscEnabled = settings.value(CARLA_KEY_OSC_ENABLED, CARLA_DEFAULT_OSC_ENABLED, bool)
3403
3404        if not settings.value(CARLA_KEY_OSC_TCP_PORT_ENABLED, CARLA_DEFAULT_OSC_TCP_PORT_ENABLED, bool):
3405            portNumTCP = -1
3406        elif settings.value(CARLA_KEY_OSC_TCP_PORT_RANDOM, CARLA_DEFAULT_OSC_TCP_PORT_RANDOM, bool):
3407            portNumTCP = 0
3408        else:
3409            portNumTCP = settings.value(CARLA_KEY_OSC_TCP_PORT_NUMBER, CARLA_DEFAULT_OSC_TCP_PORT_NUMBER, int)
3410
3411        if not settings.value(CARLA_KEY_OSC_UDP_PORT_ENABLED, CARLA_DEFAULT_OSC_UDP_PORT_ENABLED, bool):
3412            portNumUDP = -1
3413        elif settings.value(CARLA_KEY_OSC_UDP_PORT_RANDOM, CARLA_DEFAULT_OSC_UDP_PORT_RANDOM, bool):
3414            portNumUDP = 0
3415        else:
3416            portNumUDP = settings.value(CARLA_KEY_OSC_UDP_PORT_NUMBER, CARLA_DEFAULT_OSC_UDP_PORT_NUMBER, int)
3417
3418    host.set_engine_option(ENGINE_OPTION_OSC_ENABLED, 1 if oscEnabled else 0, "")
3419    host.set_engine_option(ENGINE_OPTION_OSC_PORT_TCP, portNumTCP, "")
3420    host.set_engine_option(ENGINE_OPTION_OSC_PORT_UDP, portNumUDP, "")
3421
3422    # --------------------------------------------------------------------------------------------------------
3423    # wine settings
3424
3425    optWineExecutable = settings.value(CARLA_KEY_WINE_EXECUTABLE, CARLA_DEFAULT_WINE_EXECUTABLE, str)
3426    optWineAutoPrefix = settings.value(CARLA_KEY_WINE_AUTO_PREFIX, CARLA_DEFAULT_WINE_AUTO_PREFIX, bool)
3427    optWineFallbackPrefix = settings.value(CARLA_KEY_WINE_FALLBACK_PREFIX, CARLA_DEFAULT_WINE_FALLBACK_PREFIX, str)
3428    optWineRtPrioEnabled = settings.value(CARLA_KEY_WINE_RT_PRIO_ENABLED, CARLA_DEFAULT_WINE_RT_PRIO_ENABLED, bool)
3429    optWineBaseRtPrio = settings.value(CARLA_KEY_WINE_BASE_RT_PRIO,   CARLA_DEFAULT_WINE_BASE_RT_PRIO, int)
3430    optWineServerRtPrio = settings.value(CARLA_KEY_WINE_SERVER_RT_PRIO, CARLA_DEFAULT_WINE_SERVER_RT_PRIO, int)
3431
3432    host.set_engine_option(ENGINE_OPTION_WINE_EXECUTABLE, 0, optWineExecutable)
3433    host.set_engine_option(ENGINE_OPTION_WINE_AUTO_PREFIX, 1 if optWineAutoPrefix else 0, "")
3434    host.set_engine_option(ENGINE_OPTION_WINE_FALLBACK_PREFIX, 0, os.path.expanduser(optWineFallbackPrefix))
3435    host.set_engine_option(ENGINE_OPTION_WINE_RT_PRIO_ENABLED, 1 if optWineRtPrioEnabled else 0, "")
3436    host.set_engine_option(ENGINE_OPTION_WINE_BASE_RT_PRIO, optWineBaseRtPrio, "")
3437    host.set_engine_option(ENGINE_OPTION_WINE_SERVER_RT_PRIO, optWineServerRtPrio, "")
3438
3439    # --------------------------------------------------------------------------------------------------------
3440    # driver and device settings
3441
3442    # driver name
3443    if host.audioDriverForced is not None:
3444        audioDriver = host.audioDriverForced
3445    else:
3446        try:
3447            audioDriver = settings.value(CARLA_KEY_ENGINE_AUDIO_DRIVER, CARLA_DEFAULT_AUDIO_DRIVER, str)
3448        except:
3449            audioDriver = CARLA_DEFAULT_AUDIO_DRIVER
3450
3451    # driver options
3452    audioDevice = settings.value("%s%s/Device" % (CARLA_KEY_ENGINE_DRIVER_PREFIX, audioDriver), "", str)
3453    audioBufferSize = settings.value("%s%s/BufferSize" % (CARLA_KEY_ENGINE_DRIVER_PREFIX, audioDriver), CARLA_DEFAULT_AUDIO_BUFFER_SIZE, int)
3454    audioSampleRate = settings.value("%s%s/SampleRate" % (CARLA_KEY_ENGINE_DRIVER_PREFIX, audioDriver), CARLA_DEFAULT_AUDIO_SAMPLE_RATE, int)
3455    audioTripleBuffer = settings.value("%s%s/TripleBuffer" % (CARLA_KEY_ENGINE_DRIVER_PREFIX, audioDriver), CARLA_DEFAULT_AUDIO_TRIPLE_BUFFER, bool)
3456
3457    # Only setup audio things if engine is not running
3458    if not host.is_engine_running():
3459        host.set_engine_option(ENGINE_OPTION_AUDIO_DRIVER, 0, audioDriver)
3460        host.set_engine_option(ENGINE_OPTION_AUDIO_DEVICE, 0, audioDevice)
3461
3462        if not audioDriver.startswith("JACK"):
3463            host.set_engine_option(ENGINE_OPTION_AUDIO_BUFFER_SIZE, audioBufferSize, "")
3464            host.set_engine_option(ENGINE_OPTION_AUDIO_SAMPLE_RATE, audioSampleRate, "")
3465            host.set_engine_option(ENGINE_OPTION_AUDIO_TRIPLE_BUFFER, 1 if audioTripleBuffer else 0, "")
3466
3467    # --------------------------------------------------------------------------------------------------------
3468    # fix things if needed
3469
3470    if audioDriver != "JACK" and host.transportMode == ENGINE_TRANSPORT_MODE_JACK:
3471        host.transportMode = ENGINE_TRANSPORT_MODE_INTERNAL
3472        host.set_engine_option(ENGINE_OPTION_TRANSPORT_MODE, ENGINE_TRANSPORT_MODE_INTERNAL, host.transportExtra)
3473
3474    # --------------------------------------------------------------------------------------------------------
3475    # return selected driver name
3476
3477    return audioDriver
3478
3479# ------------------------------------------------------------------------------------------------------------
3480# Run Carla without showing UI
3481
3482def runHostWithoutUI(host):
3483    # kdevelop likes this :)
3484    if False: host = CarlaHostNull()
3485
3486    # --------------------------------------------------------------------------------------------------------
3487    # Some initial checks
3488
3489    if not gCarla.nogui:
3490        return
3491
3492    projectFile = getInitialProjectFile(True)
3493
3494    if not isinstance(gCarla.nogui, int):
3495        oscPort = None
3496
3497        if not projectFile:
3498            print("Carla no-gui mode can only be used together with a project file.")
3499            sys.exit(1)
3500
3501    else:
3502        oscPort = gCarla.nogui
3503
3504    # --------------------------------------------------------------------------------------------------------
3505    # Additional imports
3506
3507    from time import sleep
3508
3509    # --------------------------------------------------------------------------------------------------------
3510    # Init engine
3511
3512    audioDriver = setEngineSettings(host, oscPort)
3513    if not host.engine_init(audioDriver, "Carla"):
3514        print("Engine failed to initialize, possible reasons:\n%s" % host.get_last_error())
3515        sys.exit(1)
3516
3517    if projectFile and not host.load_project(projectFile):
3518        print("Failed to load selected project file, possible reasons:\n%s" % host.get_last_error())
3519        host.engine_close()
3520        sys.exit(1)
3521
3522    # --------------------------------------------------------------------------------------------------------
3523    # Idle
3524
3525    print("Carla ready!")
3526
3527    while host.is_engine_running() and not gCarla.term:
3528        host.engine_idle()
3529        sleep(0.0333) # 30 Hz
3530
3531    # --------------------------------------------------------------------------------------------------------
3532    # Stop
3533
3534    host.engine_close()
3535    sys.exit(0)
3536
3537# ------------------------------------------------------------------------------------------------------------
3538