From d0caaeaa4a0e94b7d5fbcac864097d847d83e44a Mon Sep 17 00:00:00 2001 From: Marc Date: Wed, 15 Apr 2015 23:09:06 +0200 Subject: [PATCH] initial commit --- .gitignore | 8 + README.md | 0 customControl/__init__.py | 26 ++ customControl/static/js/customControl.js | 228 ++++++++++++++++++ customControl/templates/customControl.jinja2 | 11 + .../templates/customControl_settings.jinja2 | 110 +++++++++ requirements.txt | 2 + setup.py | 96 ++++++++ 8 files changed, 481 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 customControl/__init__.py create mode 100644 customControl/static/js/customControl.js create mode 100644 customControl/templates/customControl.jinja2 create mode 100644 customControl/templates/customControl_settings.jinja2 create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ae58ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.pyc +*.swp +.idea +*.iml +build +dist +*.egg* +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/customControl/__init__.py b/customControl/__init__.py new file mode 100644 index 0000000..5f2eb5e --- /dev/null +++ b/customControl/__init__.py @@ -0,0 +1,26 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Marc Hannappel " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" + +import octoprint.plugin + +class CustomControlPlugin(octoprint.plugin.SettingsPlugin, + octoprint.plugin.TemplatePlugin, + octoprint.plugin.AssetPlugin): + def get_settings_defaults(self): + return dict(); + + def get_assets(self): + return { + "js": ["js/customControl.js"], + } + +# If you want your plugin to be registered within OctoPrint under a different name than what you defined in setup.py +# ("OctoPrint-PluginSkeleton"), you may define that here. Same goes for the other metadata derived from setup.py that +# can be overwritten via __plugin_xyz__ control properties. See the documentation for that. +__plugin_name__ = "CustomControl" +__plugin_license__ = "AGPLv3" +__plugin_implementation__ = CustomControlPlugin() diff --git a/customControl/static/js/customControl.js b/customControl/static/js/customControl.js new file mode 100644 index 0000000..16fdfe3 --- /dev/null +++ b/customControl/static/js/customControl.js @@ -0,0 +1,228 @@ +$(function () { + $.fn.isChildOf = function (element) { + return $(element).has(this).length > 0; + } + + // from http://jsfiddle.net/KyleMit/X9tgY/ + $.fn.contextMenu = function (settings) { + return this.each(function () { + // Open context menu + $(this).on("contextmenu", function (e) { + // return native menu if pressing control + if (e.ctrlKey) return; + + $(settings.menuSelector) + .data("invokedOn", $(e.target)) + .show() + .css({ + position: "absolute", + left: getMenuPosition(e.clientX, 'width', 'scrollLeft'), + top: getMenuPosition(e.clientY, 'height', 'scrollTop'), + "z-index": 9999 + }).off('click') + .on('click', function (e) { + if (e.target.tagName.toLowerCase() == "input") + return; + + $(this).hide(); + + var $invokedOn = $(this).data("invokedOn"); + var $selectedMenu = $(e.target); + + settings.menuSelected.call(this, $invokedOn, $selectedMenu); + }); + + return false; + }); + + //make sure menu closes on any click + $(document).click(function () { + $(settings.menuSelector).hide(); + }); + }); + + function getMenuPosition(mouse, direction, scrollDir) { + var win = $(window)[direction](), + scroll = $(window)[scrollDir](), + menu = $(settings.menuSelector)[direction](), + position = mouse + scroll; + + // opening menu would pass the side of the page + if (mouse + menu > win && menu < mouse) + position -= menu; + + return position; + } + }; + + ko.bindingHandlers.contextMenu = { + init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + var val = ko.utils.unwrapObservable(valueAccessor()); + + $(element).contextMenu(val); + }, + update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + var val = ko.utils.unwrapObservable(valueAccessor()); + + $(element).contextMenu(val); + } + } + + function CustomControlViewModel(parameters) { + var self = this; + + self.loginState = parameters[0]; + self.settingsViewModel = parameters[1]; + self.controlViewModel = parameters[2]; + + self.controls = ko.observableArray([]); + + self.controlsFromServer = []; + self.additionalControls = []; + + self.staticID = 0; + + self.onBeforeBinding = function () { + self.settings = self.settingsViewModel.settings; + self.rerenderControls(); + }; + + self.onAllBound = function (allViewModels) { + var additionalControls = []; + _.each(allViewModels, function (viewModel) { + if (viewModel.hasOwnProperty("getAdditionalControls")) { + additionalControls = additionalControls.concat(viewModel.getAdditionalControls()); + } + }); + if (additionalControls.length > 0) { + self.additionalControls = additionalControls; + self.rerenderControls(); + } + }; + + self.rerenderControls = function () { + self.staticID = 0; + + var allControls = self.controlsFromServer.concat(self.additionalControls); + self.controls(self._processControls(allControls)) + }; + + self._processControls = function (controls) { + for (var i = 0; i < controls.length; i++) { + controls[i] = self._processControl(controls[i]); + } + return controls; + }; + + self._processControl = function (control) { + if (control.hasOwnProperty("template") && control.hasOwnProperty("key") && control.hasOwnProperty("template_key") && !control.hasOwnProperty("output")) { + control.output = ko.observable(""); + } + + if (control.hasOwnProperty("children")) { + if (control.processed) + control.children = ko.observableArray(self._processControls(control.children())); + else + control.children = ko.observableArray(self._processControls(control.children)); + + if (!control.hasOwnProperty("layout") || !(control.layout == "vertical" || control.layout == "horizontal")) { + control.layout = "vertical"; + } + } + + if (control.hasOwnProperty("input")) { + for (var i = 0; i < control.input.length; i++) { + control.input[i].value = ko.observable(control.input[i].default); + if (!control.input[i].hasOwnProperty("slider")) { + control.input[i].slider = false; + } + } + } + + var js; + if (control.hasOwnProperty("javascript")) { + js = control.javascript; + + // if js is a function everything's fine already, but if it's a string we need to eval that first + if (!_.isFunction(js)) { + control.javascript = function (data) { + eval(js); + }; + } + } + + if (control.hasOwnProperty("enabled")) { + js = control.enabled; + + // if js is a function everything's fine already, but if it's a string we need to eval that first + if (!_.isFunction(js)) { + control.enabled = function (data) { + return eval(js); + } + } + } + + control.id = "settingsCustomControl_id" + self.staticID++; + control.processed = true; + return control; + }; + + self.displayMode = function (customControl) { + if (customControl.hasOwnProperty("children")) { + return "settingsCustomControls_containerTemplate"; + } else { + return "settingsCustomControls_controlTemplate"; + } + } + + self.searchElement = function (list, id) { + for (var i = 0; i < list.length; i++) + { + if (list[i].id == id) + return list[i]; + + if (list[i].hasOwnProperty("children")) { + var element = self.searchElement(list[i].children(), id); + if (element != undefined) + return element; + } + } + + return undefined; + } + + self.controlContextMenu = function (invokedOn, selectedMenu) + { + if (selectedMenu.attr('cmd') == "createContainer") { + if (invokedOn.attr('id') == "base") { + // TODO: make a create dialog + self.controlsFromServer.push({ children: [], id: "settingsCustomControl_id" + self.staticID++ }); + self.rerenderControls(); + } + else { + var parentElement = self.searchElement(self.controlsFromServer, invokedOn.attr('id')); + if (parentElement == undefined) + { + // TODO: make an Warning dialog + alert("Something went wrong while creating the new Element"); + return; + } + + // TODO: make a create dialog + parentElement.children.push(self._processControl({ children: [], id: "settingsCustomControl_id" + self.staticID++ })); + } + } + } + + self.editStyle = function (type) { + } + + } + + // view model class, parameters for constructor, container to bind to + OCTOPRINT_VIEWMODELS.push([ + CustomControlViewModel, + ["loginStateViewModel", "settingsViewModel", "controlViewModel"], + "#settings_plugin_customControl" + ]); +}); \ No newline at end of file diff --git a/customControl/templates/customControl.jinja2 b/customControl/templates/customControl.jinja2 new file mode 100644 index 0000000..a5eeeaa --- /dev/null +++ b/customControl/templates/customControl.jinja2 @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/customControl/templates/customControl_settings.jinja2 b/customControl/templates/customControl_settings.jinja2 new file mode 100644 index 0000000..011ece9 --- /dev/null +++ b/customControl/templates/customControl_settings.jinja2 @@ -0,0 +1,110 @@ +

