import logging import datetime import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Pango class ScreenPanel: _screen = None _config = None _files = None _printer = None _gtk = None ks_printer_cfg = None def __init__(self, screen, title, **kwargs): self.menu = None ScreenPanel._screen = screen ScreenPanel._config = screen._config ScreenPanel._files = screen.files ScreenPanel._printer = screen.printer ScreenPanel._gtk = screen.gtk self.labels = {} self.control = {} self.title = title self.devices = {} self.active_heaters = [] self.content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, hexpand=True, vexpand=True) self.content.get_style_context().add_class("content") self._show_heater_power = self._config.get_main_config().getboolean('show_heater_power', False) self.bts = self._gtk.bsidescale self.update_dialog = None def _autoscroll(self, scroll, *args): adj = scroll.get_vadjustment() adj.set_value(adj.get_upper() - adj.get_page_size()) def emergency_stop(self, widget): if self._config.get_main_config().getboolean('confirm_estop', False): self._screen._confirm_send_action(widget, _("Are you sure you want to run Emergency Stop?"), "printer.emergency_stop") else: self._screen._ws.klippy.emergency_stop() self._screen._ws.klippy.emergency_stop() def get_file_image(self, filename, width=None, height=None, small=False): if not self._files.has_thumbnail(filename): return None loc = self._files.get_thumbnail_location(filename, small) if loc is None: return None width = width if width is not None else self._gtk.img_width height = height if height is not None else self._gtk.img_height if loc[0] == "file": return self._gtk.PixbufFromFile(loc[1], width, height) if loc[0] == "http": return self._gtk.PixbufFromHttp(loc[1], width, height) return None def menu_item_clicked(self, widget, item): if 'extra' in item: self._screen.show_panel(item['panel'], item['name'], extra=item['extra']) return self._screen.show_panel(item['panel'], item['name']) def load_menu(self, widget, name, title=None): logging.info(f"loading menu {name}") if f"{name}_menu" not in self.labels: logging.error(f"{name} not in labels") return for child in self.content.get_children(): self.content.remove(child) self.menu.append(f'{name}_menu') self.content.add(self.labels[self.menu[-1]]) self.content.show_all() if title: self._screen.base_panel.set_title(f"{self.title} | {title}") def unload_menu(self, widget=None): logging.debug(f"self.menu: {self.menu}") if len(self.menu) <= 1 or self.menu[-2] not in self.labels: return self._screen.base_panel.set_title(self._screen.panels[self._screen._cur_panels[-1]].title) 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 on_dropdown_change(self, combo, section, option, callback=None): tree_iter = combo.get_active_iter() if tree_iter is not None: model = combo.get_model() value = model[tree_iter][1] logging.debug(f"[{section}] {option} changed to {value}") self._config.set(section, option, value) self._config.save_user_config_options() if callback is not None: callback(value) def scale_moved(self, widget, event, section, option): logging.debug(f"[{section}] {option} changed to {widget.get_value()}") if section not in self._config.get_config().sections(): self._config.get_config().add_section(section) self._config.set(section, option, str(int(widget.get_value()))) self._config.save_user_config_options() def switch_config_option(self, switch, gparam, section, option, callback=None): logging.debug(f"[{section}] {option} toggled {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() if callback is not None: callback(switch.get_active()) @staticmethod def format_time(seconds): if seconds is None or seconds < 1: return "-" days = seconds // 86400 seconds %= 86400 hours = seconds // 3600 seconds %= 3600 minutes = round(seconds / 60) seconds %= 60 return f"{f'{days:2.0f}d ' if days > 0 else ''}" \ f"{f'{hours:2.0f}h ' if hours > 0 else ''}" \ f"{f'{minutes:2.0f}m ' if minutes > 0 else ''}" \ f"{f'{seconds:2.0f}s' if days == 0 and hours == 0 and minutes == 0 else ''}" def format_eta(self, total, elapsed): if total is None: return "-" seconds = total - elapsed if seconds <= 0: return "-" days = seconds // 86400 seconds %= 86400 hours = seconds // 3600 seconds %= 3600 minutes = seconds // 60 eta = datetime.datetime.now() + datetime.timedelta(days=days, hours=hours, minutes=minutes) if self._config.get_main_config().getboolean("24htime", True): return f"{self.format_time(total - elapsed)} | {eta:%H:%M} {f' +{days:2.0f}d' if days > 0 else ''}" return f"{self.format_time(total - elapsed)} | {eta:%I:%M %p} {f' +{days:2.0f}d' if days > 0 else ''}" @staticmethod def format_size(size): size = float(size) suffixes = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] for i, suffix in enumerate(suffixes, start=2): unit = 1024 ** i if size < unit: return f"{(1024 * size / unit):.1f} {suffix}" @staticmethod def prettify(name: str): name = name.replace("_", " ") if name.islower(): name = name.title() return name def update_temp(self, dev, temp, target, power, lines=1): if temp is None: return show_target = bool(target) if dev in self.devices and not self.devices[dev]["can_target"]: show_target = False show_power = show_target and self._show_heater_power and power is not None new_label_text = f"{int(temp):3}" if show_target: new_label_text += f"/{int(target)}" if dev not in self.devices: new_label_text += "°" if show_power: if lines == 2: # The label should wrap, but it doesn't work # this is a workaround new_label_text += "\n " new_label_text += f" {int(power * 100):3}%" if dev in self.labels: self.labels[dev].set_label(new_label_text) if show_power: self.labels[dev].get_style_context().add_class("heater-grid-temp-power") else: self.labels[dev].get_style_context().remove_class("heater-grid-temp-power") elif dev in self.devices: self.devices[dev]["temp"].get_child().set_label(new_label_text) def add_option(self, boxname, opt_array, opt_name, option): if option['type'] is None: return name = Gtk.Label( hexpand=True, vexpand=True, halign=Gtk.Align.START, valign=Gtk.Align.CENTER, wrap=True, wrap_mode=Pango.WrapMode.CHAR) name.set_markup(f"{option['name']}") labels = Gtk.Box(spacing=0, orientation=Gtk.Orientation.VERTICAL, valign=Gtk.Align.CENTER) labels.add(name) if 'tooltip' in option: tooltip = Gtk.Label( label=option['tooltip'], hexpand=True, vexpand=True, halign=Gtk.Align.START, valign=Gtk.Align.CENTER, wrap=True, wrap_mode=Pango.WrapMode.CHAR) labels.add(tooltip) row_box = Gtk.Box(spacing=5, valign=Gtk.Align.CENTER, hexpand=True, vexpand=False) row_box.get_style_context().add_class("frame-item") row_box.add(labels) setting = {} if option['type'] == "binary": switch = Gtk.Switch(active=self._config.get_config().getboolean(option['section'], opt_name, fallback=True)) switch.set_vexpand(False) switch.set_valign(Gtk.Align.CENTER) switch.connect("notify::active", self.switch_config_option, option['section'], opt_name, option['callback'] if "callback" in option else None) row_box.add(switch) setting = {opt_name: switch} elif option['type'] == "dropdown": dropdown = Gtk.ComboBoxText() for i, opt in enumerate(option['options']): dropdown.append(opt['value'], opt['name']) if opt['value'] == self._config.get_config()[option['section']].get(opt_name, option['value']): dropdown.set_active(i) dropdown.connect("changed", self.on_dropdown_change, option['section'], opt_name, option['callback'] if "callback" in option else None) dropdown.set_entry_text_column(0) row_box.add(dropdown) setting = {opt_name: dropdown} elif option['type'] == "scale": row_box.set_orientation(Gtk.Orientation.VERTICAL) scale = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, min=option['range'][0], max=option['range'][1], step=option['step']) scale.set_hexpand(True) scale.set_value(int(self._config.get_config().get(option['section'], opt_name, fallback=option['value']))) scale.set_digits(0) scale.connect("button-release-event", self.scale_moved, option['section'], opt_name) row_box.add(scale) setting = {opt_name: scale} elif option['type'] == "printer": box = Gtk.Box(vexpand=False) label = Gtk.Label(f"{option['moonraker_host']}:{option['moonraker_port']}") box.add(label) row_box.add(box) elif option['type'] == "menu": open_menu = self._gtk.Button("settings", style="color3") open_menu.connect("clicked", self.load_menu, option['menu'], option['name']) open_menu.set_hexpand(False) open_menu.set_halign(Gtk.Align.END) row_box.add(open_menu) elif option['type'] == "lang": select = self._gtk.Button("load", style="color3") select.connect("clicked", self._screen.change_language, option['name']) select.set_hexpand(False) select.set_halign(Gtk.Align.END) row_box.add(select) opt_array[opt_name] = { "name": option['name'], "row": row_box } opts = sorted(list(opt_array), key=lambda x: opt_array[x]['name'].casefold()) 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() return setting