Merge pull request #7873 from Ultimaker/CURA-7438_Show_cloud_connection_not_available_printer_removed_from_account

CURA-7438 Handle the case when a cloud printer is removed from the account
This commit is contained in:
Nino van Hooff 2020-06-10 11:46:10 +02:00 committed by GitHub
commit feeeb972f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 137 additions and 30 deletions

View file

@ -10,7 +10,7 @@ from UM.Message import Message
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings from cura.OAuth2.Models import OAuth2Settings
from cura.UltimakerCloud import UltimakerCloudAuthentication from cura.UltimakerCloud import UltimakerCloudConstants
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
@ -69,7 +69,7 @@ class Account(QObject):
self._last_sync_str = "-" self._last_sync_str = "-"
self._callback_port = 32118 self._callback_port = 32118
self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot self._oauth_root = UltimakerCloudConstants.CuraCloudAccountAPIRoot
self._oauth_settings = OAuth2Settings( self._oauth_settings = OAuth2Settings(
OAUTH_SERVER_URL= self._oauth_root, OAUTH_SERVER_URL= self._oauth_root,

View file

@ -4,14 +4,14 @@ from PyQt5.QtCore import QObject, pyqtSignal, QTimer, pyqtProperty
from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtNetwork import QNetworkReply
from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from cura.UltimakerCloud import UltimakerCloudAuthentication from cura.UltimakerCloud import UltimakerCloudConstants
class ConnectionStatus(QObject): class ConnectionStatus(QObject):
"""Status info for some web services""" """Status info for some web services"""
UPDATE_INTERVAL = 10.0 # seconds UPDATE_INTERVAL = 10.0 # seconds
ULTIMAKER_CLOUD_STATUS_URL = UltimakerCloudAuthentication.CuraCloudAPIRoot + "/connect/v1/" ULTIMAKER_CLOUD_STATUS_URL = UltimakerCloudConstants.CuraCloudAPIRoot + "/connect/v1/"
__instance = None # type: Optional[ConnectionStatus] __instance = None # type: Optional[ConnectionStatus]

View file

@ -106,7 +106,7 @@ from cura.UI.RecommendedMode import RecommendedMode
from cura.UI.TextManager import TextManager from cura.UI.TextManager import TextManager
from cura.UI.WelcomePagesModel import WelcomePagesModel from cura.UI.WelcomePagesModel import WelcomePagesModel
from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel
from cura.UltimakerCloud import UltimakerCloudAuthentication from cura.UltimakerCloud import UltimakerCloudConstants
from cura.Utils.NetworkingUtil import NetworkingUtil from cura.Utils.NetworkingUtil import NetworkingUtil
from . import BuildVolume from . import BuildVolume
from . import CameraAnimation from . import CameraAnimation
@ -255,11 +255,11 @@ class CuraApplication(QtApplication):
@pyqtProperty(str, constant=True) @pyqtProperty(str, constant=True)
def ultimakerCloudApiRootUrl(self) -> str: def ultimakerCloudApiRootUrl(self) -> str:
return UltimakerCloudAuthentication.CuraCloudAPIRoot return UltimakerCloudConstants.CuraCloudAPIRoot
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def ultimakerCloudAccountRootUrl(self) -> str: def ultimakerCloudAccountRootUrl(self) -> str:
return UltimakerCloudAuthentication.CuraCloudAccountAPIRoot return UltimakerCloudConstants.CuraCloudAccountAPIRoot
def addCommandLineOptions(self): def addCommandLineOptions(self):
"""Adds command line options to the command line parser. """Adds command line options to the command line parser.

View file

@ -22,6 +22,7 @@ from UM.Settings.SettingFunction import SettingFunction
from UM.Signal import postponeSignals, CompressTechnique from UM.Signal import postponeSignals, CompressTechnique
import cura.CuraApplication # Imported like this to prevent circular references. import cura.CuraApplication # Imported like this to prevent circular references.
from UM.Util import parseBool
from cura.Machines.ContainerNode import ContainerNode from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.ContainerTree import ContainerTree from cura.Machines.ContainerTree import ContainerTree
@ -37,6 +38,7 @@ from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.cura_empty_instance_containers import (empty_definition_changes_container, empty_variant_container, from cura.Settings.cura_empty_instance_containers import (empty_definition_changes_container, empty_variant_container,
empty_material_container, empty_quality_container, empty_material_container, empty_quality_container,
empty_quality_changes_container, empty_intent_container) empty_quality_changes_container, empty_intent_container)
from cura.UltimakerCloud.UltimakerCloudConstants import META_UM_LINKED_TO_ACCOUNT
from .CuraStackBuilder import CuraStackBuilder from .CuraStackBuilder import CuraStackBuilder
@ -494,6 +496,10 @@ class MachineManager(QObject):
group_size = int(self.activeMachine.getMetaDataEntry("group_size", "-1")) group_size = int(self.activeMachine.getMetaDataEntry("group_size", "-1"))
return group_size > 1 return group_size > 1
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
def activeMachineIsLinkedToCurrentAccount(self) -> bool:
return parseBool(self.activeMachine.getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "True"))
@pyqtProperty(bool, notify = printerConnectedStatusChanged) @pyqtProperty(bool, notify = printerConnectedStatusChanged)
def activeMachineHasNetworkConnection(self) -> bool: def activeMachineHasNetworkConnection(self) -> bool:
# A network connection is only available if any output device is actually a network connected device. # A network connection is only available if any output device is actually a network connected device.

View file

@ -8,6 +8,10 @@ DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
DEFAULT_CLOUD_API_VERSION = "1" # type: str DEFAULT_CLOUD_API_VERSION = "1" # type: str
DEFAULT_CLOUD_ACCOUNT_API_ROOT = "https://account.ultimaker.com" # type: str DEFAULT_CLOUD_ACCOUNT_API_ROOT = "https://account.ultimaker.com" # type: str
# Container Metadata keys
META_UM_LINKED_TO_ACCOUNT = "um_linked_to_account"
"""(bool) Whether a cloud printer is linked to an Ultimaker account"""
try: try:
from cura.CuraVersion import CuraCloudAPIRoot # type: ignore from cura.CuraVersion import CuraCloudAPIRoot # type: ignore
if CuraCloudAPIRoot == "": if CuraCloudAPIRoot == "":

View file

@ -1,13 +1,13 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from cura.UltimakerCloud import UltimakerCloudAuthentication from cura.UltimakerCloud import UltimakerCloudConstants
class Settings: class Settings:
# Keeps the plugin settings. # Keeps the plugin settings.
DRIVE_API_VERSION = 1 DRIVE_API_VERSION = 1
DRIVE_API_URL = "{}/cura-drive/v{}".format(UltimakerCloudAuthentication.CuraCloudAPIRoot, str(DRIVE_API_VERSION)) DRIVE_API_URL = "{}/cura-drive/v{}".format(UltimakerCloudConstants.CuraCloudAPIRoot, str(DRIVE_API_VERSION))
AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled" AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled"
AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date" AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date"

View file

@ -1,13 +1,13 @@
from typing import Union from typing import Union
from cura import ApplicationMetadata from cura import ApplicationMetadata
from cura.UltimakerCloud import UltimakerCloudAuthentication from cura.UltimakerCloud import UltimakerCloudConstants
class CloudApiModel: class CloudApiModel:
sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int] sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: str cloud_api_version = UltimakerCloudConstants.CuraCloudAPIVersion # type: str
cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str cloud_api_root = UltimakerCloudConstants.CuraCloudAPIRoot # type: str
api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format( api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
cloud_api_root = cloud_api_root, cloud_api_root = cloud_api_root,
cloud_api_version = cloud_api_version, cloud_api_version = cloud_api_version,

View file

@ -13,7 +13,7 @@ from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from cura.API import Account from cura.API import Account
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.UltimakerCloud import UltimakerCloudAuthentication from cura.UltimakerCloud import UltimakerCloudConstants
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from .ToolPathUploader import ToolPathUploader from .ToolPathUploader import ToolPathUploader
from ..Models.BaseModel import BaseModel from ..Models.BaseModel import BaseModel
@ -35,7 +35,7 @@ class CloudApiClient:
""" """
# The cloud URL to use for this remote cluster. # The cloud URL to use for this remote cluster.
ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot ROOT_PATH = UltimakerCloudConstants.CuraCloudAPIRoot
CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH) CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH)
CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH) CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH)

