add save array per json
This commit is contained in:
parent
1527526188
commit
3f2ba715c1
@ -80,7 +80,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"column_name": "exp_pulse1_delay",
|
"column_name": "exp_pulse1_delay",
|
||||||
"data_path": "timer_data.blob.payload.exp_pulse1_delay",
|
"data_path": "timer_data.blob.payload.exp_pulse1_delay[0]",
|
||||||
"translate_with_enum": false
|
"translate_with_enum": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import ctypes
|
|||||||
from ..utils.config_manager import ConfigManager
|
from ..utils.config_manager import ConfigManager
|
||||||
from ..core.file_reader import run_worker_process
|
from ..core.file_reader import run_worker_process
|
||||||
from ..core.cpp_runner import run_cpp_converter
|
from ..core.cpp_runner import run_cpp_converter
|
||||||
from ..core.data_structures import DataBatch
|
from ..core.data_structures import DataBatch, CtypesStructureBase
|
||||||
from ..core.data_enums import ENUM_REGISTRY, get_enum_name
|
from ..core.data_enums import ENUM_REGISTRY, get_enum_name
|
||||||
from ..utils import logger
|
from ..utils import logger
|
||||||
from ..gui.profile_editor_window import ProfileEditorWindow
|
from ..gui.profile_editor_window import ProfileEditorWindow
|
||||||
@ -40,7 +40,6 @@ def _get_value_from_path(batch: DataBatch, field: ExportField) -> Any:
|
|||||||
if path == "batch_id":
|
if path == "batch_id":
|
||||||
return batch.batch_id
|
return batch.batch_id
|
||||||
|
|
||||||
# Split the path by dots and brackets to handle attributes and indices
|
|
||||||
parts = re.split(r"\.|\[", path)
|
parts = re.split(r"\.|\[", path)
|
||||||
|
|
||||||
current_obj = batch
|
current_obj = batch
|
||||||
@ -49,7 +48,6 @@ def _get_value_from_path(batch: DataBatch, field: ExportField) -> Any:
|
|||||||
return "N/A"
|
return "N/A"
|
||||||
|
|
||||||
if part.endswith("]"):
|
if part.endswith("]"):
|
||||||
# This is an index access
|
|
||||||
index_str = part[:-1]
|
index_str = part[:-1]
|
||||||
if not index_str.isdigit():
|
if not index_str.isdigit():
|
||||||
log.warning(f"Invalid index '{index_str}' in path: {path}")
|
log.warning(f"Invalid index '{index_str}' in path: {path}")
|
||||||
@ -62,14 +60,11 @@ def _get_value_from_path(batch: DataBatch, field: ExportField) -> Any:
|
|||||||
)
|
)
|
||||||
return "N/A"
|
return "N/A"
|
||||||
else:
|
else:
|
||||||
# This is an attribute access
|
|
||||||
current_obj = getattr(current_obj, part, None)
|
current_obj = getattr(current_obj, part, None)
|
||||||
|
|
||||||
value = current_obj if current_obj is not None else "N/A"
|
value = current_obj if current_obj is not None else "N/A"
|
||||||
|
|
||||||
# Handle translation for enums if the final value is an integer
|
|
||||||
if field.translate_with_enum and isinstance(value, int):
|
if field.translate_with_enum and isinstance(value, int):
|
||||||
# For enum translation, we need the path without indices
|
|
||||||
enum_path = re.sub(r"\[\d+\]", "", path)
|
enum_path = re.sub(r"\[\d+\]", "", path)
|
||||||
enum_class = ENUM_REGISTRY.get(enum_path)
|
enum_class = ENUM_REGISTRY.get(enum_path)
|
||||||
if enum_class:
|
if enum_class:
|
||||||
@ -81,6 +76,36 @@ def _get_value_from_path(batch: DataBatch, field: ExportField) -> Any:
|
|||||||
return "N/A"
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_ctypes_for_json(obj: Any) -> Any:
|
||||||
|
"""
|
||||||
|
Recursively converts ctypes objects into JSON-serializable Python types (lists, dicts, primitives).
|
||||||
|
"""
|
||||||
|
if isinstance(obj, (int, float, str, bool)) or obj is None:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
# Handle basic ctypes values (c_int, c_float, etc.)
|
||||||
|
if isinstance(obj, (ctypes._SimpleCData)):
|
||||||
|
return obj.value
|
||||||
|
|
||||||
|
# Handle ctypes Structures
|
||||||
|
if isinstance(obj, CtypesStructureBase):
|
||||||
|
result = {}
|
||||||
|
for field_name, _ in obj._fields_:
|
||||||
|
# Skip fields that are not meant for direct data representation
|
||||||
|
if field_name.startswith("_"):
|
||||||
|
continue
|
||||||
|
value = getattr(obj, field_name)
|
||||||
|
result[field_name] = _convert_ctypes_for_json(value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Handle ctypes Arrays
|
||||||
|
if isinstance(obj, ctypes.Array):
|
||||||
|
return [_convert_ctypes_for_json(item) for item in obj]
|
||||||
|
|
||||||
|
# Return the object itself if it's not a ctypes type we handle
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class AppController:
|
class AppController:
|
||||||
"""The main controller of the application."""
|
"""The main controller of the application."""
|
||||||
|
|
||||||
@ -96,7 +121,6 @@ class AppController:
|
|||||||
self.output_file_handles: Dict[str, Any] = {}
|
self.output_file_handles: Dict[str, Any] = {}
|
||||||
self.csv_writers: Dict[str, Any] = {}
|
self.csv_writers: Dict[str, Any] = {}
|
||||||
|
|
||||||
# Buffer for JSON data
|
|
||||||
self.json_data_buffer: List[Dict[str, Any]] = []
|
self.json_data_buffer: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
self.last_generated_out_file: Optional[Path] = None
|
self.last_generated_out_file: Optional[Path] = None
|
||||||
@ -110,18 +134,14 @@ class AppController:
|
|||||||
if Path(last_file).is_file():
|
if Path(last_file).is_file():
|
||||||
self.view.out_filepath_var.set(last_file)
|
self.view.out_filepath_var.set(last_file)
|
||||||
self.on_out_config_changed()
|
self.on_out_config_changed()
|
||||||
|
|
||||||
if last_dir := self.config_manager.get("last_out_output_dir"):
|
if last_dir := self.config_manager.get("last_out_output_dir"):
|
||||||
self.view.out_output_dir_var.set(last_dir)
|
self.view.out_output_dir_var.set(last_dir)
|
||||||
|
|
||||||
if last_file := self.config_manager.get("last_opened_rec_file"):
|
if last_file := self.config_manager.get("last_opened_rec_file"):
|
||||||
if Path(last_file).is_file():
|
if Path(last_file).is_file():
|
||||||
self.view.rec_filepath_var.set(last_file)
|
self.view.rec_filepath_var.set(last_file)
|
||||||
self.on_rec_config_changed()
|
self.on_rec_config_changed()
|
||||||
|
|
||||||
if last_dir := self.config_manager.get("last_rec_output_dir"):
|
if last_dir := self.config_manager.get("last_rec_output_dir"):
|
||||||
self.view.rec_output_dir_var.set(last_dir)
|
self.view.rec_output_dir_var.set(last_dir)
|
||||||
|
|
||||||
profiles = self.config_manager.get_export_profiles()
|
profiles = self.config_manager.get_export_profiles()
|
||||||
self.view.update_export_profiles(
|
self.view.update_export_profiles(
|
||||||
profiles=profiles,
|
profiles=profiles,
|
||||||
@ -188,13 +208,12 @@ class AppController:
|
|||||||
self.output_file_handles.clear()
|
self.output_file_handles.clear()
|
||||||
self.csv_writers.clear()
|
self.csv_writers.clear()
|
||||||
self.active_export_profiles.clear()
|
self.active_export_profiles.clear()
|
||||||
self.json_data_buffer.clear() # Clear JSON buffer
|
self.json_data_buffer.clear()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
output_dir = Path(self.view.out_output_dir_var.get())
|
output_dir = Path(self.view.out_output_dir_var.get())
|
||||||
basename = self.view.out_basename_var.get()
|
basename = self.view.out_basename_var.get()
|
||||||
profiles = self.config_manager.get_export_profiles()
|
profiles = self.config_manager.get_export_profiles()
|
||||||
|
use_full_path = self.view.out_use_full_path_var.get()
|
||||||
if self.view.out_output_csv_var.get():
|
if self.view.out_output_csv_var.get():
|
||||||
profile = next(
|
profile = next(
|
||||||
(
|
(
|
||||||
@ -210,22 +229,18 @@ class AppController:
|
|||||||
)
|
)
|
||||||
self.active_export_profiles["csv"] = profile
|
self.active_export_profiles["csv"] = profile
|
||||||
path = (output_dir / basename).with_suffix(".csv")
|
path = (output_dir / basename).with_suffix(".csv")
|
||||||
|
|
||||||
# Determine the delimiter based on the GUI checkbox
|
|
||||||
use_tab_delimiter = self.view.out_csv_use_tab_var.get()
|
use_tab_delimiter = self.view.out_csv_use_tab_var.get()
|
||||||
delimiter = "\t" if use_tab_delimiter else ","
|
delimiter = "\t" if use_tab_delimiter else ","
|
||||||
log.info(f"Preparing CSV file with '{delimiter}' as delimiter.")
|
log.info(f"Preparing CSV file with '{delimiter}' as delimiter.")
|
||||||
|
|
||||||
fh = open(path, "w", encoding="utf-8", newline="")
|
fh = open(path, "w", encoding="utf-8", newline="")
|
||||||
self.output_file_handles["csv"] = fh
|
self.output_file_handles["csv"] = fh
|
||||||
|
|
||||||
# Create the CSV writer with the chosen delimiter
|
|
||||||
csv_writer = csv.writer(fh, delimiter=delimiter)
|
csv_writer = csv.writer(fh, delimiter=delimiter)
|
||||||
self.csv_writers["csv"] = csv_writer
|
self.csv_writers["csv"] = csv_writer
|
||||||
self.csv_writers["csv"].writerow(
|
headers = [
|
||||||
[f.column_name for f in profile.fields]
|
field.data_path if use_full_path else field.column_name
|
||||||
)
|
for field in profile.fields
|
||||||
|
]
|
||||||
|
self.csv_writers["csv"].writerow(headers)
|
||||||
if self.view.out_output_json_var.get():
|
if self.view.out_output_json_var.get():
|
||||||
profile = next(
|
profile = next(
|
||||||
(
|
(
|
||||||
@ -240,8 +255,6 @@ class AppController:
|
|||||||
f"JSON profile '{self.view.out_json_profile_var.get()}' not found."
|
f"JSON profile '{self.view.out_json_profile_var.get()}' not found."
|
||||||
)
|
)
|
||||||
self.active_export_profiles["json"] = profile
|
self.active_export_profiles["json"] = profile
|
||||||
# JSON file is no longer opened here, it's written at the end.
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except (IOError, ValueError) as e:
|
except (IOError, ValueError) as e:
|
||||||
log.error(f"Failed to prepare output files: {e}")
|
log.error(f"Failed to prepare output files: {e}")
|
||||||
@ -252,11 +265,9 @@ class AppController:
|
|||||||
if self.is_processing:
|
if self.is_processing:
|
||||||
log.warning("Processing already in progress.")
|
log.warning("Processing already in progress.")
|
||||||
return
|
return
|
||||||
|
|
||||||
filepath_str = self.view.out_filepath_var.get()
|
|
||||||
if not all(
|
if not all(
|
||||||
[
|
[
|
||||||
filepath_str,
|
self.view.out_filepath_var.get(),
|
||||||
self.view.out_output_dir_var.get(),
|
self.view.out_output_dir_var.get(),
|
||||||
self.view.out_basename_var.get(),
|
self.view.out_basename_var.get(),
|
||||||
]
|
]
|
||||||
@ -270,10 +281,9 @@ class AppController:
|
|||||||
return
|
return
|
||||||
if not self._prepare_out_processor_files():
|
if not self._prepare_out_processor_files():
|
||||||
return
|
return
|
||||||
|
|
||||||
self.is_processing = True
|
self.is_processing = True
|
||||||
self.view.start_processing_ui()
|
self.view.start_processing_ui()
|
||||||
|
filepath_str = self.view.out_filepath_var.get()
|
||||||
self.config_manager.set("last_opened_out_file", filepath_str)
|
self.config_manager.set("last_opened_out_file", filepath_str)
|
||||||
self.config_manager.set(
|
self.config_manager.set(
|
||||||
"last_out_output_dir", self.view.out_output_dir_var.get()
|
"last_out_output_dir", self.view.out_output_dir_var.get()
|
||||||
@ -282,7 +292,6 @@ class AppController:
|
|||||||
"active_out_export_profile_name", self.view.out_csv_profile_var.get()
|
"active_out_export_profile_name", self.view.out_csv_profile_var.get()
|
||||||
)
|
)
|
||||||
self.config_manager.save_config()
|
self.config_manager.save_config()
|
||||||
|
|
||||||
active_profile = self.active_export_profiles.get(
|
active_profile = self.active_export_profiles.get(
|
||||||
"csv"
|
"csv"
|
||||||
) or self.active_export_profiles.get("json")
|
) or self.active_export_profiles.get("json")
|
||||||
@ -301,7 +310,6 @@ class AppController:
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
"g_reconvert.exe path is not set or is invalid. Please set it in the Advanced Config."
|
"g_reconvert.exe path is not set or is invalid. Please set it in the Advanced Config."
|
||||||
)
|
)
|
||||||
|
|
||||||
rec_file = self.view.rec_filepath_var.get()
|
rec_file = self.view.rec_filepath_var.get()
|
||||||
output_dir = self.view.rec_output_dir_var.get()
|
output_dir = self.view.rec_output_dir_var.get()
|
||||||
out_basename = self.view.rec_basename_var.get()
|
out_basename = self.view.rec_basename_var.get()
|
||||||
@ -309,17 +317,14 @@ class AppController:
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Missing required paths for C++ converter (REC file or Output)."
|
"Missing required paths for C++ converter (REC file or Output)."
|
||||||
)
|
)
|
||||||
|
|
||||||
output_file_path = Path(output_dir) / f"{out_basename}.out"
|
output_file_path = Path(output_dir) / f"{out_basename}.out"
|
||||||
self.last_generated_out_file = output_file_path
|
self.last_generated_out_file = output_file_path
|
||||||
|
|
||||||
command = [
|
command = [
|
||||||
exe_path,
|
exe_path,
|
||||||
rec_file,
|
rec_file,
|
||||||
f"/o={str(output_file_path)}",
|
f"/o={str(output_file_path)}",
|
||||||
f"/n={self.view.rec_file_count_var.get()}",
|
f"/n={self.view.rec_file_count_var.get()}",
|
||||||
]
|
]
|
||||||
|
|
||||||
if config.get("post_process"):
|
if config.get("post_process"):
|
||||||
command.append(f"/p={config.get('post_process_level', '1')}")
|
command.append(f"/p={config.get('post_process_level', '1')}")
|
||||||
if config.get("video_show"):
|
if config.get("video_show"):
|
||||||
@ -330,7 +335,6 @@ class AppController:
|
|||||||
command.append("/gps")
|
command.append("/gps")
|
||||||
if config.get("silent_overwrite"):
|
if config.get("silent_overwrite"):
|
||||||
command.append("//o")
|
command.append("//o")
|
||||||
|
|
||||||
log.info(f"Assembled C++ command: {' '.join(command)}")
|
log.info(f"Assembled C++ command: {' '.join(command)}")
|
||||||
return command
|
return command
|
||||||
|
|
||||||
@ -345,7 +349,6 @@ class AppController:
|
|||||||
log.error(f"Configuration error: {e}")
|
log.error(f"Configuration error: {e}")
|
||||||
messagebox.showerror("Configuration Error", str(e), parent=self.view)
|
messagebox.showerror("Configuration Error", str(e), parent=self.view)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.is_processing = True
|
self.is_processing = True
|
||||||
self.view.start_processing_ui()
|
self.view.start_processing_ui()
|
||||||
worker_args = (command_list, self.result_queue, output_dir)
|
worker_args = (command_list, self.result_queue, output_dir)
|
||||||
@ -378,11 +381,8 @@ class AppController:
|
|||||||
self.csv_writers.clear()
|
self.csv_writers.clear()
|
||||||
|
|
||||||
def handle_data_batch(self, batch: DataBatch):
|
def handle_data_batch(self, batch: DataBatch):
|
||||||
"""Writes a data batch to CSV and buffers it for JSON."""
|
"""Writes a data batch to CSV and buffers it for JSON, converting ctypes."""
|
||||||
# Dato che il worker ora potrebbe restituire una struttura non completamente serializzabile
|
use_full_path = self.view.out_use_full_path_var.get()
|
||||||
# tra processi (ctypes), è meglio gestire il batch come un dizionario o
|
|
||||||
# assicurarsi che la comunicazione avvenga in modo sicuro.
|
|
||||||
# Per ora, la logica principale non cambia, ma teniamo a mente questa potenziale complessità.
|
|
||||||
|
|
||||||
if self.csv_writers.get("csv"):
|
if self.csv_writers.get("csv"):
|
||||||
profile = self.active_export_profiles["csv"]
|
profile = self.active_export_profiles["csv"]
|
||||||
@ -395,13 +395,11 @@ class AppController:
|
|||||||
profile = self.active_export_profiles["json"]
|
profile = self.active_export_profiles["json"]
|
||||||
row_dict = {}
|
row_dict = {}
|
||||||
for field in profile.fields:
|
for field in profile.fields:
|
||||||
value = _get_value_from_path(batch, field)
|
raw_value = _get_value_from_path(batch, field)
|
||||||
# I tipi ctypes non sono serializzabili in JSON, li convertiamo.
|
# Convert the entire value structure to be JSON-safe
|
||||||
if isinstance(value, (ctypes.c_int, ctypes.c_uint, ctypes.c_float)):
|
serializable_value = _convert_ctypes_for_json(raw_value)
|
||||||
value = value.value
|
key = field.data_path if use_full_path else field.column_name
|
||||||
elif isinstance(value, ctypes.Array):
|
row_dict[key] = serializable_value
|
||||||
value = list(value)
|
|
||||||
row_dict[field.column_name] = value
|
|
||||||
self.json_data_buffer.append(row_dict)
|
self.json_data_buffer.append(row_dict)
|
||||||
|
|
||||||
if batch.batch_id % 20 == 0:
|
if batch.batch_id % 20 == 0:
|
||||||
@ -416,20 +414,17 @@ class AppController:
|
|||||||
"JSON export was enabled, but no data batches were generated. Skipping file creation."
|
"JSON export was enabled, but no data batches were generated. Skipping file creation."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
output_dir = Path(self.view.out_output_dir_var.get())
|
output_dir = Path(self.view.out_output_dir_var.get())
|
||||||
basename = self.view.out_basename_var.get()
|
basename = self.view.out_basename_var.get()
|
||||||
path = (output_dir / basename).with_suffix(".json")
|
path = (output_dir / basename).with_suffix(".json")
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
f"Writing {len(self.json_data_buffer)} records to JSON file: {path}"
|
f"Writing {len(self.json_data_buffer)} records to JSON file: {path}"
|
||||||
)
|
)
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
json.dump(self.json_data_buffer, f, indent=4)
|
json.dump(self.json_data_buffer, f, indent=4)
|
||||||
log.info("JSON file written successfully.")
|
log.info("JSON file written successfully.")
|
||||||
|
except (IOError, TypeError) as e:
|
||||||
except (IOError, ValueError) as e:
|
|
||||||
log.error(f"Failed to write JSON output file: {e}")
|
log.error(f"Failed to write JSON output file: {e}")
|
||||||
finally:
|
finally:
|
||||||
self.json_data_buffer.clear()
|
self.json_data_buffer.clear()
|
||||||
@ -455,16 +450,12 @@ class AppController:
|
|||||||
def handle_worker_completion(self, msg: Dict[str, Any]):
|
def handle_worker_completion(self, msg: Dict[str, Any]):
|
||||||
status = "Interrupted" if msg.get("interrupted") else "Complete"
|
status = "Interrupted" if msg.get("interrupted") else "Complete"
|
||||||
log.info(f"--- Process {status}. ---")
|
log.info(f"--- Process {status}. ---")
|
||||||
|
|
||||||
# Write buffered JSON data before closing files
|
|
||||||
if self.view.out_output_json_var.get():
|
if self.view.out_output_json_var.get():
|
||||||
self._write_json_buffer_to_file()
|
self._write_json_buffer_to_file()
|
||||||
|
|
||||||
self._close_all_files()
|
self._close_all_files()
|
||||||
self.is_processing = False
|
self.is_processing = False
|
||||||
self.worker_process = None
|
self.worker_process = None
|
||||||
self.view.update_ui_for_processing_state(False)
|
self.view.update_ui_for_processing_state(False)
|
||||||
|
|
||||||
is_cpp_success = "Conversion process completed successfully" in msg.get(
|
is_cpp_success = "Conversion process completed successfully" in msg.get(
|
||||||
"message", ""
|
"message", ""
|
||||||
)
|
)
|
||||||
@ -473,7 +464,6 @@ class AppController:
|
|||||||
log.info(
|
log.info(
|
||||||
f"C++ converter successfully generated: {self.last_generated_out_file}"
|
f"C++ converter successfully generated: {self.last_generated_out_file}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if stats := msg.get("stats"):
|
if stats := msg.get("stats"):
|
||||||
self._log_summary(stats)
|
self._log_summary(stats)
|
||||||
|
|
||||||
|
|||||||
@ -11,30 +11,21 @@ import logging
|
|||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List
|
||||||
import queue
|
import queue
|
||||||
|
|
||||||
|
from .gui_utils import center_window
|
||||||
from ..utils import logger
|
from ..utils import logger
|
||||||
from ..core.export_profiles import ExportProfile
|
from ..core.export_profiles import ExportProfile
|
||||||
|
|
||||||
log = logger.get_logger(__name__)
|
log = logger.get_logger(__name__)
|
||||||
|
|
||||||
# --- Import Version Info FOR THE WRAPPER ITSELF ---
|
|
||||||
try:
|
try:
|
||||||
# Use absolute import based on package name
|
|
||||||
from radar_data_reader import _version as wrapper_version
|
from radar_data_reader import _version as wrapper_version
|
||||||
|
|
||||||
WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})"
|
WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})"
|
||||||
WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}"
|
WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}"
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# This might happen if you run the wrapper directly from source
|
|
||||||
# without generating its _version.py first (if you use that approach for the wrapper itself)
|
|
||||||
WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)"
|
WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)"
|
||||||
WRAPPER_BUILD_INFO = "Wrapper build time unknown"
|
WRAPPER_BUILD_INFO = "Wrapper build time unknown"
|
||||||
|
|
||||||
# --- Constants for Version Generation ---
|
|
||||||
DEFAULT_VERSION = "0.0.0+unknown"
|
|
||||||
DEFAULT_COMMIT = "Unknown"
|
|
||||||
DEFAULT_BRANCH = "Unknown"
|
|
||||||
# --- End Constants ---
|
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(tk.Frame):
|
class MainWindow(tk.Frame):
|
||||||
"""The main application window (View)."""
|
"""The main application window (View)."""
|
||||||
@ -70,10 +61,9 @@ class MainWindow(tk.Frame):
|
|||||||
self.out_output_dir_var = tk.StringVar()
|
self.out_output_dir_var = tk.StringVar()
|
||||||
self.out_basename_var = tk.StringVar()
|
self.out_basename_var = tk.StringVar()
|
||||||
self.out_output_csv_var = tk.BooleanVar(value=True)
|
self.out_output_csv_var = tk.BooleanVar(value=True)
|
||||||
self.out_csv_use_tab_var = tk.BooleanVar(
|
self.out_csv_use_tab_var = tk.BooleanVar(value=False)
|
||||||
value=False
|
|
||||||
) # New variable for tab separator
|
|
||||||
self.out_output_json_var = tk.BooleanVar(value=False)
|
self.out_output_json_var = tk.BooleanVar(value=False)
|
||||||
|
self.out_use_full_path_var = tk.BooleanVar(value=False) # New variable
|
||||||
self.out_csv_profile_var = tk.StringVar()
|
self.out_csv_profile_var = tk.StringVar()
|
||||||
self.out_json_profile_var = tk.StringVar()
|
self.out_json_profile_var = tk.StringVar()
|
||||||
|
|
||||||
@ -145,7 +135,7 @@ class MainWindow(tk.Frame):
|
|||||||
self.out_browse_button.grid(row=0, column=2, padx=5, pady=5)
|
self.out_browse_button.grid(row=0, column=2, padx=5, pady=5)
|
||||||
self.out_filepath_var.trace_add("write", self.controller.on_out_config_changed)
|
self.out_filepath_var.trace_add("write", self.controller.on_out_config_changed)
|
||||||
|
|
||||||
output_frame = ttk.LabelFrame(parent, text="CSV/JSON Output Configuration")
|
output_frame = ttk.LabelFrame(parent, text="Output Configuration")
|
||||||
output_frame.grid(row=1, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
|
output_frame.grid(row=1, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
|
||||||
output_frame.columnconfigure(1, weight=1)
|
output_frame.columnconfigure(1, weight=1)
|
||||||
ttk.Label(output_frame, text="Output Directory:").grid(
|
ttk.Label(output_frame, text="Output Directory:").grid(
|
||||||
@ -154,7 +144,6 @@ class MainWindow(tk.Frame):
|
|||||||
out_dir_entry = ttk.Entry(output_frame, textvariable=self.out_output_dir_var)
|
out_dir_entry = ttk.Entry(output_frame, textvariable=self.out_output_dir_var)
|
||||||
out_dir_entry.grid(row=0, column=1, sticky="ew", padx=5)
|
out_dir_entry.grid(row=0, column=1, sticky="ew", padx=5)
|
||||||
|
|
||||||
# --- Buttons Frame ---
|
|
||||||
out_dir_buttons_frame = ttk.Frame(output_frame)
|
out_dir_buttons_frame = ttk.Frame(output_frame)
|
||||||
out_dir_buttons_frame.grid(row=0, column=2, padx=5)
|
out_dir_buttons_frame.grid(row=0, column=2, padx=5)
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
@ -177,49 +166,49 @@ class MainWindow(tk.Frame):
|
|||||||
row=1, column=1, columnspan=2, sticky="ew", padx=5
|
row=1, column=1, columnspan=2, sticky="ew", padx=5
|
||||||
)
|
)
|
||||||
|
|
||||||
formats_frame = ttk.LabelFrame(parent, text="Output Formats & Profiles")
|
# --- Reorganized Options Grid ---
|
||||||
|
formats_frame = ttk.LabelFrame(parent, text="Output Formats & Options")
|
||||||
formats_frame.grid(row=2, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
|
formats_frame.grid(row=2, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
|
||||||
formats_frame.columnconfigure(1, weight=1)
|
formats_frame.columnconfigure(1, weight=1)
|
||||||
formats_frame.columnconfigure(2, weight=1) # Add weight to third column
|
|
||||||
|
|
||||||
# CSV Options
|
# CSV Options
|
||||||
csv_options_frame = ttk.Frame(formats_frame)
|
|
||||||
csv_options_frame.grid(row=0, column=0, columnspan=3, sticky="ew")
|
|
||||||
|
|
||||||
ttk.Checkbutton(
|
ttk.Checkbutton(
|
||||||
csv_options_frame,
|
formats_frame, text="Generate .csv file", variable=self.out_output_csv_var
|
||||||
text="Generate .csv file",
|
).grid(row=0, column=0, sticky="w", padx=5, pady=2)
|
||||||
variable=self.out_output_csv_var,
|
|
||||||
).pack(side=tk.LEFT, padx=(5, 10))
|
|
||||||
self.out_csv_profile_combobox = ttk.Combobox(
|
self.out_csv_profile_combobox = ttk.Combobox(
|
||||||
csv_options_frame,
|
formats_frame,
|
||||||
textvariable=self.out_csv_profile_var,
|
textvariable=self.out_csv_profile_var,
|
||||||
state="readonly",
|
state="readonly",
|
||||||
width=20,
|
width=25,
|
||||||
)
|
)
|
||||||
self.out_csv_profile_combobox.pack(side=tk.LEFT, padx=5)
|
self.out_csv_profile_combobox.grid(row=0, column=1, sticky="w", padx=5)
|
||||||
ttk.Checkbutton(
|
|
||||||
csv_options_frame,
|
|
||||||
text="Use Tab Separator",
|
|
||||||
variable=self.out_csv_use_tab_var,
|
|
||||||
).pack(side=tk.LEFT, padx=(10, 5))
|
|
||||||
|
|
||||||
# JSON Options
|
# JSON Options
|
||||||
json_options_frame = ttk.Frame(formats_frame)
|
|
||||||
json_options_frame.grid(row=1, column=0, columnspan=3, sticky="ew")
|
|
||||||
|
|
||||||
ttk.Checkbutton(
|
ttk.Checkbutton(
|
||||||
json_options_frame,
|
formats_frame, text="Generate .json file", variable=self.out_output_json_var
|
||||||
text="Generate .json file",
|
).grid(row=1, column=0, sticky="w", padx=5, pady=2)
|
||||||
variable=self.out_output_json_var,
|
|
||||||
).pack(side=tk.LEFT, padx=(5, 10))
|
|
||||||
self.out_json_profile_combobox = ttk.Combobox(
|
self.out_json_profile_combobox = ttk.Combobox(
|
||||||
json_options_frame,
|
formats_frame,
|
||||||
textvariable=self.out_json_profile_var,
|
textvariable=self.out_json_profile_var,
|
||||||
state="readonly",
|
state="readonly",
|
||||||
width=20,
|
width=25,
|
||||||
)
|
)
|
||||||
self.out_json_profile_combobox.pack(side=tk.LEFT, padx=5)
|
self.out_json_profile_combobox.grid(row=1, column=1, sticky="w", padx=5)
|
||||||
|
|
||||||
|
# General Formatting Options in a separate sub-frame for alignment
|
||||||
|
options_subframe = ttk.Frame(formats_frame)
|
||||||
|
options_subframe.grid(row=0, column=2, rowspan=2, sticky="w", padx=(20, 5))
|
||||||
|
|
||||||
|
ttk.Checkbutton(
|
||||||
|
options_subframe,
|
||||||
|
text="Use Tab Separator (CSV)",
|
||||||
|
variable=self.out_csv_use_tab_var,
|
||||||
|
).pack(anchor="w")
|
||||||
|
ttk.Checkbutton(
|
||||||
|
options_subframe,
|
||||||
|
text="Use Full Path for Headers",
|
||||||
|
variable=self.out_use_full_path_var,
|
||||||
|
).pack(anchor="w")
|
||||||
|
|
||||||
action_frame = ttk.Frame(parent)
|
action_frame = ttk.Frame(parent)
|
||||||
action_frame.grid(row=3, column=0, columnspan=3, pady=(10, 0))
|
action_frame.grid(row=3, column=0, columnspan=3, pady=(10, 0))
|
||||||
@ -243,11 +232,9 @@ class MainWindow(tk.Frame):
|
|||||||
|
|
||||||
def _create_rec_converter_tab(self, parent):
|
def _create_rec_converter_tab(self, parent):
|
||||||
parent.columnconfigure(1, weight=1)
|
parent.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
input_frame = ttk.LabelFrame(parent, text="Input REC Sequence")
|
input_frame = ttk.LabelFrame(parent, text="Input REC Sequence")
|
||||||
input_frame.grid(row=0, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
|
input_frame.grid(row=0, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
|
||||||
input_frame.columnconfigure(1, weight=1)
|
input_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
ttk.Label(input_frame, text="First .rec File:").grid(
|
ttk.Label(input_frame, text="First .rec File:").grid(
|
||||||
row=0, column=0, padx=5, pady=5, sticky="w"
|
row=0, column=0, padx=5, pady=5, sticky="w"
|
||||||
)
|
)
|
||||||
@ -258,19 +245,16 @@ class MainWindow(tk.Frame):
|
|||||||
ttk.Button(
|
ttk.Button(
|
||||||
input_frame, text="Browse...", command=self.controller.select_rec_file
|
input_frame, text="Browse...", command=self.controller.select_rec_file
|
||||||
).grid(row=0, column=2, padx=5)
|
).grid(row=0, column=2, padx=5)
|
||||||
|
|
||||||
ttk.Label(input_frame, text="Number of Files (/n):").grid(
|
ttk.Label(input_frame, text="Number of Files (/n):").grid(
|
||||||
row=1, column=0, padx=5, pady=5, sticky="w"
|
row=1, column=0, padx=5, pady=5, sticky="w"
|
||||||
)
|
)
|
||||||
rec_file_count_spinbox = ttk.Spinbox(
|
ttk.Spinbox(
|
||||||
input_frame,
|
input_frame,
|
||||||
from_=1,
|
from_=1,
|
||||||
to=1000,
|
to=1000,
|
||||||
textvariable=self.rec_file_count_var,
|
textvariable=self.rec_file_count_var,
|
||||||
width=10,
|
width=10,
|
||||||
)
|
).grid(row=1, column=1, padx=5, pady=5, sticky="w")
|
||||||
rec_file_count_spinbox.grid(row=1, column=1, padx=5, pady=5, sticky="w")
|
|
||||||
|
|
||||||
self.rec_filepath_var.trace_add("write", self.controller.on_rec_config_changed)
|
self.rec_filepath_var.trace_add("write", self.controller.on_rec_config_changed)
|
||||||
self.rec_file_count_var.trace_add(
|
self.rec_file_count_var.trace_add(
|
||||||
"write", self.controller.on_rec_config_changed
|
"write", self.controller.on_rec_config_changed
|
||||||
@ -279,13 +263,11 @@ class MainWindow(tk.Frame):
|
|||||||
output_frame = ttk.LabelFrame(parent, text="Generated .out File")
|
output_frame = ttk.LabelFrame(parent, text="Generated .out File")
|
||||||
output_frame.grid(row=1, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
|
output_frame.grid(row=1, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
|
||||||
output_frame.columnconfigure(1, weight=1)
|
output_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
ttk.Label(output_frame, text="Output Directory:").grid(
|
ttk.Label(output_frame, text="Output Directory:").grid(
|
||||||
row=0, column=0, padx=5, pady=5, sticky="w"
|
row=0, column=0, padx=5, pady=5, sticky="w"
|
||||||
)
|
)
|
||||||
rec_dir_entry = ttk.Entry(output_frame, textvariable=self.rec_output_dir_var)
|
rec_dir_entry = ttk.Entry(output_frame, textvariable=self.rec_output_dir_var)
|
||||||
rec_dir_entry.grid(row=0, column=1, sticky="ew", padx=5)
|
rec_dir_entry.grid(row=0, column=1, sticky="ew", padx=5)
|
||||||
|
|
||||||
rec_dir_buttons_frame = ttk.Frame(output_frame)
|
rec_dir_buttons_frame = ttk.Frame(output_frame)
|
||||||
rec_dir_buttons_frame.grid(row=0, column=2, padx=5)
|
rec_dir_buttons_frame.grid(row=0, column=2, padx=5)
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
@ -300,7 +282,6 @@ class MainWindow(tk.Frame):
|
|||||||
self.rec_output_dir_var.get()
|
self.rec_output_dir_var.get()
|
||||||
),
|
),
|
||||||
).pack(side=tk.LEFT, padx=(5, 0))
|
).pack(side=tk.LEFT, padx=(5, 0))
|
||||||
|
|
||||||
ttk.Label(output_frame, text="Generated Filename:").grid(
|
ttk.Label(output_frame, text="Generated Filename:").grid(
|
||||||
row=1, column=0, padx=5, pady=5, sticky="w"
|
row=1, column=0, padx=5, pady=5, sticky="w"
|
||||||
)
|
)
|
||||||
@ -313,21 +294,18 @@ class MainWindow(tk.Frame):
|
|||||||
|
|
||||||
action_frame = ttk.Frame(parent)
|
action_frame = ttk.Frame(parent)
|
||||||
action_frame.grid(row=2, column=0, columnspan=3, pady=(10, 0))
|
action_frame.grid(row=2, column=0, columnspan=3, pady=(10, 0))
|
||||||
|
|
||||||
self.rec_convert_button = ttk.Button(
|
self.rec_convert_button = ttk.Button(
|
||||||
action_frame,
|
action_frame,
|
||||||
text="Convert REC to OUT",
|
text="Convert REC to OUT",
|
||||||
command=self.controller.start_rec_conversion,
|
command=self.controller.start_rec_conversion,
|
||||||
)
|
)
|
||||||
self.rec_convert_button.pack(side=tk.LEFT, padx=5)
|
self.rec_convert_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
self.rec_config_button = ttk.Button(
|
self.rec_config_button = ttk.Button(
|
||||||
action_frame,
|
action_frame,
|
||||||
text="g_reconverter Advanced Config...",
|
text="g_reconverter Advanced Config...",
|
||||||
command=self.controller.open_rec_config_editor,
|
command=self.controller.open_rec_config_editor,
|
||||||
)
|
)
|
||||||
self.rec_config_button.pack(side=tk.LEFT, padx=5)
|
self.rec_config_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
self.process_generated_out_button = ttk.Button(
|
self.process_generated_out_button = ttk.Button(
|
||||||
action_frame,
|
action_frame,
|
||||||
text="Process Generated .out File ->",
|
text="Process Generated .out File ->",
|
||||||
@ -341,28 +319,24 @@ class MainWindow(tk.Frame):
|
|||||||
parent, text="Live Data & Progress (.out Processor)"
|
parent, text="Live Data & Progress (.out Processor)"
|
||||||
)
|
)
|
||||||
status_frame.columnconfigure(1, weight=1)
|
status_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
self.progress_bar = ttk.Progressbar(
|
self.progress_bar = ttk.Progressbar(
|
||||||
status_frame, variable=self.progress_bar_var, maximum=100
|
status_frame, variable=self.progress_bar_var, maximum=100
|
||||||
)
|
)
|
||||||
self.progress_bar.grid(
|
self.progress_bar.grid(
|
||||||
row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(5, 2)
|
row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(5, 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
ttk.Label(status_frame, text="Progress:", anchor="e").grid(
|
ttk.Label(status_frame, text="Progress:", anchor="e").grid(
|
||||||
row=1, column=0, padx=(10, 5), pady=2, sticky="e"
|
row=1, column=0, padx=(10, 5), pady=2, sticky="e"
|
||||||
)
|
)
|
||||||
ttk.Label(status_frame, textvariable=self.progress_text_var, anchor="w").grid(
|
ttk.Label(status_frame, textvariable=self.progress_text_var, anchor="w").grid(
|
||||||
row=1, column=1, padx=5, pady=2, sticky="w"
|
row=1, column=1, padx=5, pady=2, sticky="w"
|
||||||
)
|
)
|
||||||
|
|
||||||
ttk.Label(status_frame, text="Batches Found:", anchor="e").grid(
|
ttk.Label(status_frame, text="Batches Found:", anchor="e").grid(
|
||||||
row=2, column=0, padx=(10, 5), pady=2, sticky="e"
|
row=2, column=0, padx=(10, 5), pady=2, sticky="e"
|
||||||
)
|
)
|
||||||
ttk.Label(status_frame, textvariable=self.batches_found_var, anchor="w").grid(
|
ttk.Label(status_frame, textvariable=self.batches_found_var, anchor="w").grid(
|
||||||
row=2, column=1, padx=5, pady=2, sticky="w"
|
row=2, column=1, padx=5, pady=2, sticky="w"
|
||||||
)
|
)
|
||||||
|
|
||||||
return status_frame
|
return status_frame
|
||||||
|
|
||||||
def _create_log_console_frame(self, parent):
|
def _create_log_console_frame(self, parent):
|
||||||
@ -374,7 +348,6 @@ class MainWindow(tk.Frame):
|
|||||||
log_frame, state=tk.DISABLED, wrap=tk.WORD, bg="#f0f0f0"
|
log_frame, state=tk.DISABLED, wrap=tk.WORD, bg="#f0f0f0"
|
||||||
)
|
)
|
||||||
self.log_widget.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
self.log_widget.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||||||
|
|
||||||
self.log_widget.tag_config("INFO", foreground="black")
|
self.log_widget.tag_config("INFO", foreground="black")
|
||||||
self.log_widget.tag_config("ERROR", foreground="red", font=("", 0, "bold"))
|
self.log_widget.tag_config("ERROR", foreground="red", font=("", 0, "bold"))
|
||||||
self.log_widget.tag_config("SUCCESS", foreground="green")
|
self.log_widget.tag_config("SUCCESS", foreground="green")
|
||||||
@ -388,7 +361,6 @@ class MainWindow(tk.Frame):
|
|||||||
def update_export_profiles(self, profiles: List[ExportProfile], **kwargs):
|
def update_export_profiles(self, profiles: List[ExportProfile], **kwargs):
|
||||||
profile_names = [p.name for p in profiles] if profiles else []
|
profile_names = [p.name for p in profiles] if profiles else []
|
||||||
active_out_profile = kwargs.get("active_out_profile", "")
|
active_out_profile = kwargs.get("active_out_profile", "")
|
||||||
|
|
||||||
for combo, var, active_name in [
|
for combo, var, active_name in [
|
||||||
(
|
(
|
||||||
self.out_csv_profile_combobox,
|
self.out_csv_profile_combobox,
|
||||||
@ -419,17 +391,13 @@ class MainWindow(tk.Frame):
|
|||||||
|
|
||||||
def update_ui_for_processing_state(self, is_processing: bool):
|
def update_ui_for_processing_state(self, is_processing: bool):
|
||||||
state = tk.DISABLED if is_processing else tk.NORMAL
|
state = tk.DISABLED if is_processing else tk.NORMAL
|
||||||
|
|
||||||
self.out_browse_button.config(state=state)
|
self.out_browse_button.config(state=state)
|
||||||
self.out_process_button.config(state=state)
|
self.out_process_button.config(state=state)
|
||||||
self.rec_convert_button.config(state=state)
|
self.rec_convert_button.config(state=state)
|
||||||
self.rec_config_button.config(state=state)
|
self.rec_config_button.config(state=state)
|
||||||
|
|
||||||
if is_processing:
|
if is_processing:
|
||||||
self.process_generated_out_button.config(state=tk.DISABLED)
|
self.process_generated_out_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
self.out_stop_button.config(state=tk.NORMAL if is_processing else tk.DISABLED)
|
self.out_stop_button.config(state=tk.NORMAL if is_processing else tk.DISABLED)
|
||||||
|
|
||||||
if is_processing:
|
if is_processing:
|
||||||
self.status_bar_var.set("Processing... Please wait.")
|
self.status_bar_var.set("Processing... Please wait.")
|
||||||
self.master.config(cursor="watch")
|
self.master.config(cursor="watch")
|
||||||
@ -440,11 +408,9 @@ class MainWindow(tk.Frame):
|
|||||||
self.master.config(cursor="")
|
self.master.config(cursor="")
|
||||||
|
|
||||||
def update_rec_tab_buttons_state(self, conversion_successful: bool):
|
def update_rec_tab_buttons_state(self, conversion_successful: bool):
|
||||||
"""Called by the controller to update button states after conversion."""
|
self.process_generated_out_button.config(
|
||||||
if conversion_successful:
|
state=tk.NORMAL if conversion_successful else tk.DISABLED
|
||||||
self.process_generated_out_button.config(state=tk.NORMAL)
|
)
|
||||||
else:
|
|
||||||
self.process_generated_out_button.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
def poll_result_queue(self):
|
def poll_result_queue(self):
|
||||||
try:
|
try:
|
||||||
@ -467,10 +433,10 @@ class MainWindow(tk.Frame):
|
|||||||
elif msg_type == "progress":
|
elif msg_type == "progress":
|
||||||
blocks_done = msg.get("blocks_done", 0)
|
blocks_done = msg.get("blocks_done", 0)
|
||||||
self.batches_found_var.set(str(msg.get("batch_id", "N/A")))
|
self.batches_found_var.set(str(msg.get("batch_id", "N/A")))
|
||||||
self.progress_text_var.set(
|
|
||||||
f"Block {blocks_done} / {self.total_blocks_for_progress}"
|
|
||||||
)
|
|
||||||
if self.total_blocks_for_progress > 0:
|
if self.total_blocks_for_progress > 0:
|
||||||
|
self.progress_text_var.set(
|
||||||
|
f"Block {blocks_done} / {self.total_blocks_for_progress}"
|
||||||
|
)
|
||||||
self.progress_bar_var.set(
|
self.progress_bar_var.set(
|
||||||
(blocks_done / self.total_blocks_for_progress) * 100
|
(blocks_done / self.total_blocks_for_progress) * 100
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user