alfrix ad2df873d3 refactor printer initialization and connection handling
removes the lag while trying to connect
cleaner code, seems faster
2024-04-30 14:27:14 -03:00

418 lines
16 KiB
Python

import os.path
import pathlib
import logging
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GdkPixbuf, GObject, Pango, Gdk
from ks_includes.screen_panel import ScreenPanel
from ks_includes.KlippyRest import KlippyRest
from datetime import datetime
try:
from zoneinfo import ZoneInfo
except ImportError:
from backports.zoneinfo import ZoneInfo
def format_date(date):
try:
return datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%f').replace(tzinfo=ZoneInfo('UTC'))
except ValueError:
try:
return datetime.strptime(date, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=ZoneInfo('UTC'))
except ValueError:
return None
class SpoolmanVendor:
id: int
name: str
registered: datetime = None
def __init__(self, **entries):
self.__dict__.update(entries)
for date in ["registered"]:
if date in entries:
self.__setattr__(date, format_date(entries[date]))
class SpoolmanFilament:
article_number: str
color_hex: str
comment: str
density: float
diameter: float
id: int
material: str
name: str
price: float
registered: datetime = None
settings_bed_temp: int
settings_extruder_temp: int
spool_weight: float
vendor: SpoolmanVendor = None
weight: float
def __init__(self, **entries):
self.__dict__.update(entries)
if "vendor" in entries:
self.vendor = SpoolmanVendor(**(entries["vendor"]))
for date in ["registered"]:
if date in entries:
self.__setattr__(date, format_date(entries[date]))
class SpoolmanSpool(GObject.GObject):
archived: bool
id: int
remaining_length: float
remaining_weight: float
used_length: float
used_weight: float
lot_nr: str
filament: SpoolmanFilament = None
first_used: datetime = None
last_used: datetime = None
registered: datetime = None
_icon: Gtk.Image = None
theme_path: str = None
_spool_icon: str = None
def __init__(self, **entries):
GObject.GObject.__init__(self)
self.__dict__.update(entries)
if "filament" in entries:
self.filament = SpoolmanFilament(**(entries["filament"]))
for date in ["first_used", "last_used", "registered"]:
if date in entries:
self.__setattr__(date, format_date(entries[date]))
@property
def name(self):
result = self.filament.name
if self.filament.vendor:
result = " ".join([self.filament.vendor.name, "-", result])
return result
@property
def icon(self):
if self._icon is None:
if SpoolmanSpool._spool_icon is None:
klipperscreendir = pathlib.Path(__file__).parent.resolve().parent
_spool_icon_path = os.path.join(
klipperscreendir, "styles", SpoolmanSpool.theme_path, "images", "spool.svg"
)
if not os.path.isfile(_spool_icon_path):
_spool_icon_path = os.path.join(klipperscreendir, "styles", "spool.svg")
SpoolmanSpool._spool_icon = pathlib.Path(_spool_icon_path).read_text()
loader = GdkPixbuf.PixbufLoader()
color = self.filament.color_hex if hasattr(self.filament, 'color_hex') else '000000'
loader.write(
SpoolmanSpool._spool_icon.replace('var(--filament-color)', f'#{color}').encode()
)
loader.close()
self._icon = loader.get_pixbuf()
return self._icon
class Panel(ScreenPanel):
apiClient: KlippyRest
_active_spool_id: int = None
@staticmethod
def spool_compare_id(model, row1, row2, user_data):
spool1 = model.get_value(row1, 0)
spool2 = model.get_value(row2, 0)
return spool1.id - spool2.id
@staticmethod
def spool_compare_date(model, row1, row2, user_data):
spool1 = model.get_value(row1, 0)
spool2 = model.get_value(row2, 0)
return 1 if (spool1.last_used or datetime.min).replace(tzinfo=None) > \
(spool2.last_used or datetime.min).replace(tzinfo=None) else -1
def _on_material_filter_clear(self, sender, combobox):
self._filters["material"] = None
self._filterable.refilter()
self._filter_expander.set_expanded(False)
combobox.set_active_iter(self._materials.get_iter_first())
def _on_material_filter_changed(self, sender):
treeiter = sender.get_active_iter()
if treeiter is not None:
model = sender.get_model()
self._filters["material"] = model[treeiter][0]
self._filterable.refilter()
def __init__(self, screen, title):
super().__init__(screen, title)
self.apiClient = screen.apiclient
if self._config.get_main_config().getboolean("24htime", True):
self.timeFormat = '%Y-%m-%d %H:%M'
else:
self.timeFormat = '%Y-%m-%d %I:%M %p'
SpoolmanSpool.theme_path = screen.theme
GObject.type_register(SpoolmanSpool)
self._filters = {}
self._model = Gtk.TreeStore(SpoolmanSpool.__gtype__)
self._materials = Gtk.ListStore(str, str)
self._filterable = self._model.filter_new()
self._filterable.set_visible_func(self._filter_spools)
sortable = Gtk.TreeModelSort(self._filterable)
sortable.set_sort_func(0, self.spool_compare_id)
sortable.set_sort_func(1, self.spool_compare_date)
self.scroll = self._gtk.ScrolledWindow()
if self._screen.vertical_mode:
self.scroll.set_property("overlay-scrolling", True)
else:
self.scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
clear_active_spool = self._gtk.Button("cancel", _("Clear"), "color2", self.bts, Gtk.PositionType.LEFT, 1)
clear_active_spool.get_style_context().add_class("buttons_slim")
clear_active_spool.connect('clicked', self.clear_active_spool)
refresh = self._gtk.Button("refresh", style="color1", scale=.66)
refresh.get_style_context().add_class("buttons_slim")
refresh.connect('clicked', self.load_spools)
sort_btn_id = self._gtk.Button(None, _("ID"), "color4", self.bts, Gtk.PositionType.RIGHT, 1)
sort_btn_id.connect("clicked", self.change_sort, "id")
sort_btn_id.get_style_context().add_class("buttons_slim")
sort_btn_used = self._gtk.Button(None, _("Last Used"), "color3", self.bts, Gtk.PositionType.RIGHT, 1)
sort_btn_used.connect("clicked", self.change_sort, "last_used")
sort_btn_used.get_style_context().add_class("buttons_slim")
switch = Gtk.Switch(hexpand=False, vexpand=False)
switch.set_active(self._config.get_config().getboolean("spoolman", "hide_archived", fallback=True))
switch.connect("notify::active", self.switch_config_option, "spoolman", "hide_archived", self.load_spools)
name = Gtk.Label(halign=Gtk.Align.START, valign=Gtk.Align.CENTER, wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR)
name.set_markup(_("Archived"))
archived = Gtk.Box(valign=Gtk.Align.CENTER)
archived.add(name)
archived.add(switch)
sbox = Gtk.Box(hexpand=True, vexpand=False)
sbox.pack_start(sort_btn_id, True, True, 0)
sbox.pack_start(sort_btn_used, True, True, 0)
sbox.pack_start(clear_active_spool, True, True, 0)
sbox.pack_start(refresh, True, True, 0)
sbox.pack_start(archived, False, False, 5)
filter_box = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
self._filter_expander = Gtk.Expander(label=_("Filter"))
self._filter_expander.add(filter_box)
row = Gtk.ListBoxRow()
hbox = Gtk.Box(spacing=5)
row.add(hbox)
label = Gtk.Label(_("Material"))
_material_filter = Gtk.ComboBox(model=self._materials, hexpand=True)
_material_filter.connect("changed", self._on_material_filter_changed)
cellrenderertext = Gtk.CellRendererText()
_material_filter.pack_start(cellrenderertext, True)
_material_filter.add_attribute(cellrenderertext, "text", 1)
_material_reset_filter = self._gtk.Button("cancel", _("Clear"), "color2", self.bts, Gtk.PositionType.LEFT, 1)
_material_reset_filter.get_style_context().add_class("buttons_slim")
_material_reset_filter.connect('clicked', self._on_material_filter_clear, _material_filter)
hbox.pack_start(label, False, True, 0)
hbox.pack_start(_material_filter, True, True, 0)
hbox.pack_end(_material_reset_filter, False, True, 0)
filter_box.add(row)
self.main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, vexpand=True)
self.main.pack_start(sbox, False, False, 0)
self.main.pack_start(self._filter_expander, False, True, 0)
self.main.pack_start(self.scroll, True, True, 0)
self.load_spools()
self.get_active_spool()
self._treeview = Gtk.TreeView(model=sortable, headers_visible=False, show_expanders=False)
text_renderer = Gtk.CellRendererText(wrap_width=self._gtk.content_width / 4)
pixbuf_renderer = Gtk.CellRendererPixbuf(xpad=5, ypad=5)
checkbox_renderer = Gtk.CellRendererToggle()
column_id = Gtk.TreeViewColumn(cell_renderer=text_renderer)
column_id.set_cell_data_func(
text_renderer,
lambda column, cell, model, it, data:
self._set_cell_background(cell, model.get_value(it, 0)) and
cell.set_property('text', f'{model.get_value(it, 0).id}')
)
column_id.set_sort_column_id(0)
column_icon = Gtk.TreeViewColumn(cell_renderer=pixbuf_renderer)
column_icon.set_cell_data_func(
pixbuf_renderer,
lambda column, cell, model, it, data:
self._set_cell_background(cell, model.get_value(it, 0)) and
cell.set_property('pixbuf', model.get_value(it, 0).icon)
)
column_spool = Gtk.TreeViewColumn(cell_renderer=text_renderer)
column_spool.set_expand(True)
column_spool.set_cell_data_func(
text_renderer,
lambda column, cell, model, it, data:
self._set_cell_background(cell, model.get_value(it, 0)) and
cell.set_property('markup', self._get_filament_formated(model.get_value(it, 0)))
)
column_last_used = Gtk.TreeViewColumn(cell_renderer=text_renderer)
column_last_used.set_visible(False)
column_last_used.set_sort_column_id(1)
column_material = Gtk.TreeViewColumn(cell_renderer=text_renderer)
column_material.set_cell_data_func(
text_renderer,
lambda column, cell, model, it, data:
self._set_cell_background(cell, model.get_value(it, 0)) and
cell.set_property('text', model.get_value(it, 0).filament.material)
)
checkbox_renderer.connect("toggled", self._set_active_spool)
column_toggle_active_spool = Gtk.TreeViewColumn(cell_renderer=checkbox_renderer)
column_toggle_active_spool.set_cell_data_func(
checkbox_renderer,
lambda column, cell, model, it, data:
self._set_cell_background(cell, model.get_value(it, 0)) and
cell.set_property('active', model.get_value(it, 0).id == self._active_spool_id)
)
self._treeview.append_column(column_id)
self._treeview.append_column(column_icon)
self._treeview.append_column(column_spool)
self._treeview.append_column(column_last_used)
self._treeview.append_column(column_material)
self._treeview.append_column(column_toggle_active_spool)
self.current_sort_widget = sort_btn_id
sort_btn_used.clicked()
self.scroll.add(self._treeview)
self.content.add(self.main)
def _filter_spools(self, model, i, data):
spool: SpoolmanSpool = model[i][0]
matches = True
if ("material" in self._filters) and (self._filters["material"] is not None):
matches &= spool.filament.material == self._filters["material"]
return matches
def _set_cell_background(self, cell, spool: SpoolmanSpool):
cell.set_property('cell-background-rgba', Gdk.RGBA(1, 1, 1, .1) if spool.id == self._active_spool_id else None)
return True
def _get_filament_formated(self, spool: SpoolmanSpool):
if spool.id == self._active_spool_id:
result = f'<big><b>{spool.name}</b></big>\n'
else:
result = f'<big>{spool.name}</big>\n'
if spool.last_used:
result += f'{_("Last used")}:<b> {spool.last_used.astimezone():{self.timeFormat}}</b>\n'
if hasattr(spool, "remaining_weight"):
result += f'{_("Remaining weight")}: <b>{round(spool.remaining_weight, 2)} g</b>\n'
if hasattr(spool, "remaining_length"):
result += f'{_("Remaining length")}: <b>{round(spool.remaining_length / 1000, 2)} m</b>\n'
return result.strip()
def _set_active_spool(self, sender, path):
model = self._treeview.get_model()
it = model.get_iter(path)
spool = model.get_value(it, 0)
if spool.id == self._active_spool_id:
self.clear_active_spool()
else:
self.set_active_spool(spool)
def change_sort(self, widget, sort_type):
self.current_sort_widget.set_image(None)
self.current_sort_widget = widget
if sort_type == "id":
logging.info("Sorting by ID")
column = 0
elif sort_type == "last_used":
logging.info("Sorting by Last Used")
column = 1
else:
logging.error("Unknown sort type")
return
if self._treeview.get_column(column).get_sort_order() == Gtk.SortType.DESCENDING:
new_sort_order = Gtk.SortType.ASCENDING
else:
new_sort_order = Gtk.SortType.DESCENDING
self._treeview.get_column(column).set_sort_order(new_sort_order)
self._treeview.get_model().set_sort_column_id(column, new_sort_order)
icon = "arrow-down" if new_sort_order == Gtk.SortType.DESCENDING else "arrow-up"
widget.set_image(self._gtk.Image(icon, self._gtk.img_scale * self.bts))
def process_update(self, action, data):
if action == "notify_active_spool_set":
self._active_spool_id = data['spool_id']
self._treeview.get_model().foreach(lambda store, treepath, treeiter:
store.row_changed(treepath, treeiter)
)
self._treeview.queue_draw()
def load_spools(self, data=None):
hide_archived = self._config.get_config().getboolean("spoolman", "hide_archived", fallback=True)
self._model.clear()
self._materials.clear()
spools = self.apiClient.post_request("server/spoolman/proxy", json={
"request_method": "GET",
"path": f"/v1/spool?allow_archived={not hide_archived}",
})
if not spools or "result" not in spools:
self._screen.show_popup_message(_("Error trying to fetch spools"))
return
materials = []
for spool in spools["result"]:
spoolObject = SpoolmanSpool(**spool)
self._model.append(None, [spoolObject])
if not hasattr(spoolObject.filament, 'material'):
spoolObject.filament.material = ''
elif spoolObject.filament.material not in materials:
materials.append(spoolObject.filament.material)
materials.sort()
self._materials.append([None, _("All")])
for material in materials:
self._materials.append([material, material])
def clear_active_spool(self, sender: Gtk.Button = None):
result = self.apiClient.post_request("server/spoolman/spool_id", json={})
if not result:
self._screen.show_popup_message(_("Error clearing active spool"))
return
def set_active_spool(self, spool: SpoolmanSpool):
result = self.apiClient.post_request("server/spoolman/spool_id", json={
"spool_id": spool.id
})
if not result:
self._screen.show_popup_message(_("Error setting active spool"))
return
def get_active_spool(self) -> SpoolmanSpool:
result = self.apiClient.send_request("server/spoolman/spool_id")
if not result:
self._screen.show_popup_message(_("Error getting active spool"))
return
self._active_spool_id = result["spool_id"]
return self._active_spool_id