A kits step after the zip is chosen, laid out as the Manage Kits panel: installed kits (what the app needs, greyed), installed but not used, kits available on the computer, not supported; the Action cell offers Keep at / Upgrade to / Downgrade to / Add at / REMOVE; Upgrade all kits to latest version. Kits not in the backup can be added, with their dependencies. The ticked kits make app.sab's schema (encode_sax takes the kit list, as Ontrol's PutApp rewrites it). When they are not the device's kits, or Rebuild kits.scode is ticked, kits.scode is built for them with Ontrol's sedonac from the Niagara install, after checking the natives the kits call against the device's platform manifest (PutApp.checkNativeCompatibility). - kit_build.py: platform manifest lookup, native check, sedonac run - palette.kit_versions: a checksum released more than once stands for its highest version (ontrolPlatform 1.0.1, not 1.0.0) - palette.unsupported_kits takes the kit list to judge against - sab_app: a type from a kit the app does not list is a clear error Tested on a P-ION: a rebuild for its own kits is byte-identical to the one it runs; adding and then removing the logic kit both come back running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
243 lines
9.7 KiB
Python
243 lines
9.7 KiB
Python
"""The kits step of App > Put with Files, laid out as the Manage Kits panel is.
|
|
|
|
The same groups (kits the app needs, kits declared without being used, kits available,
|
|
kits not supported), the same tick column and the same Action cell: "Keep at",
|
|
"Upgrade to", "Downgrade to", "Add at", "REMOVE". Installed is what the device runs;
|
|
Backup is what the backup's app was saved with.
|
|
|
|
What is ticked goes: into app.sab's schema and, when that is not the device's kits,
|
|
into a kits.scode built for them (Ontrol's PutApp does the same). "Rebuild kits.scode"
|
|
forces the build when nothing changed.
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import messagebox, ttk
|
|
|
|
import app_backup
|
|
import manage_kits
|
|
from manage_kits import NOT_DECLARED, SECTION_TAG, column_label, make_checkbox_image
|
|
|
|
REMOVE_ACTION = "REMOVE from device"
|
|
|
|
|
|
def _options(row):
|
|
"""The Action choices for a row, as (label, checksum) pairs."""
|
|
if row.device:
|
|
return [(label, checksum or row.device) for label, checksum in
|
|
manage_kits.action_options(row.name, row.device, True, row.builds)]
|
|
return [(f"Add at {manage_kits.build_label(v, c)}", c) for v, c in row.builds]
|
|
|
|
|
|
def _build_text(row, checksum):
|
|
if not checksum:
|
|
return NOT_DECLARED
|
|
version = next((v for v, c in row.builds if c == checksum), "")
|
|
return column_label(version, checksum)
|
|
|
|
|
|
class _Dialog:
|
|
def __init__(self, parent, rows, title):
|
|
self.rows = rows
|
|
self.result = None
|
|
self.original = [(r.included, r.use) for r in rows]
|
|
|
|
self.top = top = tk.Toplevel(parent)
|
|
top.title(f"{title} - Kits")
|
|
top.transient(parent)
|
|
top.grab_set()
|
|
top.geometry(f"+{parent.winfo_x() + 60}+{parent.winfo_y() + 40}")
|
|
frame = ttk.Frame(top, padding=10)
|
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
columns = ("name", "installed", "backup", "latest", "action")
|
|
table_frame = ttk.Frame(frame)
|
|
table_frame.pack(fill=tk.BOTH, expand=True)
|
|
self.table = ttk.Treeview(table_frame, columns=columns, show="tree headings",
|
|
selectmode="browse", height=20)
|
|
self.table.heading("#0", text="")
|
|
self.table.column("#0", width=32, minwidth=32, stretch=False, anchor="center")
|
|
for column, heading, width in [("name", "Kit name", 190),
|
|
("installed", "Installed", 105),
|
|
("backup", "Backup", 105),
|
|
("latest", "Latest", 105),
|
|
("action", "Action", 215)]:
|
|
self.table.heading(column, text=heading, anchor="w")
|
|
self.table.column(column, width=width, minwidth=width, anchor="w", stretch=False)
|
|
vscroll = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.table.yview)
|
|
self.table.configure(yscrollcommand=vscroll.set)
|
|
vscroll.pack(side=tk.RIGHT, fill=tk.Y)
|
|
self.table.pack(fill=tk.BOTH, expand=True)
|
|
self.table.tag_configure(SECTION_TAG, background="#dcdcdc")
|
|
|
|
self.boxes = {(checked, enabled): make_checkbox_image(checked, enabled)
|
|
for checked in (True, False) for enabled in (True, False)}
|
|
self.cross_box = make_checkbox_image(False, False, cross=True)
|
|
|
|
locked = any(r.locked for r in rows)
|
|
hint = ("The backup holds no app.sax, so its app goes with the kits it was saved "
|
|
"with." if locked else
|
|
"A tick means the kit goes to the device. Grey ticks are needed by the app "
|
|
"and cannot be removed; untick another to remove it, tick one to add it. "
|
|
"Click the Action cell to choose a build. Installed is the device's build. "
|
|
"Kits other than the device's mean a new kits.scode.")
|
|
ttk.Label(frame, text=hint, foreground="#666666", wraplength=780
|
|
).pack(anchor="w", pady=(8, 0))
|
|
|
|
self.rebuild = tk.BooleanVar(value=False)
|
|
self.rebuild_check = ttk.Checkbutton(frame, text="Rebuild kits.scode",
|
|
variable=self.rebuild)
|
|
self.rebuild_check.pack(anchor="w", pady=(6, 0))
|
|
|
|
button_row = ttk.Frame(frame)
|
|
button_row.pack(fill=tk.X, pady=(10, 0))
|
|
upgrade = ttk.Button(button_row, text="Upgrade all kits to latest version",
|
|
command=self._upgrade_all)
|
|
upgrade.pack(side=tk.LEFT)
|
|
if locked:
|
|
upgrade.state(["disabled"])
|
|
ttk.Button(button_row, text="Cancel", command=lambda: self._close(False)
|
|
).pack(side=tk.RIGHT)
|
|
ttk.Button(button_row, text="Next", command=lambda: self._close(True)
|
|
).pack(side=tk.RIGHT, padx=6)
|
|
|
|
self.row_by_item = {}
|
|
self._editor = None
|
|
self.table.bind("<Button-1>", self._on_click)
|
|
self.table.bind("<MouseWheel>", lambda _e: self._close_editor(), add="+")
|
|
top.protocol("WM_DELETE_WINDOW", lambda: self._close(False))
|
|
self._rebuild_table()
|
|
|
|
# --- the table ---
|
|
|
|
def _action_text(self, row):
|
|
if row.unsupported:
|
|
return "Not supported"
|
|
if not row.included:
|
|
return REMOVE_ACTION if row.device else NOT_DECLARED
|
|
return next((label for label, c in _options(row) if c == row.use),
|
|
_build_text(row, row.use))
|
|
|
|
def _values(self, row):
|
|
latest = row.builds[0] if row.builds else None
|
|
return (row.name, _build_text(row, row.device), _build_text(row, row.backup),
|
|
column_label(*latest) if latest else NOT_DECLARED, self._action_text(row))
|
|
|
|
def _image(self, row):
|
|
if row.unsupported:
|
|
return self.cross_box
|
|
return self.boxes[(row.included, not (row.required or row.locked))]
|
|
|
|
def _rebuild_table(self):
|
|
self._close_editor()
|
|
top = self.table.yview()[0]
|
|
self.table.delete(*self.table.get_children())
|
|
self.row_by_item.clear()
|
|
for title in app_backup.KIT_GROUPS:
|
|
members = [r for r in self.rows if app_backup.group(r) == title]
|
|
if not members:
|
|
continue
|
|
self.table.insert("", "end", values=(title, "", "", "", ""), tags=(SECTION_TAG,))
|
|
for row in members:
|
|
item = self.table.insert("", "end", image=self._image(row),
|
|
values=self._values(row))
|
|
self.row_by_item[item] = row
|
|
self.table.yview_moveto(top)
|
|
|
|
# Kits other than the device's cannot run on its kits.scode.
|
|
changed = any(app_backup.kit_change(r) for r in self.rows)
|
|
if changed:
|
|
self.rebuild.set(True)
|
|
self.rebuild_check.state(["disabled"])
|
|
else:
|
|
self.rebuild_check.state(["!disabled"])
|
|
|
|
def _changed(self):
|
|
app_backup.settle_required(self.rows)
|
|
self._rebuild_table()
|
|
|
|
# --- editing ---
|
|
|
|
def _close_editor(self):
|
|
if self._editor is not None:
|
|
self._editor.destroy()
|
|
self._editor = None
|
|
|
|
def _on_click(self, event):
|
|
self._close_editor()
|
|
if self.table.identify_region(event.x, event.y) not in ("tree", "cell"):
|
|
return
|
|
item = self.table.identify_row(event.y)
|
|
row = self.row_by_item.get(item)
|
|
if row is None or row.locked or row.unsupported:
|
|
return
|
|
column = self.table.identify_column(event.x)
|
|
if column == "#0":
|
|
self._toggle(row)
|
|
elif column == "#5":
|
|
self._edit_action(item, row)
|
|
|
|
def _toggle(self, row):
|
|
if row.required:
|
|
return
|
|
if not row.included and not row.builds:
|
|
messagebox.showwarning("Put with Files", f"Sedona home holds no build of "
|
|
f"'{row.name}' to add it at.", parent=self.top)
|
|
return
|
|
row.included = not row.included
|
|
if row.included and not row.use:
|
|
row.use = row.latest
|
|
self._changed()
|
|
|
|
def _edit_action(self, item, row):
|
|
options = _options(row)
|
|
if not options:
|
|
return
|
|
cell = self.table.bbox(item, "action")
|
|
if not cell:
|
|
return
|
|
labels = [label for label, _ in options]
|
|
variable = tk.StringVar(value=self._action_text(row))
|
|
combo = ttk.Combobox(self.table, textvariable=variable, values=labels,
|
|
state="readonly")
|
|
combo.place(x=cell[0], y=cell[1], width=cell[2], height=cell[3])
|
|
self._editor = combo
|
|
|
|
def commit(_event=None):
|
|
if variable.get() in labels:
|
|
row.use = options[labels.index(variable.get())][1]
|
|
row.included = True
|
|
self._close_editor()
|
|
self._changed()
|
|
|
|
combo.bind("<<ComboboxSelected>>", commit)
|
|
combo.bind("<Escape>", lambda _e: self._close_editor())
|
|
combo.focus_set()
|
|
combo.after_idle(lambda: combo.event_generate("<Down>"))
|
|
|
|
def _upgrade_all(self):
|
|
for row in self.rows:
|
|
if row.included and row.latest and not row.locked:
|
|
row.use = row.latest
|
|
self._changed()
|
|
|
|
def _close(self, ok):
|
|
self._close_editor()
|
|
if ok:
|
|
self.result = bool(self.rebuild.get())
|
|
else:
|
|
for row, (included, use) in zip(self.rows, self.original):
|
|
row.included, row.use = included, use
|
|
app_backup.settle_required(self.rows)
|
|
self.top.grab_release()
|
|
self.top.destroy()
|
|
|
|
|
|
def ask(parent, rows, title="Put with Files"):
|
|
"""Shows the kits step; returns whether to rebuild kits.scode, or None when cancelled.
|
|
|
|
The rows are updated in place with what was chosen.
|
|
"""
|
|
dialog = _Dialog(parent, rows, title)
|
|
dialog.top.wait_window()
|
|
return dialog.result
|