View file

@ -1,9 +1,8 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
from typing import Dict, List, Optional from typing import Dict, List, Optional, Set
from PyQt5.QtCore import QTimer
from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtNetwork import QNetworkReply
from UM import i18nCatalog from UM import i18nCatalog
@ -11,11 +10,13 @@ from UM.Logger import Logger # To log errors talking to the API.
from UM.Message import Message from UM.Message import Message
from UM.Settings.Interfaces import ContainerInterface from UM.Settings.Interfaces import ContainerInterface
from UM.Signal import Signal from UM.Signal import Signal
from UM.Util import parseBool
from cura.API import Account from cura.API import Account
from cura.API.Account import SyncState from cura.API.Account import SyncState
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Settings.CuraStackBuilder import CuraStackBuilder from cura.Settings.CuraStackBuilder import CuraStackBuilder
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from cura.UltimakerCloud.UltimakerCloudConstants import META_UM_LINKED_TO_ACCOUNT
from .CloudApiClient import CloudApiClient from .CloudApiClient import CloudApiClient
from .CloudOutputDevice import CloudOutputDevice from .CloudOutputDevice import CloudOutputDevice
from ..Models.Http.CloudClusterResponse import CloudClusterResponse from ..Models.Http.CloudClusterResponse import CloudClusterResponse
@ -30,6 +31,7 @@ class CloudOutputDeviceManager:
META_CLUSTER_ID = "um_cloud_cluster_id" META_CLUSTER_ID = "um_cloud_cluster_id"
META_NETWORK_KEY = "um_network_key" META_NETWORK_KEY = "um_network_key"
SYNC_SERVICE_NAME = "CloudOutputDeviceManager" SYNC_SERVICE_NAME = "CloudOutputDeviceManager"
# The translation catalog for this device. # The translation catalog for this device.
@ -41,6 +43,10 @@ class CloudOutputDeviceManager:
def __init__(self) -> None: def __init__(self) -> None:
# Persistent dict containing the remote clusters for the authenticated user. # Persistent dict containing the remote clusters for the authenticated user.
self._remote_clusters = {} # type: Dict[str, CloudOutputDevice] self._remote_clusters = {} # type: Dict[str, CloudOutputDevice]
# Dictionary containing all the cloud printers loaded in Cura
self._um_cloud_printers = {} # type: Dict[str, GlobalStack]
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
self._api = CloudApiClient(CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error))) self._api = CloudApiClient(CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error)))
self._account.loginStateChanged.connect(self._onLoginStateChanged) self._account.loginStateChanged.connect(self._onLoginStateChanged)
@ -98,23 +104,36 @@ class CloudOutputDeviceManager:
def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None: def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None:
"""Callback for when the request for getting the clusters is finished.""" """Callback for when the request for getting the clusters is finished."""
self._um_cloud_printers = {m.getMetaDataEntry(self.META_CLUSTER_ID): m for m in
CuraApplication.getInstance().getContainerRegistry().findContainerStacks(
type = "machine") if m.getMetaDataEntry(self.META_CLUSTER_ID, None)}
new_clusters = [] new_clusters = []
all_clusters = {c.cluster_id: c for c in clusters} # type: Dict[str, CloudClusterResponse] all_clusters = {c.cluster_id: c for c in clusters} # type: Dict[str, CloudClusterResponse]
online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse] online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse]
# Add the new printers in Cura. If a printer was previously added and is rediscovered, set its metadata to
# reflect that and mark the printer not removed from the account
for device_id, cluster_data in all_clusters.items(): for device_id, cluster_data in all_clusters.items():
if device_id not in self._remote_clusters: if device_id not in self._remote_clusters:
new_clusters.append(cluster_data) new_clusters.append(cluster_data)
if device_id in self._um_cloud_printers and not parseBool(self._um_cloud_printers[device_id].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")):
self._um_cloud_printers[device_id].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
self._onDevicesDiscovered(new_clusters) self._onDevicesDiscovered(new_clusters)
removed_device_keys = set(self._remote_clusters.keys()) - set(online_clusters.keys()) # Remove the CloudOutput device for offline printers
for device_id in removed_device_keys: offline_device_keys = set(self._remote_clusters.keys()) - set(online_clusters.keys())
for device_id in offline_device_keys:
self._onDiscoveredDeviceRemoved(device_id) self._onDiscoveredDeviceRemoved(device_id)
if new_clusters or removed_device_keys: # Handle devices that were previously added in Cura but do not exist in the account anymore (i.e. they were
self.discoveredDevicesChanged.emit() # removed from the account)
removed_device_keys = set(self._um_cloud_printers.keys()) - set(all_clusters.keys())
if removed_device_keys: if removed_device_keys:
self._devicesRemovedFromAccount(removed_device_keys)
if new_clusters or offline_device_keys or removed_device_keys:
self.discoveredDevicesChanged.emit()
if offline_device_keys:
# If the removed device was active we should connect to the new active device # If the removed device was active we should connect to the new active device
self._connectToActiveMachine() self._connectToActiveMachine()
@ -144,10 +163,13 @@ class CloudOutputDeviceManager:
if machine_manager.getMachine(device.printerType, {self.META_CLUSTER_ID: device.key}) is None \ if machine_manager.getMachine(device.printerType, {self.META_CLUSTER_ID: device.key}) is None \
and machine_manager.getMachine(device.printerType, {self.META_NETWORK_KEY: cluster_data.host_name + "*"}) is None: # The host name is part of the network key. and machine_manager.getMachine(device.printerType, {self.META_NETWORK_KEY: cluster_data.host_name + "*"}) is None: # The host name is part of the network key.
new_devices.append(device) new_devices.append(device)
elif device.getId() not in self._remote_clusters: elif device.getId() not in self._remote_clusters:
self._remote_clusters[device.getId()] = device self._remote_clusters[device.getId()] = device
remote_clusters_added = True remote_clusters_added = True
# If a printer that was removed from the account is re-added, change its metadata to mark it not removed
# from the account
elif not parseBool(self._um_cloud_printers[device.key].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")):
self._um_cloud_printers[device.key].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
# Inform the Cloud printers model about new devices. # Inform the Cloud printers model about new devices.
new_devices_list_of_dicts = [{ new_devices_list_of_dicts = [{
@ -208,19 +230,86 @@ class CloudOutputDeviceManager:
max_disp_devices = 3 max_disp_devices = 3
if len(new_devices) > max_disp_devices: if len(new_devices) > max_disp_devices:
num_hidden = len(new_devices) - max_disp_devices + 1 num_hidden = len(new_devices) - max_disp_devices + 1
device_name_list = ["- {} ({})".format(device.name, device.printerTypeName) for device in new_devices[0:num_hidden]] device_name_list = ["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices[0:num_hidden]]
device_name_list.append(self.I18N_CATALOG.i18nc("info:hidden list items", "- and {} others", num_hidden)) device_name_list.append(self.I18N_CATALOG.i18nc("info:hidden list items", "<li>... and {} others</li>", num_hidden))
device_names = "\n".join(device_name_list) device_names = "\n".join(device_name_list)
else: else:
device_names = "\n".join(["- {} ({})".format(device.name, device.printerTypeName) for device in new_devices]) device_names = "\n".join(["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices])
message_text = self.I18N_CATALOG.i18nc( message_text = self.I18N_CATALOG.i18nc(
"info:status", "info:status",
"Cloud printers added from your account:\n{}", "Cloud printers added from your account:\n<ul>{}</ul>",
device_names device_names
) )
message.setText(message_text) message.setText(message_text)
def _devicesRemovedFromAccount(self, removed_device_ids: Set[str]) -> None:
"""
Removes the CloudOutputDevice from the received device ids and marks the specific printers as "removed from
account". In addition, it generates a message to inform the user about the printers that are no longer linked to
his/her account. The message is not generated if all the printers have been previously reported as not linked
to the account.
:param removed_device_ids: Set of device ids, whose CloudOutputDevice needs to be removed
:return: None
"""
if not CuraApplication.getInstance().getCuraAPI().account.isLoggedIn:
return
# Do not report device ids which have been previously marked as non-linked to the account
ignored_device_ids = set()
for device_id in removed_device_ids:
if not parseBool(self._um_cloud_printers[device_id].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")):
ignored_device_ids.add(device_id)
# Keep the reported_device_ids list in a class variable, so that the message button actions can access it and
# take the necessary steps to fulfill their purpose.
self.reported_device_ids = removed_device_ids - ignored_device_ids
if not self.reported_device_ids:
return
# Generate message
removed_printers_message = Message(
title = self.I18N_CATALOG.i18ncp(
"info:status",
"Cloud connection is not available for a printer",
"Cloud connection is not available for some printers",
len(self.reported_device_ids)
),
lifetime = 0
)
device_names = "\n".join(["<li>{} ({})</li>".format(self._um_cloud_printers[device].name, self._um_cloud_printers[device].definition.name) for device in self.reported_device_ids])
message_text = self.I18N_CATALOG.i18ncp(
"info:status",
"The following cloud printer is not linked to your account:\n",
"The following cloud printers are not linked to your account:\n",
len(self.reported_device_ids)
)
message_text += self.I18N_CATALOG.i18nc(
"info:status",
"<ul>{}</ul>\nTo establish a connection, please visit the "
"<a href='https://mycloud.ultimaker.com/'>Ultimaker Digital Factory</a>.",
device_names
)
removed_printers_message.setText(message_text)
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
# Remove the output device from the printers
for device_id in removed_device_ids:
device = self._um_cloud_printers.get(device_id, None) # type: Optional[GlobalStack]
if not device:
continue
if device_id in output_device_manager.getOutputDeviceIds():
output_device_manager.removeOutputDevice(device_id)
if device_id in self._remote_clusters:
del self._remote_clusters[device_id]
# Update the printer's metadata to mark it as not linked to the account
device.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, False)
removed_printers_message.show()
def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: def _onDiscoveredDeviceRemoved(self, device_id: str) -> None:
device = self._remote_clusters.pop(device_id, None) # type: Optional[CloudOutputDevice] device = self._remote_clusters.pop(device_id, None) # type: Optional[CloudOutputDevice]
if not device: if not device:

View file

@ -35,14 +35,21 @@ Cura.ExpandablePopup
} }
} }
readonly property string connectionStatusMessage: { function getConnectionStatusMessage() {
if (connectionStatus == "printer_cloud_not_available") if (connectionStatus == "printer_cloud_not_available")
{ {
if(Cura.API.connectionStatus.isInternetReachable) if(Cura.API.connectionStatus.isInternetReachable)
{ {
if (Cura.API.account.isLoggedIn) if (Cura.API.account.isLoggedIn)
{ {
return catalog.i18nc("@status", "The cloud printer is offline. Please check if the printer is turned on and connected to the internet.") if (Cura.MachineManager.activeMachineIsLinkedToCurrentAccount)
{
return catalog.i18nc("@status", "The cloud printer is offline. Please check if the printer is turned on and connected to the internet.")
}
else
{
return catalog.i18nc("@status", "This printer is not linked to your account. Please visit the Ultimaker Digital Factory to establish a connection.")
}
} }
else else
{ {
@ -139,12 +146,13 @@ Cura.ExpandablePopup
{ {
id: connectionStatusTooltipHoverArea id: connectionStatusTooltipHoverArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: connectionStatusMessage !== "" hoverEnabled: getConnectionStatusMessage() !== ""
acceptedButtons: Qt.NoButton // react to hover only, don't steal clicks acceptedButtons: Qt.NoButton // react to hover only, don't steal clicks
onEntered: onEntered:
{ {
machineSelector.mouseArea.entered() // we want both this and the outer area to be entered machineSelector.mouseArea.entered() // we want both this and the outer area to be entered
tooltip.tooltipText = getConnectionStatusMessage()
tooltip.show() tooltip.show()
} }
onExited: { tooltip.hide() } onExited: { tooltip.hide() }
@ -155,7 +163,7 @@ Cura.ExpandablePopup
id: tooltip id: tooltip
width: 250 * screenScaleFactor width: 250 * screenScaleFactor
tooltipText: connectionStatusMessage tooltipText: getConnectionStatusMessage()
arrowSize: UM.Theme.getSize("button_tooltip_arrow").width arrowSize: UM.Theme.getSize("button_tooltip_arrow").width
x: connectionStatusImage.x - UM.Theme.getSize("narrow_margin").width x: connectionStatusImage.x - UM.Theme.getSize("narrow_margin").width
y: connectionStatusImage.y + connectionStatusImage.height + UM.Theme.getSize("narrow_margin").height y: connectionStatusImage.y + connectionStatusImage.height + UM.Theme.getSize("narrow_margin").height