diff --git a/ks_includes/KlipperScreen.conf b/ks_includes/KlipperScreen.conf index 686c3307..14c7d884 100644 --- a/ks_includes/KlipperScreen.conf +++ b/ks_includes/KlipperScreen.conf @@ -140,6 +140,11 @@ confirm: {{ gettext('Klipper will reboot') }} +[menu __main config settings] +name: {{ gettext('Settings') }} +icon: settings +panel: settings + [menu __print] name: {{ gettext('Print Control') }} diff --git a/ks_includes/config.py b/ks_includes/config.py index 062bf0a8..7bf2ba2a 100644 --- a/ks_includes/config.py +++ b/ks_includes/config.py @@ -16,27 +16,67 @@ class ConfigError(Exception): class KlipperScreenConfig: config = None configfile_name = "KlipperScreen.conf" + do_not_edit_line = "#~# --- Do not edit below this line. This section is auto generated --- #~#" + do_not_edit_prefix = "#~#" + + def __init__(self, configfile, lang=None): + _ = lang.gettext + + self.configurable_options = [ + {"invert_x": {"section": "main", "name": _("Invert X"), "type": "binary", "value": "False"}}, + {"invert_y": {"section": "main", "name": _("Invert Y"), "type": "binary", "value": "False"}}, + {"invert_z": {"section": "main", "name": _("Invert Z"), "type": "binary", "value": "False"}} + ] - def __init__(self, configfile): self.default_config_path = "%s/ks_includes/%s" % (os.getcwd(), self.configfile_name) self.config = configparser.ConfigParser() self.config_path = self.get_config_file_location(configfile) + logger.debug("Config path location: %s" % self.config_path) + self.defined_config = None try: self.config.read(self.default_config_path) if self.config_path != self.default_config_path: - + user_def, saved_def = self.separate_saved_config(self.config_path) self.defined_config = configparser.ConfigParser() - self.defined_config.read(self.config_path) + self.defined_config.read_string(user_def) self.log_config(self.defined_config) - - self.config.read(self.config_path) + self.config.read_string(user_def) + if saved_def != None: + self.config.read_string(saved_def) except KeyError: raise ConfigError(f"Error reading config: {self.config_path}") self.get_menu_items("__main") + for item in self.configurable_options: + name = list(item)[0] + vals = item[name] + if vals['section'] not in self.config.sections(): + self.config.add_section(vals['section']) + if name not in list(self.config[vals['section']]): + self.config.set(vals['section'], name, vals['value']) #self.build_main_menu(self.config) + def separate_saved_config(self, config_path): + user_def = [] + saved_def = None + found_saved = False + if not path.exists(config_path): + return [None, None] + with open(config_path) as file: + for line in file: + line = line.replace('\n','') + if line == self.do_not_edit_line: + found_saved = True + saved_def = [] + continue + if found_saved == False: + user_def.append(line.replace('\n','')) + else: + if line.startswith(self.do_not_edit_prefix): + saved_def.append(line[(len(self.do_not_edit_prefix)+1):]) + return ["\n".join(user_def), None if saved_def == None else "\n".join(saved_def)] + def get_config_file_location(self, file): if not path.exists(file): file = "%s/%s" % (os.getcwd(), self.configfile_name) @@ -46,6 +86,12 @@ class KlipperScreenConfig: logger.info("Found configuration file at: %s" % file) return file + def get_config(self): + return self.config + + def get_configurable_options(self): + return self.configurable_options + def get_main_config(self): return self.config['main'] @@ -56,7 +102,6 @@ class KlipperScreenConfig: if subsection != "": subsection = subsection + " " index = "menu %s %s" % (menu, subsection) - logger.debug("Getting menu items for: %s" % index) items = [i[len(index):] for i in self.config.sections() if i.startswith(index)] menu_items = [] for item in items: @@ -68,7 +113,6 @@ class KlipperScreenConfig: def get_menu_name(self, menu="__main", subsection=""): name = ("menu %s %s" % (menu, subsection)) if subsection != "" else ("menu %s" % menu) - logger.debug("Menu name: %s" % name) if name not in self.config: return False return self.config[name].get('name') @@ -77,7 +121,6 @@ class KlipperScreenConfig: def get_preheat_options(self): index = "preheat " items = [i[len(index):] for i in self.config.sections() if i.startswith(index)] - logger.debug("Items: %s" % items) preheat_options = {} for item in items: @@ -88,6 +131,63 @@ class KlipperScreenConfig: def get_printer_power_name(self): return self.config['settings'].get("printer_power_name", "printer") + def get_user_saved_config(self): + if self.config_path != self.default_config_path: + print("Get") + + def save_user_config_options(self): + save_config = configparser.ConfigParser() + for item in self.configurable_options: + name = list(item)[0] + opt = item[name] + curval = self.config[opt['section']].get(name) + if curval != opt["value"] or ( + self.defined_config != None and opt['section'] in self.defined_config.sections() and + self.defined_config[opt['section']].get(name,None) not in (None, curval)): + if opt['section'] not in save_config.sections(): + save_config.add_section(opt['section']) + save_config.set(opt['section'], name, str(curval)) + + if "displayed_macros" in self.config.sections(): + for item in self.config.options('displayed_macros'): + value = self.config['displayed_macros'].getboolean(item, fallback=True) + if value == False or (self.defined_config != None and + "displayed_macros" in self.defined_config.sections() and + self.defined_config['displayed_macros'].getboolean(item, fallback=True) == False and + self.defined_config['displayed_macros'].getboolean(item, fallback=True) != value): + if "displayed_macros" not in save_config.sections(): + save_config.add_section("displayed_macros") + save_config.set("displayed_macros", item, str(value)) + + save_output = self._build_config_string(save_config).split("\n") + for i in range(len(save_output)): + save_output[i] = "%s %s" % (self.do_not_edit_prefix, save_output[i]) + + if self.config_path == self.default_config_path: + user_def = "" + saved_def = None + else: + user_def, saved_def = self.separate_saved_config(self.config_path) + + extra_lb = "\n" if saved_def != None else "" + contents = "%s\n%s%s\n%s\n%s\n%s\n" % (user_def, self.do_not_edit_line, extra_lb, + self.do_not_edit_prefix, "\n".join(save_output), self.do_not_edit_prefix) + + if self.config_path != self.default_config_path: + path = self.config_path + else: + path = os.path.expanduser("~/KlipperScreen.conf") + + try: + file = open(path, 'w') + file.write(contents) + file.close() + except: + logger.error("Error writing configuration file") + + def set(self, section, name, value): + self.config.set(section, name, value) + def log_config(self, config): lines = [ diff --git a/panels/settings.py b/panels/settings.py new file mode 100644 index 00000000..3ccf37b1 --- /dev/null +++ b/panels/settings.py @@ -0,0 +1,181 @@ +import gi +import logging + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk, GLib, Pango + +from ks_includes.KlippyGcodes import KlippyGcodes +from ks_includes.screen_panel import ScreenPanel + +logger = logging.getLogger("KlipperScreen.Settings") + +def create_panel(*args): + return SettingsPanel(*args) + +class SettingsPanel(ScreenPanel): + def initialize(self, panel_name): + _ = self.lang.gettext + self.settings = {} + self.macros = {} + self.menu_cur = 'main_box' + self.menu = ['main_box'] + + self.labels['main_box'] = self.create_box('main') + self.labels['macros_box'] = self.create_box('macros') + + options = self._config.get_configurable_options() + for option in options: + name = list(option)[0] + self.add_option('main', self.settings, name, option[name]) + + self.add_option('main', self.settings, "macros", { + "name": _("Displayed Macros"), + "type": "menu", + "menu": "macros" + }) + + for macro in self._printer.get_config_section_list("gcode_macro "): + macro = macro[12:] + self.macros[macro] = { + "name": macro, + "section": "displayed_macros", + "type": "macro" + } + + for macro in list(self.macros): + self.add_option('macros', self.macros, macro, self.macros[macro]) + + logger.debug("Macros: %s" % self.macros) + + self.control['back'].disconnect_by_func(self._screen._menu_go_back) + self.control['back'].connect("clicked", self.back) + self.content.add(self.labels['main_box']) + + def activate(self): + while len(self.menu) > 1: + self.unload_menu() + + def back(self, widget): + if len(self.menu) > 1: + self.unload_menu() + else: + self._screen._menu_go_back() + + def create_box(self, name): + # Create a scroll window for the macros + scroll = Gtk.ScrolledWindow() + scroll.set_property("overlay-scrolling", False) + scroll.set_vexpand(True) + + # Create a grid for all macros + self.labels[name] = Gtk.Grid() + scroll.add(self.labels[name]) + + # Create a box to contain all of the above + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + box.set_vexpand(True) + box.pack_start(scroll, True, True, 0) + return box + + def add_option(self, boxname, opt_array, opt_name, option): + frame = Gtk.Frame() + frame.set_property("shadow-type",Gtk.ShadowType.NONE) + frame.get_style_context().add_class("frame-item") + + name = Gtk.Label() + name.set_markup("%s" % (option['name'])) + name.set_hexpand(True) + name.set_vexpand(True) + name.set_halign(Gtk.Align.START) + name.set_valign(Gtk.Align.CENTER) + name.set_line_wrap(True) + name.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) + + #open = self._gtk.ButtonImage("open",None,"color3") + #open.connect("clicked", self.run_gcode_macro, macro) + #open.set_hexpand(False) + #open.set_halign(Gtk.Align.END) + + labels = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + labels.add(name) + + dev = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + dev.set_hexpand(True) + dev.set_vexpand(False) + dev.set_valign(Gtk.Align.CENTER) + + dev.add(labels) + if option['type'] == "binary" or option['type'] == "macro": + box = Gtk.Box() + box.set_vexpand(False) + switch = Gtk.Switch() + switch.set_hexpand(False) + switch.set_vexpand(False) + if option['type'] == "macro": + switch.set_active(self._config.get_config().getboolean(option['section'], opt_name, fallback=True)) + else: + switch.set_active(self._config.get_config().getboolean(option['section'], opt_name)) + switch.connect("notify::active", self.switch_config_option, option['section'], opt_name) + switch.set_property("width-request", round(self._gtk.get_image_width()*2.5)) + switch.set_property("height-request", round(self._gtk.get_image_height()*1.25)) + box.add(switch) + dev.add(box) + elif option['type'] == "menu": + open = self._gtk.ButtonImage("open",None,"color3") + open.connect("clicked", self.load_menu, option['menu']) + open.set_hexpand(False) + open.set_halign(Gtk.Align.END) + dev.add(open) + + frame.add(dev) + + opt_array[opt_name] = { + "row": frame + } + + opts = sorted(opt_array) + pos = opts.index(opt_name) + + self.labels[boxname].insert_row(pos) + self.labels[boxname].attach(opt_array[opt_name]['row'], 0, pos, 1, 1) + self.labels[boxname].show_all() + + def load_menu(self, widget, name): + if ("%s_box" % name) not in self.labels: + return + + for child in self.content.get_children(): + self.content.remove(child) + + self.menu.append('%s_box' % name) + self.content.add(self.labels[self.menu[-1]]) + self.content.show_all() + + def unload_menu(self, widget=None): + logger.debug("self.menu: %s" % self.menu) + if len(self.menu) <= 1 or self.menu[-2] not in self.labels: + return + + self.menu.pop() + for child in self.content.get_children(): + self.content.remove(child) + self.content.add(self.labels[self.menu[-1]]) + self.content.show_all() + + def switch_config_option(self, switch, gparam, section, option): + logger.debug("[%s] %s toggled %s" % (section, option, switch.get_active())) + if section not in self._config.get_config().sections(): + self._config.get_config().add_section(section) + self._config.set(section, option, "True" if switch.get_active() else "False") + self._config.save_user_config_options() + + def add_gcode_option(self): + macros = self._screen.printer.get_gcode_macros() + for x in macros: + self.add_gcode_macro("macros", self.macros, x, { + "name": x[12:], + "type": binary + }) + + def run_gcode_macro(self, widget, macro): + self._screen._ws.klippy.gcode_script(macro) diff --git a/screen.py b/screen.py index 13ac3e1e..2b60a010 100644 --- a/screen.py +++ b/screen.py @@ -77,8 +77,8 @@ class KlipperScreen(Gtk.Window): args = parser.parse_args() configfile = os.path.normpath(os.path.expanduser(args.configfile)) - - self._config = KlipperScreenConfig(configfile) + self.lang = gettext.translation('KlipperScreen', localedir='ks_includes/locales', fallback=True) + self._config = KlipperScreenConfig(configfile, self.lang) self.printer = Printer({ "software_version": "Unknown" }, { @@ -92,6 +92,7 @@ class KlipperScreen(Gtk.Window): 'is_active': False } }) + self.printer.set_callbacks({ "disconnected": self.state_disconnected, "error": self.state_error, @@ -102,7 +103,7 @@ class KlipperScreen(Gtk.Window): }) logger.debug("OS Language: %s" % os.getenv('LANG')) - self.lang = gettext.translation('KlipperScreen', localedir='ks_includes/locales', fallback=True) + self.lang_ltr = True for lang in self.rtl_languages: if os.getenv('LANG').lower().startswith(lang): @@ -116,6 +117,10 @@ class KlipperScreen(Gtk.Window): self._config.get_main_config_option("moonraker_port"), self._config.get_main_config_option("moonraker_api_key", False)) + powerdevs = self.apiclient.send_request("machine/device_power/devices") + if powerdevs != False: + self.printer.configure_power_devices(powerdevs['result']) + Gtk.Window.__init__(self) self.width = self._config.get_main_config().getint("width", Gdk.Screen.get_width(Gdk.Screen.get_default())) self.height = self._config.get_main_config().getint("height", Gdk.Screen.get_height(Gdk.Screen.get_default()))