import configparser
import os
from functools import partial
import h5py
import numpy as np
from PyQt6.QtCore import QCoreApplication, QThread, Qt
from PyQt6.QtWidgets import QApplication, QFileDialog, QMessageBox
from coseda.logging_utils import log_print
from coseda.io import config_to_paths
from cosedaUI.h5_metadata_loader import H5MetadataLoader
from cosedaUI.metadata_window import MetadataWindow
from cosedaUI import CenterFinderSettingsWindow, CenterRefinementSettingsWindow, PeakFinderSettingsWindow
from cosedaUI.workspace_functions import (
add_generated_ini_paths,
add_ini_files,
add_ini_paths,
load_last_directory,
load_last_workspace_state,
new_workspace,
open_workspace,
remove_ini_files,
save_workspace,
save_workspace_state,
)
from cosedaUI.workflow_wizzard import (
check_peakfinding_allowed,
check_findcenters_allowed,
check_refinecenters_allowed,
check_index_integrate_allowed,
)
[docs]
class WorkspaceMixin:
add_generated_ini_paths = add_generated_ini_paths
add_ini_files = add_ini_files
add_ini_paths = add_ini_paths
remove_ini_files = remove_ini_files
new_workspace = new_workspace
open_workspace = open_workspace
save_workspace = save_workspace
save_workspace_state = save_workspace_state
load_last_workspace_state = load_last_workspace_state
load_last_directory = load_last_directory
[docs]
def on_ini_select(self):
selected_items = self.ini_listbox.selectedItems()
if not selected_items:
return
ini_file_name = selected_items[0].text()
self.full_ini_file_path = self.workspace_ini_paths.get(ini_file_name)
# Notify listeners (e.g., Index & Integrate window)
if self.full_ini_file_path:
self.ini_selection_changed.emit(self.full_ini_file_path)
# Clear any previously selected indexing stream until a run provides one
self.on_indexing_run_stream_changed(None)
self.strongest_frame_index = None
self.current_frame_index = 0
self.go_to_strongest_frame_button.setEnabled(False)
self.open_file(self.full_ini_file_path)
self.file_name_label.setText(f"File: {ini_file_name}")
self.highlight_selected_file()
log_print(f'Selected {self.full_ini_file_path}', level="debug")
self.update_workflow_buttons()
[docs]
def open_peakfinder_settings_window(self):
if not self.full_ini_file_path or not os.path.exists(self.full_ini_file_path):
QMessageBox.warning(self, "No INI File", "Please load an INI file first.")
return
if self.settings_window_instance is None or not self.settings_window_instance.isVisible():
self.settings_window_instance = PeakFinderSettingsWindow(
parent=self,
ini_directory=self.ini_directory,
ini_file_path=self.full_ini_file_path
)
self.settings_window_instance.settings_applied.connect(self.apply_peakfinder_settings)
self.settings_window_instance.coordinate_selection_requested.connect(self.start_coordinate_selection)
# Connect the close signal to stop live preview when window is closed
self.settings_window_instance.settings_closed.connect(self.stop_live_preview)
# Reload file on close, preserving current frame
self.settings_window_instance.settings_closed.connect(
self.reload_file_preserve_frame
)
self.settings_window_instance.show()
else:
self.settings_window_instance.raise_()
self.settings_window_instance.activateWindow()
[docs]
def start_coordinate_selection(self):
# Track which window requested the coordinate so we can route the result
try:
self.coordinate_selection_target = self.sender()
except Exception:
self.coordinate_selection_target = None
self.coordinate_selection_active = True
# Change cursor to crosshair
self.image_label.setCursor(Qt.CursorShape.CrossCursor)
# Show hint in status area instead of modal popup (avoids focus issues e.g. on WSL)
if hasattr(self, "_set_status_message"):
self._set_status_message("Click on the image to select x0/y0")
[docs]
def stop_live_preview(self):
# Stop the live preview
self.live_preview_active = False
# Optionally update the image if needed
self.update_image()
log_print("Live preview stopped.", level="debug")
[docs]
def apply_peakfinder_settings(self, settings):
"""
Slot function to apply peak finder settings emitted from PeakFinderSettingsWindow.
"""
try:
self.current_peak_settings = settings
self.live_preview_active = settings.get('live_preview', False) # Handle live_preview
#QMessageBox.information(self, "Settings Applied", "Peak Finder settings have been applied.")
# Reprocess the current frame to display the peak preview immediately
self.update_image()
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to apply peak finder settings:\n{str(e)}")
[docs]
def open_centerfinder_settings_window(self):
"""
Opens the Center Finder Settings window.
"""
if not self.full_ini_file_path or not os.path.exists(self.full_ini_file_path):
QMessageBox.warning(self, "No INI File", "Please load an INI file first.")
return
# Prevent multiple instances
if self.centerfindersettings_window_instance is None or not self.centerfindersettings_window_instance.isVisible():
self.centerfindersettings_window_instance = CenterFinderSettingsWindow(
parent=self,
apply_callback=self.apply_center_finder_settings,
ini_directory=self.ini_directory,
ini_file_path=self.full_ini_file_path
)
self.centerfindersettings_window_instance.coordinate_selection_requested.connect(self.start_coordinate_selection)
# Reload the file when the dialog closes (preserves current frame)
self.centerfindersettings_window_instance.finished.connect(
lambda _result=None: self.reload_file_preserve_frame()
)
self.centerfindersettings_window_instance.setModal(False)
self.centerfindersettings_window_instance.show()
else:
self.centerfindersettings_window_instance.raise_()
self.centerfindersettings_window_instance.activateWindow()
[docs]
def open_centerrefinement_settings_window(self):
"""
Opens the Center Refinement Settings window.
"""
if not self.full_ini_file_path or not os.path.exists(self.full_ini_file_path):
QMessageBox.warning(self, "No INI File", "Please load an INI file first.")
return
# Prevent multiple instances
if self.centerrefinementsettings_window_instance is None or not self.centerrefinementsettings_window_instance.isVisible():
self.centerrefinementsettings_window_instance = CenterRefinementSettingsWindow(
parent=self,
ini_directory=self.ini_directory,
ini_file_path=self.full_ini_file_path
)
# Reload the file when the dialog closes (preserves current frame)
self.centerrefinementsettings_window_instance.finished.connect(
lambda _result=None: self.reload_file_preserve_frame()
)
self.centerrefinementsettings_window_instance.show()
else:
self.centerrefinementsettings_window_instance.raise_()
self.centerrefinementsettings_window_instance.activateWindow()
[docs]
def reload_current_file(self):
"""
Reload the currently loaded INI/HDF5 while keeping the current frame.
"""
if not self.full_ini_file_path or not os.path.exists(self.full_ini_file_path):
QMessageBox.warning(self, "No INI File", "Please load an INI file first.")
return
try:
self.reload_file_preserve_frame()
except Exception as e:
QMessageBox.critical(self, "Reload Error", f"Failed to reload the current file:\n{e}")
[docs]
def reload_file_preserve_frame(self):
"""Reload the current INI/HDF5 but keep the same frame index."""
self.open_file(self.full_ini_file_path, preserve_frame=True)
[docs]
def apply_center_finder_settings(self, settings):
# Implement how to apply settings within your application
pass
[docs]
def ensure_index_dataset(self, hdf5_path):
"""
Ensure that an 'index' dataset exists in the 'entry/data' group.
If not, and if the length of the 'images' dataset matches one of the stage
position datasets, create the 'index' dataset as a sequence [0, 1, ..., num_frames-1].
Parameters:
hdf5_path (str): The path to the HDF5 file.
"""
try:
with h5py.File(hdf5_path, 'r+') as f:
data_group = f.get('entry/data')
if data_group is None:
log_print("The 'entry/data' group is missing in the HDF5 file.")
return
if 'index' in data_group:
# Index dataset already exists.
return
if 'images' not in data_group:
log_print("The 'images' dataset is missing in the HDF5 file.")
return
num_frames = data_group['images'].shape[0]
# Prefer checking stagepos_x_refined; if missing, use stagepos_y.
stagepos_len = None
if 'stagepos_x_refined' in data_group:
stagepos_len = data_group['stagepos_x_refined'].shape[0]
elif 'stagepos_y' in data_group:
stagepos_len = data_group['stagepos_y'].shape[0]
else:
log_print("No stage position dataset ('stagepos_x_refined' or 'stagepos_y') found.")
return
# If lengths match, create the index dataset.
if num_frames == stagepos_len:
index_data = np.arange(num_frames)
data_group.create_dataset('index', data=index_data)
log_print(f"Index dataset created with {num_frames} entries.")
else:
log_print(f"Mismatch: 'images' dataset has {num_frames} frames, but stage position dataset has {stagepos_len} entries. Index dataset not created.")
except Exception as e:
log_print(f"Error while ensuring the index dataset: {e}")
[docs]
def highlight_selected_file(self):
file_label = self.file_name_label.text()
if file_label.startswith("File: "):
file_name = file_label[6:]
files = [self.ini_listbox.item(i).text() for i in range(self.ini_listbox.count())]
if file_name in files:
idx = files.index(file_name)
self.ini_listbox.blockSignals(True)
self.ini_listbox.setCurrentRow(idx)
self.ini_listbox.blockSignals(False)
def _reset_ui_for_new_file(self):
"""Disable navigation controls while a new dataset is loading."""
self.total_frames = 0
self.original_image = None
self.raw_image = None
self.frame_label.setText("Frame: -/-")
self.n_peaks_label.setText("Number of Peaks: -")
self.beam_center_label.setText("Beam Center: -,-")
if hasattr(self, '_clear_strong_peaks_order'):
self._clear_strong_peaks_order()
if hasattr(self, '_clear_maxres_order'):
self._clear_maxres_order()
if hasattr(self, '_maxres_order_active'):
self._maxres_order_active = False
controls = [
self.prev_button,
self.next_button,
self.frame_slider,
self.frame_entry,
self.open_map_button,
self.go_to_strongest_frame_button,
self.process_menu,
self.view_menu,
self.inspect_image_action,
self.reload_file_btn,
]
for widget in controls:
if widget is not None:
widget.setEnabled(False)
# Avoid spurious valueChanged signals while resetting
self.frame_slider.blockSignals(True)
self.frame_slider.setValue(0)
self.frame_slider.setMaximum(0)
self.frame_slider.blockSignals(False)
self.frame_entry.clear()
self.show_point_checkbox.setEnabled(False)
self.show_point_checkbox.setChecked(False)
self.show_peaks_checkbox.setEnabled(False)
self.show_peaks_checkbox.setChecked(False)
if hasattr(self, 'show_strong_peaks_checkbox'):
self.show_strong_peaks_checkbox.blockSignals(True)
self.show_strong_peaks_checkbox.setEnabled(False)
self.show_strong_peaks_checkbox.setChecked(False)
self.show_strong_peaks_checkbox.blockSignals(False)
if hasattr(self, '_set_strong_peaks_option_available'):
self._set_strong_peaks_option_available(False)
if hasattr(self, '_set_maxres_option_available'):
self._set_maxres_option_available(False)
if hasattr(self, '_sync_frame_order_combo_with_mode'):
self._sync_frame_order_combo_with_mode()
if hasattr(self, 'show_indexing_checkbox'):
self.show_indexing_checkbox.setEnabled(False)
self.show_indexing_checkbox.setChecked(False)
if hasattr(self, 'show_resolution_rings_checkbox'):
self.show_resolution_rings_checkbox.setEnabled(False)
self.show_resolution_rings_checkbox.setChecked(False)
if hasattr(self, 'aurora_plot_action'):
self.aurora_plot_action.setEnabled(False)
self._rings_warning_shown = False
self._update_indexed_navigation_controls(False)
def _is_h5_loader_active(self) -> bool:
"""Return True while the background HDF5 loader is running."""
return self._h5_loader_active
def _start_h5_metadata_loader(self):
"""Kick off the background worker that reads essential HDF5 metadata."""
if not self.hdf5_path:
return
token = object()
self._h5_loader_token = token
self._h5_loader_active = True
thread = QThread(self)
worker = H5MetadataLoader(self.hdf5_path)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.finished.connect(partial(self._on_h5_metadata_ready, token))
worker.failed.connect(partial(self._on_h5_metadata_failed, token))
worker.finished.connect(thread.quit)
worker.failed.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
worker.failed.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
thread.finished.connect(self._clear_h5_loader_refs)
self._h5_loader_thread = thread
self._h5_loader_worker = worker
thread.start()
def _on_h5_metadata_ready(self, token, info: dict):
"""Handle successful completion of background metadata loading."""
if token != self._h5_loader_token:
return
self._h5_loader_active = False
self._h5_loader_token = None
self._clear_status_message()
QApplication.restoreOverrideCursor()
total_frames = int(info.get('total_frames', 0) or 0)
if total_frames <= 0:
QMessageBox.warning(
self,
"HDF5 Error",
"The selected HDF5 file does not contain any frames."
)
self.hdf5_path = None
self.update_workflow_buttons()
return
self.total_frames = total_frames
self._set_frame_slider_range_for_mode()
self.frame_slider.setEnabled(True)
self.frame_entry.setEnabled(True)
self.next_button.setEnabled(self.total_frames > 1)
self.prev_button.setEnabled(False)
self.process_menu.setEnabled(True)
self.view_menu.setEnabled(True)
self.inspect_image_action.setEnabled(True)
self.reload_file_btn.setEnabled(True)
stagepos_exists = bool(info.get('stagepos_exists', False))
self.open_map_button.setEnabled(stagepos_exists)
has_center = bool(info.get('has_center', False))
self.show_point_checkbox.setEnabled(has_center)
if not has_center:
self.show_point_checkbox.setChecked(False)
has_peaks = bool(info.get('has_peaks', False))
self.show_peaks_checkbox.setEnabled(has_peaks)
if not has_peaks:
self.show_peaks_checkbox.setChecked(False)
if hasattr(self, 'aurora_plot_action'):
self.aurora_plot_action.setEnabled(has_peaks)
has_npeaks = bool(info.get('has_npeaks', False))
if hasattr(self, 'show_strong_peaks_checkbox'):
if hasattr(self, '_set_strong_peaks_option_available'):
self._set_strong_peaks_option_available(has_npeaks)
if hasattr(self, '_set_maxres_option_available'):
self._set_maxres_option_available(has_npeaks)
self.show_strong_peaks_checkbox.blockSignals(True)
self.show_strong_peaks_checkbox.setEnabled(has_npeaks)
if not has_npeaks:
self.show_strong_peaks_checkbox.setChecked(False)
self._clear_strong_peaks_order()
if hasattr(self, '_clear_maxres_order'):
self._clear_maxres_order()
if hasattr(self, '_maxres_order_active'):
self._maxres_order_active = False
else:
# Rebuild order lazily for the newly loaded file.
self._clear_strong_peaks_order()
self._ensure_strong_peaks_order()
self.show_strong_peaks_checkbox.blockSignals(False)
if hasattr(self, '_sync_frame_order_combo_with_mode'):
self._sync_frame_order_combo_with_mode()
self._set_frame_slider_range_for_mode()
if hasattr(self, 'show_resolution_rings_checkbox'):
self.show_resolution_rings_checkbox.setEnabled(True)
self.calculate_intensities = False
intensity_exists = bool(info.get('intensity_exists', False))
if stagepos_exists and not intensity_exists:
msg = QMessageBox(self)
msg.setWindowTitle("Missing Intensity Datasets")
msg.setText("Intensity datasets are missing. Would you like to calculate them now?")
btn_this = msg.addButton("This file only", QMessageBox.ButtonRole.AcceptRole)
btn_all = msg.addButton("All workspace files", QMessageBox.ButtonRole.AcceptRole)
msg.addButton(QMessageBox.StandardButton.No)
msg.exec()
clicked = msg.clickedButton()
if clicked == btn_this:
self.calculate_intensities = True
elif clicked == btn_all:
self.calculate_intensities = True
self._calculate_intensities_for_workspace()
self.ensure_index_dataset(self.hdf5_path)
if self.calculate_intensities:
try:
from coseda.pipeline.frame_intensities import calculate_mean_intensities_dask_file_inprocess
with self._with_busy_status("Calculating intensities…"):
calculate_mean_intensities_dask_file_inprocess(self.hdf5_path, self.log_file_path)
except Exception as exc:
QMessageBox.critical(
self,
"Intensity Calculation Failed",
f"Failed to calculate intensity datasets:\n{exc}"
)
if stagepos_exists:
self.ensure_atlas_for_current_file()
self.update_map_thumbnail()
target_frame = 0
if self._pending_frame_restore is not None:
target_frame = min(max(int(self._pending_frame_restore), 0), self.total_frames - 1)
self.current_frame_index = target_frame
self._sync_frame_slider_with_current_frame()
try:
self.update_image()
finally:
self._pending_frame_restore = None
if has_npeaks:
self.find_strongest_frame()
else:
self.go_to_strongest_frame_button.setEnabled(False)
bit_depth = int(info.get('bit_depth', 16) or 16)
if bit_depth < 16:
self._set_warning_message(f"Warning: dataset is {bit_depth}-bit (16-bit recommended)")
self.update_workflow_buttons()
[docs]
def calculate_radial_profiles(self):
"""Compute radial intensity profiles for this file or all workspace files."""
if not getattr(self, 'hdf5_path', None):
QMessageBox.warning(self, "No file", "Open an HDF5 file first.")
return
msg = QMessageBox(self)
msg.setWindowTitle("Calculate radial profiles")
msg.setText("Compute per-frame radial intensity profiles?")
btn_this = msg.addButton("This file only", QMessageBox.ButtonRole.AcceptRole)
btn_all = msg.addButton("All workspace files", QMessageBox.ButtonRole.AcceptRole)
msg.addButton(QMessageBox.StandardButton.Cancel)
msg.exec()
clicked = msg.clickedButton()
if clicked == btn_this:
self._calculate_radial_profiles_for_file(self.hdf5_path, self.log_file_path)
elif clicked == btn_all:
self._calculate_radial_profiles_for_workspace()
def _calculate_radial_profiles_for_file(self, h5path, logpath):
"""Compute and save radial profiles for a single file."""
try:
from coseda.pipeline.frame_intensities import calculate_radial_profiles_dask_file_inprocess
with self._with_busy_status("Calculating radial profiles…"):
calculate_radial_profiles_dask_file_inprocess(h5path, logpath)
QMessageBox.information(
self, "Radial profiles",
"Saved entry/data/frame_radial_intensities."
)
except Exception as exc:
QMessageBox.critical(
self, "Radial profile calculation failed",
f"Failed to calculate radial profiles:\n{exc}"
)
def _calculate_radial_profiles_for_workspace(self):
"""Compute radial profiles for all workspace files that are missing them."""
from coseda.pipeline.frame_intensities import calculate_radial_profiles_dask_file_inprocess
workspace = getattr(self, 'workspace_ini_paths', {})
if not workspace:
QMessageBox.information(self, "Radial profiles", "No files in the workspace.")
return
files_to_process = []
for name, ini_path in workspace.items():
try:
_, _, _, logfile_path, _, h5file_path = config_to_paths(ini_path)
with h5py.File(h5file_path, 'r') as f:
data_group = f.get('entry/data')
if data_group is None:
continue
if 'frame_radial_intensities' not in data_group:
files_to_process.append((h5file_path, logfile_path))
except Exception:
continue
if not files_to_process:
QMessageBox.information(
self, "Radial profiles",
"All workspace files already have radial profiles."
)
return
total = len(files_to_process)
for idx, (h5path, logpath) in enumerate(files_to_process, 1):
try:
self._set_status_message(f"Calculating radial profiles ({idx}/{total})", animate=True)
QCoreApplication.processEvents()
calculate_radial_profiles_dask_file_inprocess(h5path, logpath)
except Exception as exc:
QMessageBox.warning(
self, "Radial profile calculation failed",
f"Failed for {os.path.basename(h5path)}:\n{exc}"
)
self._clear_status_message()
QMessageBox.information(
self, "Radial profiles",
f"Finished radial profiles for {total} file(s)."
)
def _calculate_intensities_for_workspace(self):
"""Calculate mean intensities for all workspace files that are missing them."""
from coseda.pipeline.frame_intensities import calculate_mean_intensities_dask_file_inprocess
workspace = getattr(self, 'workspace_ini_paths', {})
if not workspace:
return
files_to_process = []
for name, ini_path in workspace.items():
try:
_, _, _, logfile_path, _, h5file_path = config_to_paths(ini_path)
with h5py.File(h5file_path, 'r') as f:
data_group = f.get('entry/data')
if data_group is None:
continue
has_intensities = any(
key in data_group for key in ['frame_mean_intensities', 'frame_total_intensities']
)
if not has_intensities:
files_to_process.append((h5file_path, logfile_path))
except Exception:
continue
if not files_to_process:
return
total = len(files_to_process)
for idx, (h5path, logpath) in enumerate(files_to_process, 1):
try:
self._set_status_message(f"Calculating intensities ({idx}/{total})", animate=True)
QCoreApplication.processEvents()
calculate_mean_intensities_dask_file_inprocess(h5path, logpath)
except Exception as exc:
QMessageBox.warning(
self,
"Intensity Calculation Failed",
f"Failed for {os.path.basename(h5path)}:\n{exc}"
)
self._clear_status_message()
def _on_h5_metadata_failed(self, token, message: str):
"""Handle background loader failures."""
if token != self._h5_loader_token:
return
self._h5_loader_active = False
self._h5_loader_token = None
self._clear_status_message()
QApplication.restoreOverrideCursor()
QMessageBox.critical(
self,
"HDF5 Error",
f"Error reading HDF5 file:\n{message}"
)
self.hdf5_path = None
self.total_frames = 0
self._pending_frame_restore = None
self.file_name_label.setText("File: -")
self.update_workflow_buttons()
def _clear_h5_loader_refs(self):
"""Reset loader thread references once it finishes."""
self._h5_loader_thread = None
self._h5_loader_worker = None
[docs]
def open_file(self, ini_file_path=None, preserve_frame=False):
try:
if self._is_h5_loader_active():
QMessageBox.information(
self,
"Please wait",
"The application is still loading the previous file. Please wait a moment."
)
return
if isinstance(ini_file_path, bool):
ini_file_path = None
if not ini_file_path:
options = QFileDialog.Option.ReadOnly
ini_file_path, _ = QFileDialog.getOpenFileName(
self,
"Open INI File",
"",
"INI Files (*.ini);;All Files (*)",
options=options,
)
if not ini_file_path:
return
if not os.path.exists(ini_file_path):
QMessageBox.critical(self, "INI Error", f"INI file not found: {ini_file_path}")
return
restore_index = self.current_frame_index if preserve_frame else None
self._pending_frame_restore = restore_index
self.ini_directory = os.path.dirname(ini_file_path)
self.full_ini_file_path = ini_file_path
config = configparser.ConfigParser()
config.read(ini_file_path)
outputfolder, outputfolder_path, logfile, logfile_path, h5file, h5file_path = config_to_paths(ini_file_path)
self.hdf5_path = h5file_path
self.log_file_path = logfile_path
self.indexing_manager.reset_for_new_hdf5()
self._index_original_to_new = {}
self._last_peak_mismatch_frame = None
self._h5_width = None
self._h5_height = None
self._set_indexing_controls_checked(False)
self._update_indexed_navigation_controls(False)
if not h5file_path or not os.path.exists(h5file_path):
QMessageBox.critical(self, "HDF5 Error", f"HDF5 file not found: {h5file_path}")
self.hdf5_path = None
return
self.file_name_label.setText(f"File: {os.path.basename(h5file)}")
self.highlight_selected_file()
# If the map window is open, switch it to the newly selected file.
map_window = getattr(self, "map_window", None)
if map_window is not None and map_window.isVisible():
try:
map_window.set_hdf5_path(self.hdf5_path, ini_path=self.full_ini_file_path)
except Exception as exc:
log_print(f"Failed to switch map window file: {exc}", level="warning")
# Show a saved map (if any) as a thumbnail in the main window.
self.update_map_thumbnail()
batch_size_str = config.get('Parameters', 'centerfinding_batch_size', fallback="1000")
try:
batch_size = int(batch_size_str)
except ValueError:
batch_size = 1000
if 'Parameters' not in config:
config.add_section('Parameters')
config['Parameters']['centerfinding_batch_size'] = str(batch_size)
with open(ini_file_path, 'w') as config_file:
config.write(config_file)
self.current_frame_index = 0
self._reset_ui_for_new_file()
self.calculate_intensities = False
self._set_status_message("Loading HDF5 data", animate=True)
QCoreApplication.processEvents()
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
self._start_h5_metadata_loader()
except FileNotFoundError as e:
QMessageBox.critical(self, "File Not Found", f"The specified file was not found:\n{str(e)}")
except configparser.Error as e:
QMessageBox.critical(self, "Configuration Error", f"Error parsing INI file:\n{str(e)}")
except h5py.HDF5Error as e:
QMessageBox.critical(self, "HDF5 Error", f"Error reading HDF5 file:\n{str(e)}")
except Exception as e:
QMessageBox.critical(self, "Unexpected Error", f"An unexpected error occurred:\n{str(e)}")
[docs]
def update_frame_from_map_click(self, frame_index):
"""
Slot to update the frame index when a point is clicked in the map.
"""
if 0 <= frame_index < self.total_frames:
self.current_frame_index = frame_index
self.update_image()
else:
QMessageBox.warning(self, "Warning", f"Frame index {frame_index} is out of range.")
[docs]
def on_indexintegrate_requested_select_ini(self, ini_path: str):
"""Slot: the Index & Integrate window asked us to select a given INI path.
We set the QListWidget selection accordingly; this triggers on_ini_select()."""
try:
if not ini_path or not os.path.exists(ini_path):
return
# Find display name for this path in the workspace mapping
display_name = None
for name, path in self.workspace_ini_paths.items():
if os.path.abspath(path) == os.path.abspath(ini_path):
display_name = name
break
if display_name is None:
return
# Select in the list without re-entrancy storms
items = [self.ini_listbox.item(i).text() for i in range(self.ini_listbox.count())]
if display_name in items:
idx = items.index(display_name)
if self.ini_listbox.currentRow() != idx:
self.ini_listbox.setCurrentRow(idx)
except Exception as e:
log_print(f"on_indexintegrate_requested_select_ini failed: {e}", level="debug")