"""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("", self._on_click) self.table.bind("", 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("<>", commit) combo.bind("", lambda _e: self._close_editor()) combo.focus_set() combo.after_idle(lambda: combo.event_generate("")) 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