Browse Source

Merge branch 'master' into workaround_kde_qqc2_crash

StefanBruens 4 years ago
parent
commit
b800815378

+ 1 - 1
.github/ISSUE_TEMPLATE/bug-report.md

@@ -40,7 +40,7 @@ Thank you for using Cura!
 (What should happen after the above steps have been followed.)
 
 **Project file**
-(For slicing bugs, provide a project which clearly shows the bug, by going to File->Save. For big files you may need to use WeTransfer or similar file sharing sites.)
+(For slicing bugs, provide a project which clearly shows the bug, by going to File->Save Project. For big files you may need to use WeTransfer or similar file sharing sites. G-code files are not project files!)
 
 **Log file**
 (See https://github.com/Ultimaker/Cura#logging-issues to find the log file to upload, or copy a relevant snippet from it.)

+ 13 - 0
.github/no-response.yml

@@ -0,0 +1,13 @@
+# Configuration for probot-no-response - https://github.com/probot/no-response
+
+# Number of days of inactivity before an Issue is closed for lack of response
+daysUntilClose: 14
+# Label requiring a response
+responseRequiredLabel: 'Status: Needs Info'
+# Comment to post when closing an Issue for lack of response. Set to `false` to disable
+closeComment: >
+  This issue has been automatically closed because there has been no response
+  to our request for more information from the original author. With only the
+  information that is currently in the issue, we don't have enough information
+  to take action. Please reach out if you have or find the answers we need so
+  that we can investigate further.

+ 4 - 3
.github/workflows/cicd.yml → .github/workflows/ci.yml

@@ -1,5 +1,5 @@
 ---
-name: CI/CD
+name: CI
 on:
   push:
     branches:
@@ -10,11 +10,12 @@ on:
   pull_request:
 jobs:
   build:
-    name: Build and test
     runs-on: ubuntu-latest
     container: ultimaker/cura-build-environment
     steps:
     - name: Checkout Cura
       uses: actions/checkout@v2
-    - name: Build and test
+    - name: Build
       run: docker/build.sh
+    - name: Test
+      run: docker/test.sh

+ 4 - 0
.gitignore

@@ -53,6 +53,8 @@ plugins/GodMode
 plugins/OctoPrintPlugin
 plugins/ProfileFlattener
 plugins/SettingsGuide
+plugins/SettingsGuide2
+plugins/SVGToolpathReader
 plugins/X3GWriter
 
 #Build stuff
@@ -76,3 +78,5 @@ CuraEngine
 
 #Prevents import failures when plugin running tests
 plugins/__init__.py
+
+/venv

+ 16 - 19
CMakeLists.txt

@@ -5,8 +5,8 @@ include(GNUInstallDirs)
 
 list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
 
-set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository")
-set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
+set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE PATH "The location of the Uranium repository")
+set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE PATH "The location of the scripts directory of the Uranium repository")
 
 # Tests
 include(CuraTests)
@@ -16,6 +16,8 @@ if(CURA_DEBUGMODE)
     set(_cura_debugmode "ON")
 endif()
 
+option(GENERATE_TRANSLATIONS "Should the translations be generated?" ON)
+
 set(CURA_APP_NAME "cura" CACHE STRING "Short name of Cura, used for configuration folder")
 set(CURA_APP_DISPLAY_NAME "Ultimaker Cura" CACHE STRING "Display name of Cura")
 set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
@@ -24,30 +26,23 @@ set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
 set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
 set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version")
 set(CURA_MARKETPLACE_ROOT "" CACHE STRING "Alternative Marketplace location")
+set(CURA_DIGITAL_FACTORY_URL "" CACHE STRING "Alternative Digital Factory location")
 
 configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
 
 configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
 
 
-# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
-# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
-# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
-if(${CMAKE_VERSION} VERSION_LESS 3.12)
-    # Use FindPythonInterp and FindPythonLibs for CMake <3.12
-    find_package(PythonInterp 3 REQUIRED)
+# FIXME: The new FindPython3 finds the system's Python3.6 reather than the Python3.5 that we built for Cura's environment.
+# So we're using the old method here, with FindPythonInterp for now.
+find_package(PythonInterp 3 REQUIRED)
 
