Compare commits
12
Commits
master
...
c9636b2fcd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9636b2fcd | ||
|
|
03bc5d5268 | ||
|
|
ef685d6657 | ||
|
|
8a83794dc5 | ||
|
|
5704ada93c | ||
|
|
63711ffede | ||
|
|
5f9049a321 | ||
|
|
e2e1fdd2a0 | ||
|
|
a196211708 | ||
|
|
36fbb6a662 | ||
|
|
4ef1ce0c6b | ||
|
|
906f787748 |
+5
-56
@@ -218,21 +218,16 @@ class MainWindow(ctk.CTk):
|
|||||||
object's perspective); the FileSelector stores both the
|
object's perspective); the FileSelector stores both the
|
||||||
absolute path (for internal use) and the relative path
|
absolute path (for internal use) and the relative path
|
||||||
(for saving to YAML).
|
(for saving to YAML).
|
||||||
|
|
||||||
All attribute accesses are defensive — widgets may not exist
|
|
||||||
yet during construction (e.g. _port_var created in _build_serial).
|
|
||||||
"""
|
"""
|
||||||
if not self._config:
|
if not self._config:
|
||||||
return
|
return
|
||||||
# Sync IP address (may not exist yet during _build_config_panel)
|
# Sync IP address
|
||||||
if self._ip_string_var:
|
if self._ip_string_var:
|
||||||
self._config.zynq_ip = self._ip_string_var.get()
|
self._config.zynq_ip = self._ip_string_var.get()
|
||||||
# Sync Xilinx Kit path
|
# Sync Xilinx Kit path
|
||||||
if hasattr(self, '_xilinx_path_var'):
|
self._config.xilinx_path = self._xilinx_path_var.get()
|
||||||
self._config.xilinx_path = self._xilinx_path_var.get()
|
# Sync serial port
|
||||||
# Sync serial port (may not exist yet during _build_config_panel)
|
self._config.serial_port = self._port_var.get()
|
||||||
if hasattr(self, '_port_var'):
|
|
||||||
self._config.serial_port = self._port_var.get()
|
|
||||||
# Sync file paths from FileSelectors
|
# Sync file paths from FileSelectors
|
||||||
files = self._get_selected_files()
|
files = self._get_selected_files()
|
||||||
for attr, selector in [
|
for attr, selector in [
|
||||||
@@ -250,26 +245,7 @@ class MainWindow(ctk.CTk):
|
|||||||
if abs_path:
|
if abs_path:
|
||||||
self._config._config_path # ensure _config_path is set
|
self._config._config_path # ensure _config_path is set
|
||||||
setattr(self._config, attr, self._config._relative_path(abs_path))
|
setattr(self._config, attr, self._config._relative_path(abs_path))
|
||||||
# Sync erase checkbox (may not exist yet)
|
self._config.erase_all = self._erase_cb_var.get()
|
||||||
if hasattr(self, '_erase_cb_var'):
|
|
||||||
self._config.erase_all = self._erase_cb_var.get()
|
|
||||||
|
|
||||||
def _auto_save_config(self) -> None:
|
|
||||||
"""Auto-save config to disk after UI changes.
|
|
||||||
|
|
||||||
Ensures that changes made in the UI (file paths, IP, etc.)
|
|
||||||
are persisted immediately so that _reload_config() picks them
|
|
||||||
up on the next step execution.
|
|
||||||
"""
|
|
||||||
if not self._config:
|
|
||||||
return
|
|
||||||
self._sync_config_from_ui()
|
|
||||||
user_config_path = self._get_user_config_path()
|
|
||||||
self._config._config_path = user_config_path
|
|
||||||
try:
|
|
||||||
self._config.save()
|
|
||||||
except Exception:
|
|
||||||
pass # Silently fail — user can manually save
|
|
||||||
|
|
||||||
def _save_config(self) -> None:
|
def _save_config(self) -> None:
|
||||||
"""Save current configuration to user config file (config.yaml).
|
"""Save current configuration to user config file (config.yaml).
|
||||||
@@ -317,15 +293,6 @@ class MainWindow(ctk.CTk):
|
|||||||
if self._config:
|
if self._config:
|
||||||
self._config.zynq_ip = self._ip_string_var.get()
|
self._config.zynq_ip = self._ip_string_var.get()
|
||||||
|
|
||||||
def _on_file_selected(self, path: str) -> None:
|
|
||||||
"""Handle file selection — sync and auto-save to disk.
|
|
||||||
|
|
||||||
When the user selects a file via the browse dialog, immediately
|
|
||||||
sync the path to the Config object and persist it to YAML so
|
|
||||||
that subsequent step executions pick up the new value.
|
|
||||||
"""
|
|
||||||
self._auto_save_config()
|
|
||||||
|
|
||||||
# ── UI Construction ────────────────────────────────────────
|
# ── UI Construction ────────────────────────────────────────
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
def _build_ui(self) -> None:
|
||||||
@@ -591,9 +558,7 @@ class MainWindow(ctk.CTk):
|
|||||||
panel,
|
panel,
|
||||||
label_text="FSBL ELF (.elf):",
|
label_text="FSBL ELF (.elf):",
|
||||||
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
||||||
callback=self._on_file_selected,
|
|
||||||
)
|
)
|
||||||
self._fsbl_selector._suppress_callback = True
|
|
||||||
if self._config.fsbl_elf_path:
|
if self._config.fsbl_elf_path:
|
||||||
resolved = self._config.resolve_path(self._config.fsbl_elf_path)
|
resolved = self._config.resolve_path(self._config.fsbl_elf_path)
|
||||||
self._fsbl_selector.set_relative_path(self._config.fsbl_elf_path)
|
self._fsbl_selector.set_relative_path(self._config.fsbl_elf_path)
|
||||||
@@ -605,9 +570,7 @@ class MainWindow(ctk.CTk):
|
|||||||
panel,
|
panel,
|
||||||
label_text="Bootloader BIT (.bit):",
|
label_text="Bootloader BIT (.bit):",
|
||||||
file_types=[("BIT files", "*.bit"), ("All files", "*")],
|
file_types=[("BIT files", "*.bit"), ("All files", "*")],
|
||||||
callback=self._on_file_selected,
|
|
||||||
)
|
)
|
||||||
self._bit_selector._suppress_callback = True
|
|
||||||
if self._config.bootloader_bit_path:
|
if self._config.bootloader_bit_path:
|
||||||
resolved = self._config.resolve_path(self._config.bootloader_bit_path)
|
resolved = self._config.resolve_path(self._config.bootloader_bit_path)
|
||||||
self._bit_selector.set_relative_path(self._config.bootloader_bit_path)
|
self._bit_selector.set_relative_path(self._config.bootloader_bit_path)
|
||||||
@@ -619,9 +582,7 @@ class MainWindow(ctk.CTk):
|
|||||||
panel,
|
panel,
|
||||||
label_text="Bootloader ELF (.elf):",
|
label_text="Bootloader ELF (.elf):",
|
||||||
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
||||||
callback=self._on_file_selected,
|
|
||||||
)
|
)
|
||||||
self._elf_selector._suppress_callback = True
|
|
||||||
if self._config.bootloader_elf_path:
|
if self._config.bootloader_elf_path:
|
||||||
resolved = self._config.resolve_path(self._config.bootloader_elf_path)
|
resolved = self._config.resolve_path(self._config.bootloader_elf_path)
|
||||||
self._elf_selector.set_relative_path(self._config.bootloader_elf_path)
|
self._elf_selector.set_relative_path(self._config.bootloader_elf_path)
|
||||||
@@ -633,9 +594,7 @@ class MainWindow(ctk.CTk):
|
|||||||
panel,
|
panel,
|
||||||
label_text="Bootloader BIN (.bin):",
|
label_text="Bootloader BIN (.bin):",
|
||||||
file_types=[("BIN files", "*.bin"), ("All files", "*")],
|
file_types=[("BIN files", "*.bin"), ("All files", "*")],
|
||||||
callback=self._on_file_selected,
|
|
||||||
)
|
)
|
||||||
self._bootloader_bin_selector._suppress_callback = True
|
|
||||||
if self._config.bootloader_bin_path:
|
if self._config.bootloader_bin_path:
|
||||||
resolved = self._config.resolve_path(self._config.bootloader_bin_path)
|
resolved = self._config.resolve_path(self._config.bootloader_bin_path)
|
||||||
self._bootloader_bin_selector.set_relative_path(self._config.bootloader_bin_path)
|
self._bootloader_bin_selector.set_relative_path(self._config.bootloader_bin_path)
|
||||||
@@ -669,23 +628,13 @@ class MainWindow(ctk.CTk):
|
|||||||
panel,
|
panel,
|
||||||
label_text="Firmware BIN (.bin):",
|
label_text="Firmware BIN (.bin):",
|
||||||
file_types=[("BIN files", "*.bin"), ("All files", "*")],
|
file_types=[("BIN files", "*.bin"), ("All files", "*")],
|
||||||
callback=self._on_file_selected,
|
|
||||||
)
|
)
|
||||||
self._firmware_bin_selector._suppress_callback = True
|
|
||||||
if self._config.firmware_bin_path:
|
if self._config.firmware_bin_path:
|
||||||
resolved = self._config.resolve_path(self._config.firmware_bin_path)
|
resolved = self._config.resolve_path(self._config.firmware_bin_path)
|
||||||
self._firmware_bin_selector.set_relative_path(self._config.firmware_bin_path)
|
self._firmware_bin_selector.set_relative_path(self._config.firmware_bin_path)
|
||||||
self._firmware_bin_selector.set_path(str(resolved))
|
self._firmware_bin_selector.set_path(str(resolved))
|
||||||
self._firmware_bin_selector.grid(row=8, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._firmware_bin_selector.grid(row=8, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
# Enable callbacks now that all selectors are created
|
|
||||||
for sel in (self._fsbl_selector, self._bit_selector, self._elf_selector,
|
|
||||||
self._bootloader_bin_selector, self._firmware_bin_selector):
|
|
||||||
sel._suppress_callback = False
|
|
||||||
|
|
||||||
# Now sync and save config (all selectors exist)
|
|
||||||
self._auto_save_config()
|
|
||||||
|
|
||||||
# Save button
|
# Save button
|
||||||
save_btn = ctk.CTkButton(
|
save_btn = ctk.CTkButton(
|
||||||
panel,
|
panel,
|
||||||
|
|||||||
+21
-70
@@ -6,7 +6,6 @@ consistent styling from gui/styles.py.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
@@ -439,9 +438,7 @@ class ProgressIndicator(ctk.CTkFrame):
|
|||||||
class StatusDisplay(ctk.CTkFrame):
|
class StatusDisplay(ctk.CTkFrame):
|
||||||
"""Status display widget for operation results.
|
"""Status display widget for operation results.
|
||||||
|
|
||||||
Shows status messages in a multi-line, expandable text area that
|
Shows a status message with color-coded feedback.
|
||||||
uses the full available space. Messages are color-coded and
|
|
||||||
automatically wrapped.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, master, **kwargs):
|
def __init__(self, master, **kwargs):
|
||||||
@@ -455,19 +452,15 @@ class StatusDisplay(ctk.CTkFrame):
|
|||||||
self._create_widgets()
|
self._create_widgets()
|
||||||
|
|
||||||
def _create_widgets(self) -> None:
|
def _create_widgets(self) -> None:
|
||||||
"""Create the status display UI — multi-line expandable area."""
|
"""Create the status display UI."""
|
||||||
self.grid_rowconfigure(0, weight=1)
|
self._status_label = ctk.CTkLabel(
|
||||||
self.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
self._status_text = ctk.CTkTextbox(
|
|
||||||
self,
|
self,
|
||||||
|
text="Ready",
|
||||||
font=FONT_BODY,
|
font=FONT_BODY,
|
||||||
corner_radius=CORNER_RADIUS,
|
anchor="w",
|
||||||
state="disabled",
|
|
||||||
wrap="word",
|
|
||||||
)
|
)
|
||||||
self._status_text.grid(
|
self._status_label.pack(
|
||||||
row=0, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL
|
fill="x", padx=PADDING, pady=PADDING_SMALL
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_status(self, message: str, status: str = "info") -> None:
|
def set_status(self, message: str, status: str = "info") -> None:
|
||||||
@@ -477,32 +470,20 @@ class StatusDisplay(ctk.CTkFrame):
|
|||||||
message: Status message text.
|
message: Status message text.
|
||||||
status: Status type for coloring ('info', 'success', 'error', 'warning').
|
status: Status type for coloring ('info', 'success', 'error', 'warning').
|
||||||
"""
|
"""
|
||||||
self._status_text.configure(state="normal")
|
self._status_label.configure(text=message)
|
||||||
self._status_text.delete("1.0", "end")
|
|
||||||
|
|
||||||
colors = {
|
colors = {
|
||||||
"info": INFO_COLOR,
|
"info": INFO_COLOR,
|
||||||
"success": SUCCESS_COLOR,
|
"success": SUCCESS_COLOR,
|
||||||
"error": DANGER_COLOR,
|
"error": DANGER_COLOR,
|
||||||
"warning": WARNING_COLOR,
|
"warning": WARNING_COLOR,
|
||||||
}
|
}
|
||||||
color = colors.get(status, INFO_COLOR)
|
self._status_label.configure(text_color=colors.get(status, INFO_COLOR))
|
||||||
|
|
||||||
self._status_text.insert("1.0", message)
|
|
||||||
# Tag the entire text with the color (CTkTextbox uses tag_config)
|
|
||||||
self._status_text.tag_config("status", foreground=color)
|
|
||||||
self._status_text.tag_add("status", "1.0", "end")
|
|
||||||
|
|
||||||
self._status_text.configure(state="disabled")
|
|
||||||
|
|
||||||
|
|
||||||
class FileSelector(ctk.CTkFrame):
|
class FileSelector(ctk.CTkFrame):
|
||||||
"""File selector widget with browse button and path display.
|
"""File selector widget with browse button and path display.
|
||||||
|
|
||||||
Stores the full absolute path internally but displays only the
|
Allows users to browse for files and displays the selected path.
|
||||||
filename in the entry field. Supports relative paths: the
|
|
||||||
``set_relative_path()`` / ``get_relative_path()`` methods work
|
|
||||||
with paths relative to a given base directory.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -525,10 +506,7 @@ class FileSelector(ctk.CTkFrame):
|
|||||||
|
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
self._file_types = file_types or [("All files", "*")]
|
self._file_types = file_types or [("All files", "*")]
|
||||||
self._full_path: str = "" # absolute path (internal)
|
self._selected_path = ctk.StringVar(value="")
|
||||||
self._relative_path: str = "" # relative path (synced to Config)
|
|
||||||
self._display_text = ctk.StringVar(value="")
|
|
||||||
self._suppress_callback: bool = False # True during construction
|
|
||||||
|
|
||||||
self._create_widgets(label_text)
|
self._create_widgets(label_text)
|
||||||
|
|
||||||
@@ -546,10 +524,10 @@ class FileSelector(ctk.CTkFrame):
|
|||||||
)
|
)
|
||||||
label.grid(row=0, column=0, sticky="w", padx=(PADDING, PADDING_SMALL))
|
label.grid(row=0, column=0, sticky="w", padx=(PADDING, PADDING_SMALL))
|
||||||
|
|
||||||
# Path entry — shows only the filename
|
# Path entry
|
||||||
self._entry = ctk.CTkEntry(
|
self._entry = ctk.CTkEntry(
|
||||||
self,
|
self,
|
||||||
textvariable=self._display_text,
|
textvariable=self._selected_path,
|
||||||
font=FONT_MONO,
|
font=FONT_MONO,
|
||||||
state="readonly",
|
state="readonly",
|
||||||
)
|
)
|
||||||
@@ -571,60 +549,33 @@ class FileSelector(ctk.CTkFrame):
|
|||||||
|
|
||||||
def _browse(self) -> None:
|
def _browse(self) -> None:
|
||||||
"""Open file dialog and set selected path."""
|
"""Open file dialog and set selected path."""
|
||||||
|
import customtkinter
|
||||||
from tkinter import filedialog
|
from tkinter import filedialog
|
||||||
|
|
||||||
file_path = filedialog.askopenfilename(
|
file_path = filedialog.askopenfilename(
|
||||||
title="Select file",
|
title=f"Select {self._selected_path.get() or 'file'}",
|
||||||
filetypes=self._file_types,
|
filetypes=self._file_types,
|
||||||
)
|
)
|
||||||
if file_path:
|
if file_path:
|
||||||
self.set_path(file_path)
|
self.set_path(file_path)
|
||||||
|
|
||||||
def set_path(self, path: str) -> None:
|
def set_path(self, path: str) -> None:
|
||||||
"""Set the selected file path (absolute or relative).
|
"""Set the selected file path.
|
||||||
|
|
||||||
Internally stores the absolute path and displays only the
|
|
||||||
filename.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: Absolute or relative path to the selected file.
|
path: Absolute path to the selected file.
|
||||||
"""
|
"""
|
||||||
self._full_path = path
|
self._selected_path.set(path)
|
||||||
self._display_text.set(os.path.basename(path) if path else "")
|
if self._callback:
|
||||||
if self._callback and not self._suppress_callback:
|
|
||||||
self._callback(path)
|
|
||||||
|
|
||||||
def set_relative_path(self, path: str) -> None:
|
|
||||||
"""Set a relative path (from config).
|
|
||||||
|
|
||||||
Resolves to absolute for internal storage, shows only filename.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: Relative path string.
|
|
||||||
"""
|
|
||||||
self._relative_path = path
|
|
||||||
# Resolve to absolute for internal use
|
|
||||||
if path:
|
|
||||||
self._full_path = path # caller should resolve before calling
|
|
||||||
self._display_text.set(os.path.basename(path) if path else "")
|
|
||||||
if self._callback and not self._suppress_callback:
|
|
||||||
self._callback(path)
|
self._callback(path)
|
||||||
|
|
||||||
def get_path(self) -> str:
|
def get_path(self) -> str:
|
||||||
"""Get the currently selected file path (absolute).
|
"""Get the currently selected file path.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Selected file path string.
|
Selected file path string.
|
||||||
"""
|
"""
|
||||||
return self._full_path
|
return self._selected_path.get()
|
||||||
|
|
||||||
def get_relative_path(self) -> str:
|
|
||||||
"""Get the currently selected relative path.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Relative path string, or empty string.
|
|
||||||
"""
|
|
||||||
return self._relative_path
|
|
||||||
|
|
||||||
|
|
||||||
class LogDisplay(ctk.CTkFrame):
|
class LogDisplay(ctk.CTkFrame):
|
||||||
|
|||||||
Reference in New Issue
Block a user