diff --git a/src/octoprint/server/api/slicing.py b/src/octoprint/server/api/slicing.py index 28ab35e9..978103a6 100644 --- a/src/octoprint/server/api/slicing.py +++ b/src/octoprint/server/api/slicing.py @@ -12,19 +12,28 @@ from octoprint.server import slicingManager from octoprint.server.util.flask import restricted_access from octoprint.server.api import api, NO_CONTENT -from octoprint.settings import settings as s +from octoprint.settings import settings as s, valid_boolean_trues + +from octoprint.slicing import SlicerNotConfigured @api.route("/slicing", methods=["GET"]) def slicingListAll(): default_slicer = s().get(["slicing", "defaultSlicer"]) + if "configured" in request.values and request.values["configured"] in valid_boolean_trues: + slicers = slicingManager.configured_slicers + else: + slicers = slicingManager.registered_slicers + result = dict() - for slicer in slicingManager.registered_slicers: + for slicer in slicers: + slicer_impl = slicingManager.get_slicer(slicer, require_configured=False) result[slicer] = dict( key=slicer, - displayName=slicingManager.get_slicer(slicer).get_slicer_properties()["name"], + displayName=slicer_impl.get_slicer_properties()["name"], default=default_slicer == slicer, + configured = slicer_impl.is_slicer_configured(), profiles=_getSlicingProfilesData(slicer) ) @@ -35,7 +44,13 @@ def slicingListSlicerProfiles(slicer): if not slicer in slicingManager.registered_slicers: return make_response("Unknown slicer {slicer}".format(**locals()), 404) - return jsonify(_getSlicingProfilesData(slicer)) + configured = False + if "configured" in request.values and request.values["configured"] in valid_boolean_trues: + if not slicer in slicingManager.configured_slicers: + return make_response("Unknown slicer {slicer}".format(**locals()), 404) + configured = True + + return jsonify(_getSlicingProfilesData(slicer, require_configured=configured)) @api.route("/slicing//profiles/", methods=["GET"]) def slicingGetSlicerProfile(slicer, name): @@ -57,7 +72,7 @@ def slicingAddSlicerProfile(slicer, name): return make_response("Unknown slicer {slicer}".format(**locals()), 404) if not "application/json" in request.headers["Content-Type"]: - return None, None, make_response("Expected content-type JSON", 400) + return make_response("Expected content-type JSON", 400) try: json_data = request.json @@ -88,7 +103,7 @@ def slicingPatchSlicerProfile(slicer, name): return make_response("Unknown slicer {slicer}".format(**locals()), 404) if not "application/json" in request.headers["Content-Type"]: - return None, None, make_response("Expected content-type JSON", 400) + return make_response("Expected content-type JSON", 400) profile = slicingManager.load_profile(slicer, name) if not profile: @@ -130,9 +145,10 @@ def slicingDelSlicerProfile(slicer, name): slicingManager.delete_profile(slicer, name) return NO_CONTENT -def _getSlicingProfilesData(slicer): - profiles = slicingManager.all_profiles(slicer) - if not profiles: +def _getSlicingProfilesData(slicer, require_configured=False): + try: + profiles = slicingManager.all_profiles(slicer, require_configured=require_configured) + except SlicerNotConfigured: return dict() result = dict() diff --git a/src/octoprint/slicing/__init__.py b/src/octoprint/slicing/__init__.py index 871e4673..8d4848e4 100644 --- a/src/octoprint/slicing/__init__.py +++ b/src/octoprint/slicing/__init__.py @@ -11,6 +11,8 @@ import octoprint.plugin import octoprint.events from octoprint.settings import settings +from .exceptions import * + class SlicingProfile(object): def __init__(self, slicer, name, data, display_name=None, description=None): @@ -44,10 +46,6 @@ class TemporaryProfile(object): pass -class SlicingCancelled(BaseException): - pass - - class SlicingManager(object): def __init__(self, profile_path, printer_profile_manager): self._profile_path = profile_path @@ -71,8 +69,7 @@ class SlicingManager(object): def _load_slicers(self): plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SlicerPlugin) for name, plugin in plugins.items(): - if plugin.is_slicer_configured(): - self._slicers[plugin.get_slicer_properties()["type"]] = plugin + self._slicers[plugin.get_slicer_properties()["type"]] = plugin @property def slicing_enabled(self): @@ -89,13 +86,15 @@ class SlicingManager(object): @property def default_slicer(self): slicer_name = settings().get(["slicing", "defaultSlicer"]) - if slicer_name in self.configured_slicers: + if slicer_name in self.registered_slicers: return slicer_name else: return None def get_slicer(self, slicer, require_configured=True): - return self._slicers[slicer] if slicer in self._slicers and (not require_configured or self._slicers[slicer].is_slicer_configured()) else None + if slicer in self._slicers and (not require_configured or self._slicers[slicer].is_slicer_configured()): + return self._slicers[slicer] + raise SlicerNotConfigured(slicer) def slice(self, slicer_name, source_path, dest_path, profile_name, callback, callback_args=None, callback_kwargs=None, overrides=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None, printer_profile_id=None, position=None): if callback_args is None: @@ -106,11 +105,13 @@ class SlicingManager(object): if not slicer_name in self.configured_slicers: if not slicer_name in self.registered_slicers: error = "No such slicer: {slicer_name}".format(**locals()) + exc = UnknownSlicer(slicer_name) else: error = "Slicer not configured: {slicer_name}".format(**locals()) - callback_kwargs.update(dict(_error=error)) + exc = SlicerNotConfigured(slicer_name) + callback_kwargs.update(dict(_error=error, _exc=exc)) callback(*callback_args, **callback_kwargs) - return False, error + raise exc slicer = self.get_slicer(slicer_name) @@ -154,23 +155,24 @@ class SlicingManager(object): def cancel_slicing(self, slicer_name, source_path, dest_path): if not slicer_name in self.registered_slicers: - return + raise UnknownSlicer(slicer_name) + slicer = self.get_slicer(slicer_name) slicer.cancel_slicing(dest_path) - def load_profile(self, slicer, name): + def load_profile(self, slicer, name, require_configured=True): if not slicer in self.registered_slicers: - return None + raise UnknownSlicer(slicer) try: path = self.get_profile_path(slicer, name, must_exist=True) except IOError: return None - return self._load_profile_from_path(slicer, path) + return self._load_profile_from_path(slicer, path, require_configured=require_configured) def save_profile(self, slicer, name, profile, overrides=None, allow_overwrite=True, display_name=None, description=None): if not slicer in self.registered_slicers: - return + raise UnknownSlicer(slicer) if not isinstance(profile, SlicingProfile): if isinstance(profile, dict): @@ -191,7 +193,7 @@ class SlicingManager(object): def temporary_profile(self, slicer, name=None, overrides=None): if not slicer in self.registered_slicers: - return None + raise UnknownSlicer(slicer) profile = self._get_default_profile(slicer) if name: @@ -207,16 +209,21 @@ class SlicingManager(object): def delete_profile(self, slicer, name): if not slicer in self.registered_slicers: - return None + raise UnknownSlicer(slicer) + + if not name: + raise ValueError("name must be set") path = self.get_profile_path(slicer, name) if not os.path.exists(path) or not os.path.isfile(path): return os.remove(path) - def all_profiles(self, slicer): + def all_profiles(self, slicer, require_configured=False): if not slicer in self.registered_slicers: - return None + raise UnknownSlicer(slicer) + if require_configured and not slicer in self.configured_slicers: + raise SlicerNotConfigured(slicer) profiles = dict() slicer_profile_path = self.get_slicer_profile_path(slicer) @@ -228,12 +235,12 @@ class SlicingManager(object): path = os.path.join(slicer_profile_path, entry) profile_name = entry[:-len(".profile")] - profiles[profile_name] = self._load_profile_from_path(slicer, path) + profiles[profile_name] = self._load_profile_from_path(slicer, path, require_configured=require_configured) return profiles def get_slicer_profile_path(self, slicer): if not slicer in self.registered_slicers: - return None + raise UnknownSlicer(slicer) path = os.path.join(self._profile_path, slicer) if not os.path.exists(path): @@ -242,10 +249,10 @@ class SlicingManager(object): def get_profile_path(self, slicer, name, must_exist=False): if not slicer in self.registered_slicers: - return None + raise UnknownSlicer(slicer) if not name: - return None + raise ValueError("name must be set") name = self._sanitize(name) @@ -269,11 +276,11 @@ class SlicingManager(object): sanitized_name = sanitized_name.replace(" ", "_") return sanitized_name - def _load_profile_from_path(self, slicer, path): - return self.get_slicer(slicer).get_slicer_profile(path) + def _load_profile_from_path(self, slicer, path, require_configured=False): + return self.get_slicer(slicer, require_configured=require_configured).get_slicer_profile(path) - def _save_profile_to_path(self, slicer, path, profile, allow_overwrite=True, overrides=None): - self.get_slicer(slicer).save_slicer_profile(path, profile, allow_overwrite=allow_overwrite, overrides=overrides) + def _save_profile_to_path(self, slicer, path, profile, allow_overwrite=True, overrides=None, require_configured=False): + self.get_slicer(slicer, require_configured=require_configured).save_slicer_profile(path, profile, allow_overwrite=allow_overwrite, overrides=overrides) def _get_default_profile(self, slicer): default_profiles = settings().get(["slicing", "defaultProfiles"]) diff --git a/src/octoprint/slicing/exceptions.py b/src/octoprint/slicing/exceptions.py new file mode 100644 index 00000000..4eefe4ae --- /dev/null +++ b/src/octoprint/slicing/exceptions.py @@ -0,0 +1,28 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License" + + +class SlicingException(BaseException): + pass + +class SlicingCancelled(SlicingException): + pass + +class SlicerException(SlicingException): + def __init__(self, slicer, *args, **kwargs): + super(SlicingException, self).__init__(*args, **kwargs) + self.slicer = slicer + +class SlicerNotConfigured(SlicerException): + def __init__(self, slicer, *args, **kwargs): + super(SlicerException, self).__init__(slicer, *args, **kwargs) + self.message = "Slicer not configured: {slicer}".format(slicer=slicer) + +class UnknownSlicer(SlicerException): + def __init__(self, slicer, *args, **kwargs): + super(SlicerException, self).__init__(slicer, *args, **kwargs) + self.message = "No such slicer: {slicer}".format(slicer=slicer) diff --git a/src/octoprint/static/js/app/dataupdater.js b/src/octoprint/static/js/app/dataupdater.js index aa4ce6ac..d25a731b 100644 --- a/src/octoprint/static/js/app/dataupdater.js +++ b/src/octoprint/static/js/app/dataupdater.js @@ -167,25 +167,7 @@ function DataUpdater(allViewModels) { log.debug("Got event " + type + " with payload: " + JSON.stringify(payload)); - if (type == "UpdatedFiles") { - _.each(self.allViewModels, function (viewModel) { - if (viewModel.hasOwnProperty("onUpdatedFiles")) { - viewModel.onUpdatedFiles(payload); - } - }); - } else if (type == "MetadataStatisticsUpdated") { - _.each(self.allViewModels, function(viewModel) { - if (viewModel.hasOwnProperty("onMetadataStatisticsUpdated")) { - viewModel.onMetadataStatisticsUpdated(payload); - } - }) - } else if (type == "MetadataAnalysisFinished") { - _.each(self.allViewModels, function(viewModel) { - if (viewModel.hasOwnProperty("onMetadataAnalysisFinished")) { - viewModel.onMetadataAnalysisFinished(payload); - } - }); - } else if (type == "MovieRendering") { + if (type == "MovieRendering") { new PNotify({title: gettext("Rendering timelapse"), text: _.sprintf(gettext("Now rendering timelapse %(movie_basename)s"), payload)}); } else if (type == "MovieDone") { new PNotify({title: gettext("Timelapse ready"), text: _.sprintf(gettext("New timelapse %(movie_basename)s is done rendering."), payload)}); @@ -207,21 +189,10 @@ function DataUpdater(allViewModels) { gcodeUploadProgressBar.css("width", "0%"); gcodeUploadProgressBar.text(""); new PNotify({title: gettext("Slicing done"), text: _.sprintf(gettext("Sliced %(stl)s to %(gcode)s, took %(time).2f seconds"), payload), type: "success"}); - - _.each(self.allViewModels, function (viewModel) { - if (viewModel.hasOwnProperty("onSlicingDone")) { - viewModel.onSlicingDone(payload); - } - }); } else if (type == "SlicingCancelled") { gcodeUploadProgress.removeClass("progress-striped").removeClass("active"); gcodeUploadProgressBar.css("width", "0%"); gcodeUploadProgressBar.text(""); - _.each(self.allViewModels, function (viewModel) { - if (viewModel.hasOwnProperty("onSlicingCancelled")) { - viewModel.onSlicingCancelled(payload); - } - }); } else if (type == "SlicingFailed") { gcodeUploadProgress.removeClass("progress-striped").removeClass("active"); gcodeUploadProgressBar.css("width", "0%"); @@ -229,11 +200,6 @@ function DataUpdater(allViewModels) { html = _.sprintf(gettext("Could not slice %(stl)s to %(gcode)s: %(reason)s"), payload); new PNotify({title: gettext("Slicing failed"), text: html, type: "error", hide: false}); - _.each(self.allViewModels, function (viewModel) { - if (viewModel.hasOwnProperty("onSlicingFailed")) { - viewModel.onSlicingFailed(payload); - } - }); } else if (type == "TransferStarted") { gcodeUploadProgress.addClass("progress-striped").addClass("active"); gcodeUploadProgressBar.css("width", "100%"); @@ -249,6 +215,26 @@ function DataUpdater(allViewModels) { }); gcodeFilesViewModel.requestData(payload.remote, "sdcard"); } + + var legacyEventHandlers = { + "UpdatedFiles": "onUpdatedFiles", + "MetadataStatisticsUpdated": "onMetadataStatisticsUpdated", + "MetadataAnalysisFinished": "onMetadataAnalysisFinished", + "SlicingDone": "onSlicingDone", + "SlicingCancelled": "onSlicingCancelled", + "SlicingFailed": "onSlicingFailed" + }; + _.each(self.allViewModels, function(viewModel) { + if (viewModel.hasOwnProperty("onEvent" + type)) { + viewModel["onEvent" + type](payload); + } else if (legacyEventHandlers.hasOwnProperty(type) && viewModel.hasOwnProperty(legacyEventHandlers[type])) { + // there might still be code that uses the old callbacks, make sure those still get called + // but log a warning + log.warn("View model " + viewModel.name + " is using legacy event handler " + legacyEventHandlers[type] + ", new handler is called " + legacyEventHandlers[type]); + viewModel[legacyEventHandlers[type]](payload); + } + }); + break; } case "feedbackCommandOutput": { diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 019acb8e..cde25948 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -189,7 +189,7 @@ $(function() { self.sliceFile = function(file) { if (!file) return; - self.slicing.show(file.origin, file.name); + self.slicing.show(file.origin, file.name, true); }; self.initSdCard = function() { @@ -265,7 +265,7 @@ $(function() { }; self.enableSlicing = function(data) { - return self.loginState.isUser() && !(self.isPrinting() || self.isPaused()); + return self.loginState.isUser() && self.slicing.enableSlicingDialog(); }; self.enableAdditionalData = function(data) { @@ -379,7 +379,7 @@ $(function() { } function gcode_upload_fail(e, data) { - var error = "