-    set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
-
-    set(Python3_VERSION ${PYTHON_VERSION_STRING})
-    set(Python3_VERSION_MAJOR ${PYTHON_VERSION_MAJOR})
-    set(Python3_VERSION_MINOR ${PYTHON_VERSION_MINOR})
-    set(Python3_VERSION_PATCH ${PYTHON_VERSION_PATCH})
-else()
-    # Use FindPython3 for CMake >=3.12
-    find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
-endif()
+set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
 
+set(Python3_VERSION ${PYTHON_VERSION_STRING})
+set(Python3_VERSION_MAJOR ${PYTHON_VERSION_MAJOR})
+set(Python3_VERSION_MINOR ${PYTHON_VERSION_MINOR})
+set(Python3_VERSION_PATCH ${PYTHON_VERSION_PATCH})
 
 if(NOT ${URANIUM_DIR} STREQUAL "")
     set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${URANIUM_DIR}/cmake")
@@ -58,7 +53,9 @@ if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
     # Extract Strings
     add_custom_target(extract-messages ${URANIUM_SCRIPTS_DIR}/extract-messages ${CMAKE_SOURCE_DIR} cura)
     # Build Translations
-    CREATE_TRANSLATION_TARGETS()
+    if(${GENERATE_TRANSLATIONS})
+        CREATE_TRANSLATION_TARGETS()
+    endif()
 endif()
 
 

+ 3 - 1
README.md

@@ -1,6 +1,8 @@
 Cura
 ====
