diff --git a/MoonrakerConnection/MoonrakerMachineAction.py b/MoonrakerConnection/MoonrakerMachineAction.py index 3592d0f..8f041a8 100644 --- a/MoonrakerConnection/MoonrakerMachineAction.py +++ b/MoonrakerConnection/MoonrakerMachineAction.py @@ -43,6 +43,8 @@ def _onGlobalContainerStackChanged(self) -> None: self.settingsTranslateOutputChanged.emit() self.settingsTranslateRemoveChanged.emit() self.settingsCameraUrlChanged.emit() + self.settingsCameraImageRotationChanged.emit() + self.settingsCameraImageMirrorChanged.emit() def _onContainerAdded(self, container) -> None: # Add this action as a supported action to all machine definitions @@ -64,6 +66,8 @@ def _reset(self) -> None: self.settingsTranslateOutputChanged.emit() self.settingsTranslateRemoveChanged.emit() self.settingsCameraUrlChanged.emit() + self.settingsCameraImageRotationChanged.emit() + self.settingsCameraImageMirrorChanged.emit() settingsUrlChanged = pyqtSignal() settingsApiKeyChanged = pyqtSignal() @@ -79,6 +83,8 @@ def _reset(self) -> None: settingsTranslateOutputChanged = pyqtSignal() settingsTranslateRemoveChanged = pyqtSignal() settingsCameraUrlChanged = pyqtSignal() + settingsCameraImageRotationChanged = pyqtSignal() + settingsCameraImageMirrorChanged = pyqtSignal() @pyqtProperty(str, notify = settingsUrlChanged) def settingsUrl(self) -> Optional[str]: @@ -108,7 +114,7 @@ def settingsFrontendUrl(self) -> Optional[str]: @pyqtProperty(str, notify = settingsOutputFormatChanged) def settingsOutputFormat(self) -> Optional[str]: config = getConfig() - return config.get("output_format", "gcode" if config else "gcode") + return config.get("output_format", "gcode") if config else "gcode" @pyqtProperty(bool, notify = settingsUploadDialogChanged) def settingsUploadDialog(self) -> Optional[bool]: @@ -150,6 +156,16 @@ def settingsCameraUrl(self) -> Optional[str]: config = getConfig() return config.get("camera_url", "") if config else "" + @pyqtProperty(str, notify = settingsCameraImageRotationChanged) + def settingsCameraImageRotation(self) -> Optional[str]: + config = getConfig() + rotation = config.get("camera_image_rotation", "0") if config else "0" + return rotation if rotation == "90" or rotation == "180" or rotation == "270" else "0" + + @pyqtProperty(bool, notify = settingsCameraImageMirrorChanged) + def settingsCameraImageMirror(self) -> Optional[bool]: + config = getConfig() + return config.get("camera_image_mirror", False) if config else False @pyqtSlot(QVariant) def saveConfig(self, paramsQJSValObj): diff --git a/MoonrakerConnection/MoonrakerOutputDevice.py b/MoonrakerConnection/MoonrakerOutputDevice.py index 9c15eed..77ae14e 100644 --- a/MoonrakerConnection/MoonrakerOutputDevice.py +++ b/MoonrakerConnection/MoonrakerOutputDevice.py @@ -19,7 +19,6 @@ from cura.CuraApplication import CuraApplication from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionType -from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from UM.i18n import i18nCatalog from UM.Logger import Logger @@ -28,6 +27,7 @@ from UM.OutputDevice import OutputDeviceError from .MoonrakerOutputController import MoonrakerOutputController +from .MoonrakerOutputModel import MoonrakerOutputModel from .MoonrakerSettings import getConfig, saveConfig, validateUrl try: @@ -53,7 +53,7 @@ def __init__(self, deviceId: str, canConnect: bool = True) -> None: super().__init__(device_id = "MoonrakerOutputDevice@" + deviceId, connection_type = ConnectionType.NetworkConnection if canConnect else ConnectionType.NotConnected) # init controller and model for output globalContainerStack = CuraApplication.getInstance().getGlobalContainerStack() - self._printers = [PrinterOutputModel(output_controller = MoonrakerOutputController(self), number_of_extruders = globalContainerStack.getProperty("machine_extruder_count", "value"))]; + self._printers = [MoonrakerOutputModel(output_controller = MoonrakerOutputController(self), number_of_extruders = globalContainerStack.getProperty("machine_extruder_count", "value"))]; Logger.log("d", "number_of_extruders: {}".format(globalContainerStack.getProperty("machine_extruder_count", "value"))) self._printers[0].updateName(globalContainerStack.getName()); self._printers[0].updateUniqueName(globalContainerStack.getId()) @@ -149,6 +149,8 @@ def updateConfig(self, config: dict = None) -> None: self._translateOutput = self._config.get("trans_output", "") self._translateRemove = self._config.get("trans_remove", "") self._cameraUrl = self._config.get("camera_url", "").strip() + self._cameraImageRotation = self._config.get("camera_image_rotation", "0") + self._cameraImageMirror = self._config.get("camera_image_mirror", False) # Configure address and webcam self._address = self._url @@ -165,6 +167,8 @@ def updateConfig(self, config: dict = None) -> None: self.activePrinter.setCameraUrl(cameraUrl) else: self.activePrinter.setCameraUrl(QUrl()) + self.activePrinter.setCameraImageRotation(self._cameraImageRotation) + self.activePrinter.setCameraImageMirror(self._cameraImageMirror) # Configure ui components globalContainerStack = CuraApplication.getInstance().getGlobalContainerStack() diff --git a/MoonrakerConnection/MoonrakerOutputModel.py b/MoonrakerConnection/MoonrakerOutputModel.py new file mode 100644 index 0000000..95c91c9 --- /dev/null +++ b/MoonrakerConnection/MoonrakerOutputModel.py @@ -0,0 +1,41 @@ +USE_QT5 = False +try: + from PyQt6.QtCore import pyqtProperty, pyqtSignal +except ImportError: + from PyQt5.QtCore import pyqtProperty, pyqtSignal + USE_QT5 = True + +from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel + +from UM.Logger import Logger + +from .MoonrakerOutputController import MoonrakerOutputController + +class MoonrakerOutputModel(PrinterOutputModel): + + cameraImageRotationChanged = pyqtSignal() + cameraImageMirrorChanged = pyqtSignal() + + def __init__(self, output_controller: MoonrakerOutputController, number_of_extruders: int = 1) -> None: + super().__init__(output_controller, number_of_extruders) + self._camera_image_rotation = "0" + self._camera_image_mirror = False + Logger.log("d", "MoonrakerOutputModel [number_of_extruders: {}] created.".format(number_of_extruders)) + + def setCameraImageRotation(self, camera_image_rotation: str) -> None: + if self._camera_image_rotation != camera_image_rotation: + self._camera_image_rotation = camera_image_rotation + self.cameraImageRotationChanged.emit() + + @pyqtProperty(str, fset = setCameraImageRotation, notify = cameraImageRotationChanged) + def cameraImageRotation(self) -> str: + return self._camera_image_rotation + + def setCameraImageMirror(self, camera_image_mirror: bool) -> None: + if self._camera_image_mirror != camera_image_mirror: + self._camera_image_mirror = camera_image_mirror + self.cameraImageMirrorChanged.emit() + + @pyqtProperty(bool, fset = setCameraImageMirror, notify = cameraImageMirrorChanged) + def cameraImageMirror(self) -> str: + return self._camera_image_mirror \ No newline at end of file diff --git a/MoonrakerConnection/plugin.json b/MoonrakerConnection/plugin.json index f5a45cc..50a03e9 100644 --- a/MoonrakerConnection/plugin.json +++ b/MoonrakerConnection/plugin.json @@ -2,6 +2,6 @@ "name": "Klipper/Moonraker Connection", "author": "emtrax", "description": "Upload and Print with Klipper via Moonraker API.", - "version": "1.6.4", - "supported_sdk_versions": ["7.3.0", "7.4.0", "7.5.0", "8.0.0"] + "version": "1.6.5", + "supported_sdk_versions": ["7.3.0", "7.4.0", "7.5.0", "7.6.0", "7.7.0", "7.8.0", "7.9.0", "8.0.0", "8.1.0", "8.2.0"] } diff --git a/MoonrakerConnection/resources/qml/qt5/MoonrakerConfiguration.qml b/MoonrakerConnection/resources/qml/qt5/MoonrakerConfiguration.qml index 0801a76..1358c00 100644 --- a/MoonrakerConnection/resources/qml/qt5/MoonrakerConfiguration.qml +++ b/MoonrakerConnection/resources/qml/qt5/MoonrakerConfiguration.qml @@ -21,6 +21,10 @@ Cura.MachineAction { return outputFormatUfp.checked ? "ufp" : "gcode" } + function cameraImageRotation() { + return cameraImageRotation90.checked ? "90" : cameraImageRotation180.checked ? "180" : cameraImageRotation270.checked ? "270" : "0" + } + function updateConfig() { manager.saveConfig({ url: urlField.text, @@ -36,7 +40,9 @@ Cura.MachineAction { trans_input: translateInputField.text, trans_output: translateOutputField.text, trans_remove: translateRemoveField.text, - camera_url: cameraUrlField.text + camera_url: cameraUrlField.text, + camera_image_rotation: cameraImageRotation(), + camera_image_mirror: cameraImageMirror.checked }) } @@ -378,7 +384,7 @@ Cura.MachineAction { text: catalog.i18nc("@label", "Automatic start of print job after upload") checked: manager.settingsUploadStartPrintJob - visible: uploadDialogOverride.checked + visible: uploadDialogBypass.checked onClicked: { updateConfig() } } Cura.CheckBox { @@ -401,7 +407,7 @@ Cura.MachineAction { font: UM.Theme.getFont("default") text: catalog.i18nc("@label", "Auto hide messagebox for successful upload (30 seconds)") checked: manager.settingsUploadAutohideMessagebox - onClicked: { updateConfig() } + onClicked: { updateConfig() } } Item { @@ -531,27 +537,90 @@ Cura.MachineAction { width: parent.width height: 10 } - RowLayout { - width: parent.width + Label { x: 15 + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("default") + text: catalog.i18nc("@label", "Camera") + renderType: Text.NativeRendering + } - Label { - text: catalog.i18nc("@label", "Camera (URL - absolute or path relative to Connection-URL)") - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - renderType: Text.NativeRendering - } + Label { + x: 25 + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("default") + text: catalog.i18nc("@label", "URL (absolute or path relative to Connection-URL)") + renderType: Text.NativeRendering } Cura.TextField { id: cameraUrlField width: parent.width - 40 - x: 25 + x: 35 text: manager.settingsCameraUrl maximumLength: 1024 onEditingFinished: { updateConfig() } } + ButtonGroup { + id: cameraImageRotationValue + } + RowLayout { + x: 25 + + Cura.RadioButton { + ButtonGroup.group: cameraImageRotationValue + + id: cameraImageRotation0 + + text: catalog.i18nc("@label", "0°") + checked: !(manager.settingsCameraImageRotation == "90" || manager.settingsCameraImageRotation == "180" || manager.settingsCameraImageRotation == "270") + onClicked: { updateConfig() } + } + Cura.RadioButton { + ButtonGroup.group: cameraImageRotationValue + + id: cameraImageRotation90 + + text: catalog.i18nc("@label", "90°") + checked: manager.settingsCameraImageRotation == "90" + onClicked: { updateConfig() } + } + Cura.RadioButton { + ButtonGroup.group: cameraImageRotationValue + + id: cameraImageRotation180 + + text: catalog.i18nc("@label", "180°") + checked: manager.settingsCameraImageRotation == "180" + onClicked: { updateConfig() } + } + Cura.RadioButton { + ButtonGroup.group: cameraImageRotationValue + + id: cameraImageRotation270 + + text: catalog.i18nc("@label", "270°") + checked: manager.settingsCameraImageRotation == "270" + onClicked: { updateConfig() } + } + Label { + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("default") + text: catalog.i18nc("@label", " Rotation") + renderType: Text.NativeRendering + } + } + Cura.CheckBox { + id: cameraImageMirror + + x: 25 + height: UM.Theme.getSize("checkbox").height + font: UM.Theme.getFont("default") + text: catalog.i18nc("@label", "Mirror") + checked: manager.settingsCameraImageMirror + onClicked: { updateConfig() } + } } } diff --git a/MoonrakerConnection/resources/qml/qt5/MoonrakerMonitor.qml b/MoonrakerConnection/resources/qml/qt5/MoonrakerMonitor.qml index 644ccef..41409cf 100644 --- a/MoonrakerConnection/resources/qml/qt5/MoonrakerMonitor.qml +++ b/MoonrakerConnection/resources/qml/qt5/MoonrakerMonitor.qml @@ -29,6 +29,8 @@ Component { radius: UM.Theme.getSize("default_radius").width cornerSide: Cura.RoundedRectangle.Direction.Left + property bool cameraConfigured: OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null && OutputDevice.activePrinter.cameraUrl != "" + Label { id: cameraLabel @@ -36,48 +38,57 @@ Component { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; } - color: UM.Theme.getColor(OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null && OutputDevice.activePrinter.cameraUrl != "" ? "text" : "text_inactive") + color: UM.Theme.getColor(parent.cameraConfigured ? "text" : "text_inactive") font: UM.Theme.getFont("large_bold") - text: OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null && OutputDevice.activePrinter.cameraUrl != "" ? "Camera" : "Camera not configured" + text: parent.cameraConfigured ? "Camera" : "Camera not configured" } Label { id: cameraLabelUrl - visible: OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null && OutputDevice.activePrinter.cameraUrl != "" + visible: parent.cameraConfigured anchors { horizontalCenter: cameraLabel.horizontalCenter; top: cameraLabel.bottom; } color: UM.Theme.getColor("text_inactive") font: UM.Theme.getFont("small") - text: "Url: " + (OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null ? OutputDevice.activePrinter.cameraUrl : "Null") + text: "Url: " + (parent.cameraConfigured ? OutputDevice.activePrinter.cameraUrl : "None") } - Cura.NetworkMJPGImage { - property real scale: Math.min(Math.min((parent.width - 2 * UM.Theme.getSize("default_margin").width) / imageWidth, (parent.height - 2 * UM.Theme.getSize("default_margin").height) / imageHeight), 2); + Cura.NetworkMJPGImage { + property bool imageRotated: OutputDevice.activePrinter.cameraImageRotation == "90" || OutputDevice.activePrinter.cameraImageRotation == "270" + property real maxViewWidth: parent.width - 2 * UM.Theme.getSize("default_margin").width + property real maxViewHeight: parent.height - 2 * UM.Theme.getSize("default_margin").height + property real scaleFactor: { + if (imageRotated) { + Math.min(Math.min(maxViewWidth / imageHeight, maxViewHeight / imageWidth), 2) + } else { + Math.min(Math.min(maxViewWidth / imageWidth, maxViewHeight / imageHeight), 2) + } + } id: cameraImage; anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; } - width: Math.floor(imageWidth * scale) - height: Math.floor(imageHeight * scale) - source: OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null ? OutputDevice.activePrinter.cameraUrl : "" + width: Math.floor(imageWidth * scaleFactor) + height: Math.floor(imageHeight * scaleFactor) + source: parent.cameraConfigured ? OutputDevice.activePrinter.cameraUrl : "" + rotation: OutputDevice.activePrinter.cameraImageRotation + mirror: OutputDevice.activePrinter.cameraImageMirror onVisibleChanged: { - if (visible) { - if (OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null) { - cameraImage.start(); - } - } else { - if (OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null) { - cameraImage.stop(); + if (source != "") { + if (visible) { + start(); + } else { + stop(); } } } Component.onCompleted: { - if (OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null) { - cameraImage.start(); + if (source != "") { + start(); } } } diff --git a/MoonrakerConnection/resources/qml/qt6/MoonrakerConfiguration.qml b/MoonrakerConnection/resources/qml/qt6/MoonrakerConfiguration.qml index f5c5fc6..d79ed4e 100644 --- a/MoonrakerConnection/resources/qml/qt6/MoonrakerConfiguration.qml +++ b/MoonrakerConnection/resources/qml/qt6/MoonrakerConfiguration.qml @@ -21,6 +21,10 @@ Cura.MachineAction { return outputFormatUfp.checked ? "ufp" : "gcode" } + function cameraImageRotation() { + return cameraImageRotation90.checked ? "90" : cameraImageRotation180.checked ? "180" : cameraImageRotation270.checked ? "270" : "0" + } + function updateConfig() { manager.saveConfig({ url: urlField.text, @@ -36,7 +40,9 @@ Cura.MachineAction { trans_input: translateInputField.text, trans_output: translateOutputField.text, trans_remove: translateRemoveField.text, - camera_url: cameraUrlField.text + camera_url: cameraUrlField.text, + camera_image_rotation: cameraImageRotation(), + camera_image_mirror: cameraImageMirror.checked }) } @@ -349,7 +355,7 @@ Cura.MachineAction { x: 25 text: catalog.i18nc("@label", "Automatic start of print job after upload") checked: manager.settingsUploadStartPrintJob - visible: uploadDialogOverride.checked + visible: uploadDialogBypass.checked onClicked: { updateConfig() } } UM.CheckBox { @@ -483,24 +489,79 @@ Cura.MachineAction { width: parent.width height: 10 } - RowLayout { - width: parent.width + UM.Label { x: 15 + text: catalog.i18nc("@label", "Camera") + } - UM.Label { - text: catalog.i18nc("@label", "Camera (URL - absolute or path relative to Connection-URL)") - } + UM.Label { + x: 25 + text: catalog.i18nc("@label", "URL (absolute or path relative to Connection-URL)") } Cura.TextField { id: cameraUrlField width: parent.width - 40 - x: 25 + x: 35 text: manager.settingsCameraUrl maximumLength: 1024 onEditingFinished: { updateConfig() } } + ButtonGroup { + id: cameraImageRotationValue + } + RowLayout { + x: 25 + + Cura.RadioButton { + ButtonGroup.group: cameraImageRotationValue + + id: cameraImageRotation0 + + text: catalog.i18nc("@label", "0°") + checked: !(manager.settingsCameraImageRotation == "90" || manager.settingsCameraImageRotation == "180" || manager.settingsCameraImageRotation == "270") + onClicked: { updateConfig() } + } + Cura.RadioButton { + ButtonGroup.group: cameraImageRotationValue + + id: cameraImageRotation90 + + text: catalog.i18nc("@label", "90°") + checked: manager.settingsCameraImageRotation == "90" + onClicked: { updateConfig() } + } + Cura.RadioButton { + ButtonGroup.group: cameraImageRotationValue + + id: cameraImageRotation180 + + text: catalog.i18nc("@label", "180°") + checked: manager.settingsCameraImageRotation == "180" + onClicked: { updateConfig() } + } + Cura.RadioButton { + ButtonGroup.group: cameraImageRotationValue + + id: cameraImageRotation270 + + text: catalog.i18nc("@label", "270°") + checked: manager.settingsCameraImageRotation == "270" + onClicked: { updateConfig() } + } + UM.Label { + text: catalog.i18nc("@label", " Rotation") + } + } + UM.CheckBox { + id: cameraImageMirror + + x: 25 + text: catalog.i18nc("@label", "Mirror") + checked: manager.settingsCameraImageMirror + onClicked: { updateConfig() } + } } } diff --git a/MoonrakerConnection/resources/qml/qt6/MoonrakerMonitor.qml b/MoonrakerConnection/resources/qml/qt6/MoonrakerMonitor.qml index 036a711..255dc6c 100644 --- a/MoonrakerConnection/resources/qml/qt6/MoonrakerMonitor.qml +++ b/MoonrakerConnection/resources/qml/qt6/MoonrakerMonitor.qml @@ -29,55 +29,66 @@ Component { radius: UM.Theme.getSize("default_radius").width cornerSide: Cura.RoundedRectangle.Direction.Left - UM.Label { + property bool cameraConfigured: OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null && OutputDevice.activePrinter.cameraUrl != "" + + UM.Label { id: cameraLabel anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; } - color: UM.Theme.getColor(OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null && OutputDevice.activePrinter.cameraUrl != "" ? "text" : "text_inactive") + color: UM.Theme.getColor(parent.cameraConfigured ? "text" : "text_inactive") font: UM.Theme.getFont("large_bold") - text: OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null && OutputDevice.activePrinter.cameraUrl != "" ? "Camera" : "Camera not configured" + text: parent.cameraConfigured ? "Camera" : "Camera not configured" } UM.Label { id: cameraLabelUrl - visible: OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null && OutputDevice.activePrinter.cameraUrl != "" + visible: parent.cameraConfigured anchors { horizontalCenter: cameraLabel.horizontalCenter; top: cameraLabel.bottom; } color: UM.Theme.getColor("text_inactive") font: UM.Theme.getFont("small") - text: "Url: " + (OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null ? OutputDevice.activePrinter.cameraUrl : "Null") + text: "Url: " + (parent.cameraConfigured ? OutputDevice.activePrinter.cameraUrl : "None") } - Cura.NetworkMJPGImage { - property real scale: Math.min(Math.min((parent.width - 2 * UM.Theme.getSize("default_margin").width) / imageWidth, (parent.height - 2 * UM.Theme.getSize("default_margin").height) / imageHeight), 2); + Cura.NetworkMJPGImage { + property bool imageRotated: OutputDevice.activePrinter.cameraImageRotation == "90" || OutputDevice.activePrinter.cameraImageRotation == "270" + property real maxViewWidth: parent.width - 2 * UM.Theme.getSize("default_margin").width + property real maxViewHeight: parent.height - 2 * UM.Theme.getSize("default_margin").height + property real scaleFactor: { + if (imageRotated) { + Math.min(Math.min(maxViewWidth / imageHeight, maxViewHeight / imageWidth), 2) + } else { + Math.min(Math.min(maxViewWidth / imageWidth, maxViewHeight / imageHeight), 2) + } + } id: cameraImage; anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; } - width: Math.floor(imageWidth * scale) - height: Math.floor(imageHeight * scale) - source: OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null ? OutputDevice.activePrinter.cameraUrl : "" + width: Math.floor(imageWidth * scaleFactor) + height: Math.floor(imageHeight * scaleFactor) + source: parent.cameraConfigured ? OutputDevice.activePrinter.cameraUrl : "" + rotation: OutputDevice.activePrinter.cameraImageRotation + mirror: OutputDevice.activePrinter.cameraImageMirror onVisibleChanged: { - if (visible) { - if (OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null) { - cameraImage.start(); - } - } else { - if (OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null) { - cameraImage.stop(); + if (source != "") { + if (visible) { + start(); + } else { + stop(); } } } Component.onCompleted: { - if (OutputDevice.activePrinter != null && OutputDevice.activePrinter.cameraUrl != null) { - cameraImage.start(); + if (source != "") { + start(); } } }