{{ _('Control') }}

+ +
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c1f9b16 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +OctoPrint + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b26d51a --- /dev/null +++ b/setup.py @@ -0,0 +1,96 @@ +# coding=utf-8 +import setuptools + +######################################################################################################################## + +### Do not forget to adjust the following variables to your own plugin. + +# The plugin's identifier, has to be unique +plugin_identifier = "customControl" + +# The plugin's python package, should be "octoprint_", has to be unique +plugin_package = "octoprint_%s" % plugin_identifier + +# The plugin's human readable name. Can be overwritten within OctoPrint's internal data via __plugin_name__ in the +# plugin module +plugin_name = "OctoPrint-CustomControlPlugin" + +# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module +plugin_version = "0.1" + +# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin +# module +plugin_description = "" + +# The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module +plugin_author = "Marc Hannappel (Salandora)" + +# The plugin's author's mail address. +plugin_author_email = "sunpack@web.de" + +# The plugin's homepage URL. Can be overwritten within OctoPrint's internal data via __plugin_url__ in the plugin module +plugin_url = "" + +# The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module +plugin_license = "AGPLv3" + +# Additional package data to install for this plugin. The subfolders "templates", "static" and "translations" will +# already be installed automatically if they exist. +plugin_additional_data = [] + +######################################################################################################################## + +def package_data_dirs(source, sub_folders): + import os + dirs = [] + + for d in sub_folders: + folder = os.path.join(source, d) + if not os.path.exists(folder): + continue + + for dirname, _, files in os.walk(folder): + dirname = os.path.relpath(dirname, source) + for f in files: + dirs.append(os.path.join(dirname, f)) + + return dirs + + +def requirements(filename): + return filter(lambda line: line and not line.startswith("#"), map(lambda line: line.strip(), open(filename).read().split("\n"))) + + +def params(): + # Our metadata, as defined above + name = plugin_name + version = plugin_version + description = plugin_description + author = plugin_author + author_email = plugin_author_email + url = plugin_url + license = plugin_license + + # we only have our plugin package to install + packages = [plugin_package] + + # we might have additional data files in sub folders that need to be installed too + package_data = {plugin_package: package_data_dirs(plugin_package, ['static', 'templates', 'translations'] + plugin_additional_data)} + include_package_data = True + + # If you have any package data that needs to be accessible on the file system, such as templates or static assets + # this plugin is not zip_safe. + zip_safe = False + + # Read the requirements from our requirements.txt file + install_requires = requirements("requirements.txt") + + # Hook the plugin into the "octoprint.plugin" entry point, mapping the plugin_identifier to the plugin_package. + # That way OctoPrint will be able to find the plugin and load it. + entry_points = { + "octoprint.plugin": ["%s = %s" % (plugin_identifier, plugin_package)] + } + + return locals() + +setuptools.setup(**params()) \ No newline at end of file