" + gettext("Could not upload the file. Make sure that it is a GCODE file and has the extension \".gcode\" or \".gco\" or that it is an STL file with the extension \".stl\" and slicing support is enabled and configured.") + "

"; + var error = "

" + gettext("Could not upload the file. Make sure that it is a GCODE file and has the extension \".gcode\" or \".gco\" or that it is an STL file with the extension \".stl\".") + "

"; error += pnotifyAdditionalInfo("
" + data.jqXHR.responseText + "
"); new PNotify({ title: "Upload failed", @@ -551,21 +551,21 @@ $(function() { self.requestData(); }; - self.onUpdatedFiles = function(payload) { + self.onEventUpdatedFiles = function(payload) { if (payload.type == "gcode") { self.requestData(); } }; - self.onSlicingDone = function(payload) { + self.onEventSlicingDone = function(payload) { self.requestData(); }; - self.onMetadataAnalysisFinished = function(payload) { + self.onEventMetadataAnalysisFinished = function(payload) { self.requestData(); }; - self.onMetadataStatisticsUpdated = function(payload) { + self.onEventMetadataStatisticsUpdated = function(payload) { self.requestData(); }; } diff --git a/src/octoprint/static/js/app/viewmodels/slicing.js b/src/octoprint/static/js/app/viewmodels/slicing.js index 194bff3d..eba0581a 100644 --- a/src/octoprint/static/js/app/viewmodels/slicing.js +++ b/src/octoprint/static/js/app/viewmodels/slicing.js @@ -21,6 +21,12 @@ $(function() { self.profiles = ko.observableArray(); self.printerProfile = ko.observable(); + self.configured_slicers = ko.computed(function() { + return _.filter(self.slicers(), function(slicer) { + return slicer.configured; + }); + }); + self.afterSlicingOptions = [ {"value": "none", "text": gettext("Do nothing")}, {"value": "select", "text": gettext("Select for printing")}, @@ -28,7 +34,11 @@ $(function() { ]; self.afterSlicing = ko.observable("none"); - self.show = function(target, file) { + self.show = function(target, file, force) { + if (!self.enableSlicingDialog() && !force) { + return; + } + self.requestData(); self.target = target; self.file = file; @@ -43,6 +53,10 @@ $(function() { self.profilesForSlicer(newValue); }); + self.enableSlicingDialog = ko.computed(function() { + return self.configured_slicers().length > 0; + }); + self.enableSliceButton = ko.computed(function() { return self.gcodeFilename() != undefined && self.gcodeFilename().trim() != "" @@ -75,13 +89,14 @@ $(function() { name = slicer.key; } - if (slicer.default) { + if (slicer.default && slicer.configured) { selectedSlicer = slicer.key; } self.slicers.push({ key: slicer.key, - name: name + name: name, + configured: slicer.configured }); }); @@ -170,6 +185,10 @@ $(function() { self.onStartup = function() { self.requestData(); }; + + self.onEventSettingsUpdated = function(payload) { + self.requestData(); + }; } OCTOPRINT_VIEWMODELS.push([ diff --git a/src/octoprint/templates/dialogs/slicing.jinja2 b/src/octoprint/templates/dialogs/slicing.jinja2 index 3b0ad040..cdcf88b3 100644 --- a/src/octoprint/templates/dialogs/slicing.jinja2 +++ b/src/octoprint/templates/dialogs/slicing.jinja2 @@ -4,42 +4,47 @@