Manage Kits as embedded panel, fix checksum zero-padding mismatch
Manage Kits now opens inside the editor pane (like Properties and Link View) instead of a separate Toplevel window. Also normalize hex checksums by stripping leading zeros so .kit and manifest filenames that disagree on padding (e.g. 462d43e vs 0462d43e) still resolve to a version. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
|||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
|
||||||
VERSION = "0.0.0.058"
|
VERSION = "0.0.0.060"
|
||||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||||
DEFAULT_GRID_SIZE = 10
|
DEFAULT_GRID_SIZE = 10
|
||||||
DEFAULT_ADD_TIMESTAMP = True
|
DEFAULT_ADD_TIMESTAMP = True
|
||||||
|
|||||||
@@ -111,11 +111,12 @@ zoom_label = None
|
|||||||
menu_view = None
|
menu_view = None
|
||||||
show_left_column_var = None
|
show_left_column_var = None
|
||||||
|
|
||||||
# The Properties and Link views each take the editor pane over from the canvas rather
|
# The Properties, Link and Kits views each take the editor pane over from the canvas
|
||||||
# than floating above it, so at most one of the three is on screen and these two say
|
# rather than floating above it, so at most one of the four is on screen and these three
|
||||||
# which. Both None means the sheet is showing.
|
# say which. All None means the sheet is showing.
|
||||||
properties_panel = None
|
properties_panel = None
|
||||||
link_panel = None
|
link_panel = None
|
||||||
|
kits_panel = None
|
||||||
|
|
||||||
# The sheet to put the tree back on when a sheet change is refused because of unsaved
|
# The sheet to put the tree back on when a sheet change is refused because of unsaved
|
||||||
# edits, and the flag that stops the restoring selection from being read as a fresh
|
# edits, and the flag that stops the restoring selection from being read as a fresh
|
||||||
@@ -204,7 +205,7 @@ def delete_component(comp):
|
|||||||
|
|
||||||
def editor_panel_showing():
|
def editor_panel_showing():
|
||||||
"""Whether something other than the wiresheet has the editor pane."""
|
"""Whether something other than the wiresheet has the editor pane."""
|
||||||
return properties_panel is not None or link_panel is not None
|
return properties_panel is not None or link_panel is not None or kits_panel is not None
|
||||||
|
|
||||||
|
|
||||||
def _take_editor_pane():
|
def _take_editor_pane():
|
||||||
@@ -316,17 +317,54 @@ def dismiss_link_view(restore_canvas=True):
|
|||||||
_give_editor_pane_back()
|
_give_editor_pane_back()
|
||||||
|
|
||||||
|
|
||||||
|
def show_kits():
|
||||||
|
"""Puts the kit table in the editor pane, in place of the wiresheet."""
|
||||||
|
global kits_panel
|
||||||
|
if app_state.xml_root_element is None:
|
||||||
|
messagebox.showinfo("Manage Kits", "Open a SAX file first.")
|
||||||
|
return
|
||||||
|
if app_state.xml_root_element.find("schema") is None:
|
||||||
|
messagebox.showwarning("Manage Kits", "This file has no <schema> block to manage.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not close_editor_panel():
|
||||||
|
return
|
||||||
|
|
||||||
|
_take_editor_pane()
|
||||||
|
kits_panel = manage_kits.build_kits_panel(
|
||||||
|
frame_editor, on_applied=refresh_palette_kits, on_close=dismiss_kits,
|
||||||
|
)
|
||||||
|
kits_panel.frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
app_state.set_status("Manage Kits")
|
||||||
|
|
||||||
|
|
||||||
|
def dismiss_kits(restore_canvas=True):
|
||||||
|
"""Takes the Kits view down and gives the pane back."""
|
||||||
|
global kits_panel
|
||||||
|
if kits_panel is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
kits_panel.destroy()
|
||||||
|
kits_panel = None
|
||||||
|
|
||||||
|
if restore_canvas:
|
||||||
|
_give_editor_pane_back()
|
||||||
|
|
||||||
|
|
||||||
def close_editor_panel(force=False):
|
def close_editor_panel(force=False):
|
||||||
"""Frees the editor pane, asking about unsaved edits first. True if it is now free.
|
"""Frees the editor pane, asking about unsaved edits first. True if it is now free.
|
||||||
|
|
||||||
Every way of leaving a panel goes through this, so "you have not saved that" is
|
Every way of leaving a panel goes through this, so "you have not saved that" is
|
||||||
asked once, in one place, whether the user pressed Close, picked another sheet,
|
asked once, in one place, whether the user pressed Close, picked another sheet,
|
||||||
walked the history or opened a different component. Only the Properties view has
|
walked the history or opened a different component. Only the Properties view has
|
||||||
anything to ask about; the Link View simply stands down.
|
anything to ask about; other views simply stand down.
|
||||||
"""
|
"""
|
||||||
if link_panel is not None:
|
if link_panel is not None:
|
||||||
dismiss_link_view()
|
dismiss_link_view()
|
||||||
return True
|
return True
|
||||||
|
if kits_panel is not None:
|
||||||
|
dismiss_kits()
|
||||||
|
return True
|
||||||
if properties_panel is None:
|
if properties_panel is None:
|
||||||
return True
|
return True
|
||||||
if force:
|
if force:
|
||||||
@@ -1919,7 +1957,7 @@ menu_bar.add_cascade(label="View", menu=menu_view)
|
|||||||
menu_kits = tk.Menu(menu_bar, tearoff=0)
|
menu_kits = tk.Menu(menu_bar, tearoff=0)
|
||||||
menu_kits.add_command(
|
menu_kits.add_command(
|
||||||
label="Manage Kits...",
|
label="Manage Kits...",
|
||||||
command=lambda: manage_kits.open_dialog(on_applied=refresh_palette_kits)
|
command=show_kits
|
||||||
)
|
)
|
||||||
menu_bar.add_cascade(label="Kits", menu=menu_kits)
|
menu_bar.add_cascade(label="Kits", menu=menu_kits)
|
||||||
|
|
||||||
|
|||||||
+134
-143
@@ -203,128 +203,134 @@ def describe_changes(changes):
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def open_dialog(on_applied=None):
|
class KitsPanel:
|
||||||
"""Opens the kit table. ``on_applied`` is called after the schema really changed."""
|
"""The kit table, built as an embedded panel in the editor pane."""
|
||||||
window = app_state.window
|
|
||||||
|
|
||||||
if app_state.xml_root_element is None:
|
def __init__(self, parent, on_applied, on_close):
|
||||||
messagebox.showinfo("Manage Kits", "Open a SAX file first.")
|
self.on_applied = on_applied
|
||||||
return
|
self.on_close = on_close
|
||||||
if app_state.xml_root_element.find("schema") is None:
|
|
||||||
messagebox.showwarning("Manage Kits", "This file has no <schema> block to manage.")
|
|
||||||
return
|
|
||||||
|
|
||||||
dialog = tk.Toplevel(window)
|
self.frame = ttk.Frame(parent, padding=10)
|
||||||
dialog.title("Kits")
|
|
||||||
dialog.transient(window)
|
|
||||||
dialog.grab_set()
|
|
||||||
dialog.geometry(f"820x520+{window.winfo_x() + 60}+{window.winfo_y() + 40}")
|
|
||||||
dialog.minsize(640, 320)
|
|
||||||
|
|
||||||
frame = ttk.Frame(dialog, padding=10)
|
COLUMNS = ("name", "installed", "latest", "action")
|
||||||
frame.pack(fill=tk.BOTH, expand=True)
|
self.table = ttk.Treeview(
|
||||||
|
self.frame, columns=COLUMNS, show="tree headings", selectmode="browse",
|
||||||
|
)
|
||||||
|
self.table.heading("#0", text="")
|
||||||
|
self.table.column("#0", width=32, minwidth=32, stretch=False, anchor="center")
|
||||||
|
for column, heading, width, anchor in [
|
||||||
|
("name", "Kit name", 200, "w"),
|
||||||
|
("installed", "Installed", 105, "w"),
|
||||||
|
("latest", "Latest", 105, "w"),
|
||||||
|
("action", "Action", 300, "w"),
|
||||||
|
]:
|
||||||
|
self.table.heading(column, text=heading, anchor=anchor)
|
||||||
|
self.table.column(column, width=width, anchor=anchor, stretch=(column == "action"))
|
||||||
|
|
||||||
COLUMNS = ("name", "installed", "latest", "action")
|
scrollbar = ttk.Scrollbar(self.frame, orient=tk.VERTICAL, command=self.table.yview)
|
||||||
# 'tree headings' keeps the icon column, which is the only place a real checkbox image
|
self.table.configure(yscrollcommand=scrollbar.set)
|
||||||
# can live; the data columns follow it.
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
table = ttk.Treeview(frame, columns=COLUMNS, show="tree headings", selectmode="browse")
|
self.table.pack(fill=tk.BOTH, expand=True)
|
||||||
table.heading("#0", text="")
|
|
||||||
table.column("#0", width=32, minwidth=32, stretch=False, anchor="center")
|
|
||||||
for column, heading, width, anchor in [
|
|
||||||
("name", "Kit name", 200, "w"),
|
|
||||||
("installed", "Installed", 105, "w"),
|
|
||||||
("latest", "Latest", 105, "w"),
|
|
||||||
("action", "Action", 300, "w"),
|
|
||||||
]:
|
|
||||||
table.heading(column, text=heading, anchor=anchor)
|
|
||||||
table.column(column, width=width, anchor=anchor, stretch=(column == "action"))
|
|
||||||
|
|
||||||
scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=table.yview)
|
self.table.tag_configure(SECTION_TAG, background="#dcdcdc")
|
||||||
table.configure(yscrollcommand=scrollbar.set)
|
|
||||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
||||||
table.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
table.tag_configure(SECTION_TAG, background="#dcdcdc")
|
self.boxes = {
|
||||||
|
(True, True): make_checkbox_image(True, True),
|
||||||
|
(False, True): make_checkbox_image(False, True),
|
||||||
|
(True, False): make_checkbox_image(True, False),
|
||||||
|
}
|
||||||
|
|
||||||
# Held on the dialog: a PhotoImage is dropped as soon as nothing references it.
|
hint = ttk.Label(
|
||||||
boxes = {
|
self.frame,
|
||||||
(True, True): make_checkbox_image(True, True),
|
text="A tick means the schema declares the kit. Grey ticks are needed by the app "
|
||||||
(False, True): make_checkbox_image(False, True),
|
"and cannot be removed; untick another to remove it. Click the Action cell to "
|
||||||
(True, False): make_checkbox_image(True, False),
|
"choose a build. Nothing reaches the file until you save it.",
|
||||||
}
|
foreground="#666666",
|
||||||
|
wraplength=780,
|
||||||
|
)
|
||||||
|
hint.pack(anchor="w", pady=(8, 0))
|
||||||
|
|
||||||
hint = ttk.Label(
|
button_row = ttk.Frame(self.frame)
|
||||||
frame,
|
button_row.pack(fill=tk.X, pady=(10, 0))
|
||||||
text="A tick means the schema declares the kit. Grey ticks are needed by the app "
|
|
||||||
"and cannot be removed; untick another to remove it. Click the Action cell to "
|
|
||||||
"choose a build. Nothing reaches the file until you save it.",
|
|
||||||
foreground="#666666",
|
|
||||||
wraplength=780,
|
|
||||||
)
|
|
||||||
hint.pack(anchor="w", pady=(8, 0))
|
|
||||||
|
|
||||||
button_row = ttk.Frame(frame)
|
ttk.Button(
|
||||||
button_row.pack(fill=tk.X, pady=(10, 0))
|
button_row, text="Upgrade all kits to latest version", command=self._upgrade_all,
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
ttk.Button(
|
||||||
|
button_row, text="Update kits", command=self._update_marked,
|
||||||
|
).pack(side=tk.LEFT, padx=6)
|
||||||
|
ttk.Button(button_row, text="Close", command=self.close).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
rows = [] # row dicts, table order
|
self.rows = []
|
||||||
row_by_item = {} # Treeview item id -> row dict
|
self.row_by_item = {}
|
||||||
editor = {"widget": None}
|
self._editor_widget = None
|
||||||
|
|
||||||
def close_editor(_event=None):
|
self.table.bind("<Button-1>", self._on_click)
|
||||||
if editor["widget"] is not None:
|
self.table.bind("<MouseWheel>", lambda _e: self._close_editor(), add="+")
|
||||||
editor["widget"].destroy()
|
self.table.bind("<Configure>", lambda _e: self._close_editor(), add="+")
|
||||||
editor["widget"] = None
|
|
||||||
|
|
||||||
def action_text(row):
|
self._rebuild_table()
|
||||||
"""What the Action cell says, which an untick overrides with REMOVE."""
|
|
||||||
|
def close(self):
|
||||||
|
if self.on_close is not None:
|
||||||
|
self.on_close()
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
self._close_editor()
|
||||||
|
self.frame.destroy()
|
||||||
|
|
||||||
|
def _close_editor(self):
|
||||||
|
if self._editor_widget is not None:
|
||||||
|
self._editor_widget.destroy()
|
||||||
|
self._editor_widget = None
|
||||||
|
|
||||||
|
def _action_text(self, row):
|
||||||
if row["declared"] and not row["checked"]:
|
if row["declared"] and not row["checked"]:
|
||||||
return REMOVE_ACTION
|
return REMOVE_ACTION
|
||||||
return row["options"][row["choice"]][0]
|
return row["options"][row["choice"]][0]
|
||||||
|
|
||||||
def row_values(row):
|
def _row_values(self, row):
|
||||||
return (row["kit"], row["installed_text"], row["latest_text"], action_text(row))
|
return (row["kit"], row["installed_text"], row["latest_text"], self._action_text(row))
|
||||||
|
|
||||||
def row_image(row):
|
def _row_image(self, row):
|
||||||
return boxes[(row["checked"], not row["required"])]
|
return self.boxes[(row["checked"], not row["required"])]
|
||||||
|
|
||||||
def rebuild_table():
|
def _rebuild_table(self):
|
||||||
close_editor()
|
self._close_editor()
|
||||||
for item in table.get_children():
|
for item in self.table.get_children():
|
||||||
table.delete(item)
|
self.table.delete(item)
|
||||||
row_by_item.clear()
|
self.row_by_item.clear()
|
||||||
|
|
||||||
rows[:] = collect_rows()
|
self.rows[:] = collect_rows()
|
||||||
for row in rows:
|
for row in self.rows:
|
||||||
if "section" in row:
|
if "section" in row:
|
||||||
table.insert("", "end", values=(row["section"], "", "", ""), tags=(SECTION_TAG,))
|
self.table.insert(
|
||||||
|
"", "end", values=(row["section"], "", "", ""), tags=(SECTION_TAG,),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
item = table.insert("", "end", image=row_image(row), values=row_values(row))
|
item = self.table.insert(
|
||||||
row_by_item[item] = row
|
"", "end", image=self._row_image(row), values=self._row_values(row),
|
||||||
|
)
|
||||||
|
self.row_by_item[item] = row
|
||||||
|
|
||||||
def refresh_row(item):
|
def _refresh_row(self, item):
|
||||||
row = row_by_item[item]
|
row = self.row_by_item[item]
|
||||||
table.item(item, image=row_image(row), values=row_values(row))
|
self.table.item(item, image=self._row_image(row), values=self._row_values(row))
|
||||||
|
|
||||||
def set_choice(item, index):
|
def _set_choice(self, item, index):
|
||||||
row = row_by_item[item]
|
row = self.row_by_item[item]
|
||||||
row["choice"] = index
|
row["choice"] = index
|
||||||
|
|
||||||
# Naming a build is the user saying they want the kit at it, so the row ticks
|
|
||||||
# itself — and for a declared row that also cancels a pending removal.
|
|
||||||
if row["declared"] or row["options"][index][1] is not None:
|
if row["declared"] or row["options"][index][1] is not None:
|
||||||
row["checked"] = True
|
row["checked"] = True
|
||||||
refresh_row(item)
|
self._refresh_row(item)
|
||||||
|
|
||||||
def toggle_check(item):
|
def _toggle_check(self, item):
|
||||||
row = row_by_item[item]
|
row = self.row_by_item[item]
|
||||||
if row["required"]:
|
if row["required"]:
|
||||||
# Needed by the app: the tick is fixed, and the box is drawn greyed to say so.
|
|
||||||
return
|
return
|
||||||
|
|
||||||
row["checked"] = not row["checked"]
|
row["checked"] = not row["checked"]
|
||||||
if not row["declared"]:
|
if not row["declared"]:
|
||||||
# Ticking a kit that is not in the schema means adding it, at its newest build
|
|
||||||
# unless the user picks another; unticking puts the row back to doing nothing.
|
|
||||||
row["choice"] = 1 if (row["checked"] and len(row["options"]) > 1) else 0
|
row["choice"] = 1 if (row["checked"] and len(row["options"]) > 1) else 0
|
||||||
if row["checked"] and len(row["options"]) < 2:
|
if row["checked"] and len(row["options"]) < 2:
|
||||||
row["checked"] = False
|
row["checked"] = False
|
||||||
@@ -332,76 +338,68 @@ def open_dialog(on_applied=None):
|
|||||||
"Manage Kits",
|
"Manage Kits",
|
||||||
f"Sedona home holds no manifest for '{row['kit']}', so there is no "
|
f"Sedona home holds no manifest for '{row['kit']}', so there is no "
|
||||||
"build to add it at.",
|
"build to add it at.",
|
||||||
parent=dialog
|
|
||||||
)
|
)
|
||||||
refresh_row(item)
|
self._refresh_row(item)
|
||||||
|
|
||||||
def edit_action(item):
|
def _edit_action(self, item):
|
||||||
row = row_by_item[item]
|
row = self.row_by_item[item]
|
||||||
cell = table.bbox(item, "action")
|
cell = self.table.bbox(item, "action")
|
||||||
if not cell:
|
if not cell:
|
||||||
return
|
return
|
||||||
|
|
||||||
labels = [label for label, _ in row["options"]]
|
labels = [label for label, _ in row["options"]]
|
||||||
current = action_text(row)
|
current = self._action_text(row)
|
||||||
variable = tk.StringVar(value=current if current in labels else labels[row["choice"]])
|
variable = tk.StringVar(value=current if current in labels else labels[row["choice"]])
|
||||||
combo = ttk.Combobox(table, textvariable=variable, values=labels, state="readonly")
|
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])
|
combo.place(x=cell[0], y=cell[1], width=cell[2], height=cell[3])
|
||||||
editor["widget"] = combo
|
self._editor_widget = combo
|
||||||
|
|
||||||
def commit(_event=None):
|
def commit(_event=None):
|
||||||
if variable.get() in labels:
|
if variable.get() in labels:
|
||||||
set_choice(item, labels.index(variable.get()))
|
self._set_choice(item, labels.index(variable.get()))
|
||||||
close_editor()
|
self._close_editor()
|
||||||
|
|
||||||
combo.bind("<<ComboboxSelected>>", commit)
|
combo.bind("<<ComboboxSelected>>", commit)
|
||||||
combo.bind("<Escape>", lambda event: close_editor())
|
combo.bind("<Escape>", lambda _e: self._close_editor())
|
||||||
combo.focus_set()
|
combo.focus_set()
|
||||||
# A single click on the cell should show the builds, not just a collapsed box.
|
|
||||||
combo.after_idle(lambda: combo.event_generate("<Down>"))
|
combo.after_idle(lambda: combo.event_generate("<Down>"))
|
||||||
|
|
||||||
def on_click(event):
|
def _on_click(self, event):
|
||||||
close_editor()
|
self._close_editor()
|
||||||
if table.identify_region(event.x, event.y) not in ("tree", "cell"):
|
if self.table.identify_region(event.x, event.y) not in ("tree", "cell"):
|
||||||
return
|
return
|
||||||
item = table.identify_row(event.y)
|
item = self.table.identify_row(event.y)
|
||||||
if item not in row_by_item:
|
if item not in self.row_by_item:
|
||||||
return
|
return
|
||||||
|
|
||||||
column = table.identify_column(event.x)
|
column = self.table.identify_column(event.x)
|
||||||
if column == "#0":
|
if column == "#0":
|
||||||
toggle_check(item)
|
self._toggle_check(item)
|
||||||
elif column == "#4":
|
elif column == "#4":
|
||||||
edit_action(item)
|
self._edit_action(item)
|
||||||
|
|
||||||
table.bind("<Button-1>", on_click)
|
def _apply_changes(self, selections, removals, label, nothing_message, note=""):
|
||||||
table.bind("<MouseWheel>", close_editor, add="+")
|
|
||||||
table.bind("<Configure>", close_editor, add="+")
|
|
||||||
|
|
||||||
def apply_changes(selections, removals, label, nothing_message, note=""):
|
|
||||||
if not selections and not removals:
|
if not selections and not removals:
|
||||||
messagebox.showinfo("Manage Kits", nothing_message, parent=dialog)
|
messagebox.showinfo("Manage Kits", nothing_message)
|
||||||
return
|
return
|
||||||
|
|
||||||
changes, error = sax_file.apply_kit_selections(selections, removals, label=label)
|
changes, error = sax_file.apply_kit_selections(selections, removals, label=label)
|
||||||
if error:
|
if error:
|
||||||
messagebox.showerror("Kits unchanged", error, parent=dialog)
|
messagebox.showerror("Kits unchanged", error)
|
||||||
rebuild_table()
|
self._rebuild_table()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not changes:
|
if not changes:
|
||||||
messagebox.showinfo(
|
messagebox.showinfo("Manage Kits", "The schema already reads that way.")
|
||||||
"Manage Kits", "The schema already reads that way.", parent=dialog
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
rebuild_table()
|
self._rebuild_table()
|
||||||
if on_applied is not None:
|
if self.on_applied is not None:
|
||||||
on_applied()
|
self.on_applied()
|
||||||
|
|
||||||
app_state.set_status(
|
app_state.set_status(
|
||||||
f"schema kits changed ({len(changes)}) — save the file to keep it.",
|
f"schema kits changed ({len(changes)}) — save the file to keep it.",
|
||||||
warn=True
|
warn=True,
|
||||||
)
|
)
|
||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"Kits updated",
|
"Kits updated",
|
||||||
@@ -409,13 +407,11 @@ def open_dialog(on_applied=None):
|
|||||||
+ (note + "\n\n" if note else "")
|
+ (note + "\n\n" if note else "")
|
||||||
+ "Save the file to write this to disk. The device's scode must hold these "
|
+ "Save the file to write this to disk. The device's scode must hold these "
|
||||||
"builds or the application will not run.",
|
"builds or the application will not run.",
|
||||||
parent=dialog
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def upgrade_all():
|
def _upgrade_all(self):
|
||||||
"""Every kit the schema declares, moved to the newest build Sedona home holds."""
|
|
||||||
selections = []
|
selections = []
|
||||||
for row in rows:
|
for row in self.rows:
|
||||||
if "section" in row or not row["declared"] or not row["latest"]:
|
if "section" in row or not row["declared"] or not row["latest"]:
|
||||||
continue
|
continue
|
||||||
latest_checksum = row["latest"][1]
|
latest_checksum = row["latest"][1]
|
||||||
@@ -432,22 +428,20 @@ def open_dialog(on_applied=None):
|
|||||||
f"{len(selections)} kit{'' if len(selections) == 1 else 's'} in this app's "
|
f"{len(selections)} kit{'' if len(selections) == 1 else 's'} in this app's "
|
||||||
f"schema will move to their latest installed build:\n\n{preview}\n\nContinue?",
|
f"schema will move to their latest installed build:\n\n{preview}\n\nContinue?",
|
||||||
icon=messagebox.WARNING,
|
icon=messagebox.WARNING,
|
||||||
parent=dialog
|
|
||||||
)
|
)
|
||||||
if not proceed:
|
if not proceed:
|
||||||
return
|
return
|
||||||
|
|
||||||
apply_changes(
|
self._apply_changes(
|
||||||
selections, [],
|
selections, [],
|
||||||
"upgrade all kits",
|
"upgrade all kits",
|
||||||
"Every kit in the schema is already at its latest installed build."
|
"Every kit in the schema is already at its latest installed build.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_marked():
|
def _update_marked(self):
|
||||||
"""The ticks and the Action column, applied: repins, additions and removals."""
|
|
||||||
selections = []
|
selections = []
|
||||||
removals = []
|
removals = []
|
||||||
for row in row_by_item.values():
|
for row in self.row_by_item.values():
|
||||||
if row["declared"] and not row["checked"]:
|
if row["declared"] and not row["checked"]:
|
||||||
removals.append(row["kit"])
|
removals.append(row["kit"])
|
||||||
continue
|
continue
|
||||||
@@ -464,17 +458,14 @@ def open_dialog(on_applied=None):
|
|||||||
"installed in Sedona home can be added back here."
|
"installed in Sedona home can be added back here."
|
||||||
)
|
)
|
||||||
|
|
||||||
apply_changes(
|
self._apply_changes(
|
||||||
selections, removals,
|
selections, removals,
|
||||||
"update kits",
|
"update kits",
|
||||||
"Nothing is marked for a change. Pick a build in a kit's Action column, tick "
|
"Nothing is marked for a change. Pick a build in a kit's Action column, tick "
|
||||||
"a kit to add it, or untick one to remove it."
|
"a kit to add it, or untick one to remove it.",
|
||||||
)
|
)
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
button_row, text="Upgrade all kits to latest version", command=upgrade_all
|
|
||||||
).pack(side=tk.LEFT)
|
|
||||||
ttk.Button(button_row, text="Update kits", command=update_marked).pack(side=tk.LEFT, padx=6)
|
|
||||||
ttk.Button(button_row, text="Close", command=dialog.destroy).pack(side=tk.RIGHT)
|
|
||||||
|
|
||||||
rebuild_table()
|
def build_kits_panel(parent, on_applied, on_close):
|
||||||
|
"""Factory matching the pattern of properties.build_properties_panel."""
|
||||||
|
return KitsPanel(parent, on_applied=on_applied, on_close=on_close)
|
||||||
|
|||||||
+18
-8
@@ -46,8 +46,17 @@ def version_sort_key(version_string):
|
|||||||
return numbers or [0]
|
return numbers or [0]
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_checksum(checksum):
|
||||||
|
"""Strips leading zeros so differently-padded hex checksums compare equal."""
|
||||||
|
return checksum.lstrip("0") if checksum else checksum
|
||||||
|
|
||||||
|
|
||||||
def kit_versions(kit_name):
|
def kit_versions(kit_name):
|
||||||
"""Maps checksum -> version string, read from the .kit filenames under kits/."""
|
"""Maps checksum -> version string, read from the .kit filenames under kits/.
|
||||||
|
|
||||||
|
Keys are stored with leading zeros stripped because .kit files and manifest
|
||||||
|
filenames sometimes disagree on padding (e.g. 462d43e vs 0462d43e).
|
||||||
|
"""
|
||||||
if kit_name in _versions_cache:
|
if kit_name in _versions_cache:
|
||||||
return _versions_cache[kit_name]
|
return _versions_cache[kit_name]
|
||||||
|
|
||||||
@@ -58,7 +67,7 @@ def kit_versions(kit_name):
|
|||||||
for entry in os.listdir(kit_dir):
|
for entry in os.listdir(kit_dir):
|
||||||
match = pattern.match(entry)
|
match = pattern.match(entry)
|
||||||
if match:
|
if match:
|
||||||
versions[match.group(1)] = match.group(2)
|
versions[_norm_checksum(match.group(1))] = match.group(2)
|
||||||
|
|
||||||
_versions_cache[kit_name] = versions
|
_versions_cache[kit_name] = versions
|
||||||
return versions
|
return versions
|
||||||
@@ -89,13 +98,14 @@ def resolve_manifest_path(kit_name):
|
|||||||
if pinned:
|
if pinned:
|
||||||
candidate = f"{kit_name}-{pinned}.xml"
|
candidate = f"{kit_name}-{pinned}.xml"
|
||||||
if candidate in manifests:
|
if candidate in manifests:
|
||||||
return os.path.join(kit_dir, candidate), versions.get(pinned, pinned)
|
return os.path.join(kit_dir, candidate), versions.get(_norm_checksum(pinned), pinned)
|
||||||
|
|
||||||
available = []
|
available = []
|
||||||
for entry in manifests:
|
for entry in manifests:
|
||||||
checksum = entry[len(kit_name) + 1:-4]
|
checksum = entry[len(kit_name) + 1:-4]
|
||||||
if checksum in versions:
|
norm = _norm_checksum(checksum)
|
||||||
available.append((version_sort_key(versions[checksum]), entry, versions[checksum]))
|
if norm in versions:
|
||||||
|
available.append((version_sort_key(versions[norm]), entry, versions[norm]))
|
||||||
|
|
||||||
if available:
|
if available:
|
||||||
available.sort()
|
available.sort()
|
||||||
@@ -104,7 +114,7 @@ def resolve_manifest_path(kit_name):
|
|||||||
|
|
||||||
entry = manifests[-1]
|
entry = manifests[-1]
|
||||||
checksum = entry[len(kit_name) + 1:-4]
|
checksum = entry[len(kit_name) + 1:-4]
|
||||||
return os.path.join(kit_dir, entry), versions.get(checksum, "")
|
return os.path.join(kit_dir, entry), versions.get(_norm_checksum(checksum), "")
|
||||||
|
|
||||||
|
|
||||||
def kit_builds(kit_name):
|
def kit_builds(kit_name):
|
||||||
@@ -125,7 +135,7 @@ def kit_builds(kit_name):
|
|||||||
if not (entry.startswith(f"{kit_name}-") and entry.endswith(".xml")):
|
if not (entry.startswith(f"{kit_name}-") and entry.endswith(".xml")):
|
||||||
continue
|
continue
|
||||||
checksum = entry[len(kit_name) + 1:-4]
|
checksum = entry[len(kit_name) + 1:-4]
|
||||||
builds.append((versions.get(checksum, ""), checksum))
|
builds.append((versions.get(_norm_checksum(checksum), ""), checksum))
|
||||||
|
|
||||||
builds.sort(
|
builds.sort(
|
||||||
key=lambda build: (bool(build[0]), version_sort_key(build[0])),
|
key=lambda build: (bool(build[0]), version_sort_key(build[0])),
|
||||||
@@ -184,7 +194,7 @@ def version_for_checksum(kit_name, checksum):
|
|||||||
"""
|
"""
|
||||||
if not checksum:
|
if not checksum:
|
||||||
return ""
|
return ""
|
||||||
return kit_versions(kit_name).get(checksum, "")
|
return kit_versions(kit_name).get(_norm_checksum(checksum), "")
|
||||||
|
|
||||||
|
|
||||||
def _find_type(qualified_name):
|
def _find_type(qualified_name):
|
||||||
|
|||||||
Reference in New Issue
Block a user