-This is the new, shiny frontend for Cura. Check [daid/LegacyCura](https://github.com/daid/LegacyCura) for the legacy Cura that everyone knows and loves/hates. We re-worked the whole GUI code at Ultimaker, because the old code started to become unmaintainable.
+Ultimaker Cura is a state-of-the-art slicer application to prepare your 3D models for printing with a 3D printer. With hundreds of settings and hundreds of community-managed print profiles, Ultimaker Cura is sure to lead your next project to a success.
+
+![Screenshot](screenshot.png)
 
 Logging Issues
 ------------

+ 8 - 2
cmake/CuraPluginInstall.cmake

@@ -9,6 +9,8 @@
 # form of "a;b;c" or "a,b,c". By default all plugins will be installed.
 #
 
+option(PRINT_PLUGIN_LIST "Should the list of plugins that are installed be printed?" ON)
+
 # FIXME: Remove the code for CMake <3.12 once we have switched over completely.
 # FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
 # module is copied from the CMake repository here so in CMake <3.12 we can still use it.
@@ -81,7 +83,9 @@ foreach(_plugin_json_path ${_plugin_json_list})
     endif()
 
     if(_add_plugin)
-        message(STATUS "[+] PLUGIN TO INSTALL: ${_rel_plugin_dir}")
+        if(${PRINT_PLUGIN_LIST})
+            message(STATUS "[+] PLUGIN TO INSTALL: ${_rel_plugin_dir}")
+        endif()
         get_filename_component(_rel_plugin_parent_dir ${_rel_plugin_dir} DIRECTORY)
         install(DIRECTORY ${_rel_plugin_dir}
                 DESTINATION lib${LIB_SUFFIX}/cura/${_rel_plugin_parent_dir}
@@ -90,7 +94,9 @@ foreach(_plugin_json_path ${_plugin_json_list})
                 )
         list(APPEND _install_plugin_list ${_plugin_dir})
     elseif(_is_no_install_plugin)
-        message(STATUS "[-] PLUGIN TO REMOVE : ${_rel_plugin_dir}")
+        if(${PRINT_PLUGIN_LIST})
+            message(STATUS "[-] PLUGIN TO REMOVE : ${_rel_plugin_dir}")
+        endif()
         execute_process(COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/mod_bundled_packages_json.py
                         -d ${CMAKE_CURRENT_SOURCE_DIR}/resources/bundled_packages
                         ${_plugin_dir_name}

+ 12 - 18
cmake/CuraTests.cmake

@@ -4,18 +4,11 @@
 include(CTest)
 include(CMakeParseArguments)
 
-# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
-# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
-# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
-if(${CMAKE_VERSION} VERSION_LESS 3.12)
-    # Use FindPythonInterp and FindPythonLibs for CMake <3.12
-    find_package(PythonInterp 3 REQUIRED)
+# FIXME: The new FindPython3 finds the system's Python3.6 reather than the Python3.5 that we built for Cura's environment.
+# So we're using the old method here, with FindPythonInterp for now.
+find_package(PythonInterp 3 REQUIRED)
 
-    set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
-else()
-    # Use FindPython3 for CMake >=3.12
-    find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
-endif()
+set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
 
 add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose)
 
@@ -56,6 +49,14 @@ function(cura_add_test)
     endif()
 endfunction()
 
+
+#Add code style test.
+add_test(
+    NAME "code-style"
+    COMMAND ${Python3_EXECUTABLE} run_mypy.py
+    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+)
+
 #Add test for import statements which are not compatible with all builds
 add_test(
     NAME "invalid-imports"
@@ -74,13 +75,6 @@ foreach(_plugin ${_plugins})
     endif()
 endforeach()
 
-#Add code style test.
-add_test(
-    NAME "code-style"
-    COMMAND ${Python3_EXECUTABLE} run_mypy.py
-    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
-)
-
 #Add test for whether the shortcut alt-keys are unique in every translation.
 add_test(
     NAME "shortcut-keys"

+ 13 - 9
cmake/mod_bundled_packages_json.py

@@ -11,11 +11,13 @@ import os
 import sys
 
 
-## Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths.
-#
-#  \param work_dir The directory to look for JSON files recursively.
-#  \return A list of JSON files in absolute paths that are found in the given directory.
 def find_json_files(work_dir: str) -> list:
+    """Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths.
+
+    :param work_dir: The directory to look for JSON files recursively.
+    :return: A list of JSON files in absolute paths that are found in the given directory.
+    """
+
     json_file_list = []
     for root, dir_names, file_names in os.walk(work_dir):
         for file_name in file_names:
@@ -24,12 +26,14 @@ def find_json_files(work_dir: str) -> list:
     return json_file_list
 
 
-## Removes the given entries from the given JSON file. The file will modified in-place.
-#
-#  \param file_path The JSON file to modify.
-#  \param entries A list of strings as entries to remove.
-#  \return None
 def remove_entries_from_json_file(file_path: str, entries: list) -> None:
+    """Removes the given entries from the given JSON file. The file will modified in-place.
+
+    :param file_path: The JSON file to modify.
+    :param entries: A list of strings as entries to remove.
+    :return: None
+    """
+
     try:
         with open(file_path, "r", encoding = "utf-8") as f:
             package_dict = json.load(f, object_hook = collections.OrderedDict)

+ 190 - 23
cura/API/Account.py

@@ -1,15 +1,16 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
-from typing import Optional, Dict, TYPE_CHECKING
+from datetime import datetime
+from typing import Optional, Dict, TYPE_CHECKING, Callable
 
-from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
+from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, Q_ENUMS
 
-from UM.i18n import i18nCatalog
+from UM.Logger import Logger
 from UM.Message import Message
-from cura import UltimakerCloudAuthentication
-
+from UM.i18n import i18nCatalog
 from cura.OAuth2.AuthorizationService import AuthorizationService
 from cura.OAuth2.Models import OAuth2Settings
+from cura.UltimakerCloud import UltimakerCloudConstants
 
 if TYPE_CHECKING:
     from cura.CuraApplication import CuraApplication
@@ -17,29 +18,61 @@ if TYPE_CHECKING:
 i18n_catalog = i18nCatalog("cura")
 
 
-##  The account API provides a version-proof bridge to use Ultimaker Accounts
-#
-#   Usage:
-#       ``from cura.API import CuraAPI
-#       api = CuraAPI()
-#       api.account.login()
-#       api.account.logout()
-#       api.account.userProfile # Who is logged in``
-#
+class SyncState:
+    """QML: Cura.AccountSyncState"""
+    SYNCING = 0
+    SUCCESS = 1
+    ERROR = 2
+    IDLE = 3
+
 class Account(QObject):
-    # Signal emitted when user logged in or out.
+    """The account API provides a version-proof bridge to use Ultimaker Accounts
+
+    Usage:
+
+    .. code-block:: python
+
+      from cura.API import CuraAPI
+      api = CuraAPI()
+      api.account.login()
+      api.account.logout()
+      api.account.userProfile    # Who is logged in
+    """
+
+    # The interval in which sync services are automatically triggered
+    SYNC_INTERVAL = 30.0  # seconds
+    Q_ENUMS(SyncState)
+
     loginStateChanged = pyqtSignal(bool)
+    """Signal emitted when user logged in or out"""
+
     accessTokenChanged = pyqtSignal()
+    syncRequested = pyqtSignal()
+    """Sync services may connect to this signal to receive sync triggers.
+    Services should be resilient to receiving a signal while they are still syncing,
+    either by ignoring subsequent signals or restarting a sync.
+    See setSyncState() for providing user feedback on the state of your service. 
+    """
+    lastSyncDateTimeChanged = pyqtSignal()
+    syncStateChanged = pyqtSignal(int)  # because SyncState is an int Enum
+    manualSyncEnabledChanged = pyqtSignal(bool)
+    updatePackagesEnabledChanged = pyqtSignal(bool)
 
     def __init__(self, application: "CuraApplication", parent = None) -> None:
         super().__init__(parent)
         self._application = application
+        self._new_cloud_printers_detected = False
 
         self._error_message = None  # type: Optional[Message]
         self._logged_in = False
+        self._sync_state = SyncState.IDLE
+        self._manual_sync_enabled = False
+        self._update_packages_enabled = False
+        self._update_packages_action = None  # type: Optional[Callable]
+        self._last_sync_str = "-"
 
         self._callback_port = 32118
-        self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
+        self._oauth_root = UltimakerCloudConstants.CuraCloudAccountAPIRoot
 
         self._oauth_settings = OAuth2Settings(
             OAUTH_SERVER_URL= self._oauth_root,
@@ -56,6 +89,16 @@ class Account(QObject):
 
         self._authorization_service = AuthorizationService(self._oauth_settings)
 
+        # Create a timer for automatic account sync
+        self._update_timer = QTimer()
+        self._update_timer.setInterval(int(self.SYNC_INTERVAL * 1000))
+        # The timer is restarted explicitly after an update was processed. This prevents 2 concurrent updates
+        self._update_timer.setSingleShot(True)
+        self._update_timer.timeout.connect(self.sync)
+
+        self._sync_services = {}  # type: Dict[str, int]
+        """contains entries "service_name" : SyncState"""
+
     def initialize(self) -> None:
         self._authorization_service.initialize(self._application.getPreferences())
         self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
@@ -63,12 +106,65 @@ class Account(QObject):
         self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
         self._authorization_service.loadAuthDataFromPreferences()
 
+
+    @pyqtProperty(int, notify=syncStateChanged)
+    def syncState(self):
+        return self._sync_state
+
+    def setSyncState(self, service_name: str, state: int) -> None:
+        """ Can be used to register sync services and update account sync states
+
+        Contract: A sync service is expected exit syncing state in all cases, within reasonable time
+
+        Example: `setSyncState("PluginSyncService", SyncState.SYNCING)`
+        :param service_name: A unique name for your service, such as `plugins` or `backups`
+        :param state: One of SyncState
+        """
+        prev_state = self._sync_state
+
+        self._sync_services[service_name] = state
+
+        if any(val == SyncState.SYNCING for val in self._sync_services.values()):
+            self._sync_state = SyncState.SYNCING
+            self._setManualSyncEnabled(False)
+        elif any(val == SyncState.ERROR for val in self._sync_services.values()):
+            self._sync_state = SyncState.ERROR
+            self._setManualSyncEnabled(True)
+        else:
+            self._sync_state = SyncState.SUCCESS
+            self._setManualSyncEnabled(False)
+
+        if self._sync_state != prev_state:
+            self.syncStateChanged.emit(self._sync_state)
+
+            if self._sync_state == SyncState.SUCCESS:
+                self._last_sync_str = datetime.now().strftime("%d/%m/%Y %H:%M")
+                self.lastSyncDateTimeChanged.emit()
+
+            if self._sync_state != SyncState.SYNCING:
+                # schedule new auto update after syncing completed (for whatever reason)
+                if not self._update_timer.isActive():
+                    self._update_timer.start()
+
+    def setUpdatePackagesAction(self, action: Callable) -> None:
+        """ Set the callback which will be invoked when the user clicks the update packages button
+
+        Should be invoked after your service sets the sync state to SYNCING and before setting the
+        sync state to SUCCESS.
+
+        Action will be reset to None when the next sync starts
+        """
+        self._update_packages_action = action
+        self._update_packages_enabled = True
+        self.updatePackagesEnabledChanged.emit(self._update_packages_enabled)
+
     def _onAccessTokenChanged(self):
         self.accessTokenChanged.emit()
 
-    ## Returns a boolean indicating whether the given authentication is applied against staging or not.
     @property
     def is_staging(self) -> bool:
+        """Indication whether the given authentication is applied against staging or not."""
+
         return "staging" in self._oauth_root
 
     @pyqtProperty(bool, notify=loginStateChanged)
@@ -83,18 +179,60 @@ class Account(QObject):
             self._error_message.show()
             self._logged_in = False
             self.loginStateChanged.emit(False)
+            if self._update_timer.isActive():
+                self._update_timer.stop()
             return
 
         if self._logged_in != logged_in:
             self._logged_in = logged_in
             self.loginStateChanged.emit(logged_in)
+            if logged_in:
+                self._setManualSyncEnabled(False)
+                self._sync()
+            else:
+                if self._update_timer.isActive():
+                    self._update_timer.stop()
+
+    def _sync(self) -> None:
+        """Signals all sync services to start syncing
+
+        This can be considered a forced sync: even when a
+        sync is currently running, a sync will be requested.
+        """
+
+        self._update_packages_action = None
+        self._update_packages_enabled = False
+        self.updatePackagesEnabledChanged.emit(self._update_packages_enabled)
+        if self._update_timer.isActive():
+            self._update_timer.stop()
+        elif self._sync_state == SyncState.SYNCING:
+            Logger.warning("Starting a new sync while previous sync was not completed\n{}", str(self._sync_services))
+
+        self.syncRequested.emit()
+
+    def _setManualSyncEnabled(self, enabled: bool) -> None:
+        if self._manual_sync_enabled != enabled:
+            self._manual_sync_enabled = enabled
+            self.manualSyncEnabledChanged.emit(enabled)
 
     @pyqtSlot()
-    def login(self) -> None:
+    @pyqtSlot(bool)
+    def login(self, force_logout_before_login: bool = False) -> None:
+        """
+        Initializes the login process. If the user is logged in already and force_logout_before_login is true, Cura will
+        logout from the account before initiating the authorization flow. If the user is logged in and
+        force_logout_before_login is false, the function will return, as there is nothing to do.
+
+        :param force_logout_before_login: Optional boolean parameter
+        :return: None
+        """
         if self._logged_in:
-            # Nothing to do, user already logged in.
-            return
-        self._authorization_service.startAuthorizationFlow()
+            if force_logout_before_login:
+                self.logout()
+            else:
+                # Nothing to do, user already logged in.
+                return
+        self._authorization_service.startAuthorizationFlow(force_logout_before_login)
 
     @pyqtProperty(str, notify=loginStateChanged)
     def userName(self):
@@ -114,15 +252,44 @@ class Account(QObject):
     def accessToken(self) -> Optional[str]:
         return self._authorization_service.getAccessToken()
 
-    #   Get the profile of the logged in user
-    #   @returns None if no user is logged in, a dict containing user_id, username and profile_image_url
     @pyqtProperty("QVariantMap", notify = loginStateChanged)
     def userProfile(self) -> Optional[Dict[str, Optional[str]]]:
+        """None if no user is logged in otherwise the logged in  user as a dict containing containing user_id, username and profile_image_url """
+
         user_profile = self._authorization_service.getUserProfile()
         if not user_profile:
             return None
         return user_profile.__dict__
 
+    @pyqtProperty(str, notify=lastSyncDateTimeChanged)
+    def lastSyncDateTime(self) -> str:
+        return self._last_sync_str
+
+    @pyqtProperty(bool, notify=manualSyncEnabledChanged)
+    def manualSyncEnabled(self) -> bool:
+        return self._manual_sync_enabled
+
+    @pyqtProperty(bool, notify=updatePackagesEnabledChanged)
+    def updatePackagesEnabled(self) -> bool:
+        return self._update_packages_enabled
+
+    @pyqtSlot()
+    @pyqtSlot(bool)
+    def sync(self, user_initiated: bool = False) -> None:
+        if user_initiated:
+            self._setManualSyncEnabled(False)
+
+        self._sync()
+
+    @pyqtSlot()
+    def onUpdatePackagesClicked(self) -> None:
+        if self._update_packages_action is not None:
+            self._update_packages_action()
+
+    @pyqtSlot()
+    def popupOpened(self) -> None:
+        self._setManualSyncEnabled(True)
+
     @pyqtSlot()
     def logout(self) -> None:
         if not self._logged_in:

Some files were not shown because too many files changed in this diff