Source code for cosedaUI.mainwindow

"""Main application window for COSEDA UI."""

from coseda.logging_utils import log_print
import sys
import os
import webbrowser
import h5py
import cv2
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import colors
from PIL import Image, ImageQt, ImageDraw, ImageEnhance
import configparser
from typing import Optional
import math
from functools import partial
from cosedaUI.metadata_window import MetadataWindow
from coseda.io import config_to_paths
from coseda.nexus.paths import get_mask_dataset
from cosedaUI.update import check_for_updates


from cosedaUI.workflow_wizzard import (
    check_peakfinding_allowed,
    check_findcenters_allowed,
    check_refinecenters_allowed,
)



from cosedaUI import (
    MapWindow,
    open_about_window,
    get_version_from_log,
    set_style,
    load_changelog,
    CenterFinderSettingsWindow,
    PeakFinderSettingsWindow,
    CenterRefinementSettingsWindow,
    ImportTiffWindow,
    ImportMRCWindow,
    ImportEMDWindow
)

from cosedaUI.workspace_functions import (
    save_workspace_state,
    load_last_directory,
    add_ini_files,
    add_ini_paths,
    remove_ini_files,
    save_workspace,
    open_workspace,
    new_workspace,
    load_last_workspace_state)

# Import ImportH5Window
from cosedaUI.importers import ImportH5Window

# Add StripFilesWindow import
from cosedaUI.strip_file_window import StripFilesWindow
from cosedaUI.merging_window import MergingSettingsWindow
from cosedaUI.export import ExportMtzWindow
from cosedaUI.workflow_wizzard import check_index_integrate_allowed


from coseda.peakfinding.findpeaks import process_single_frame
from coseda.initialize import log_start, log_result
from coseda.dask_client_manager import DaskClientManager
from diffractem.peakfinder8_extension import peakfinder_8

from PyQt6.QtWidgets import (
    QApplication,
    QMainWindow,
    QWidget,
    QFileDialog,
    QMessageBox,
    QLabel,
    QMenuBar,
    QVBoxLayout,
    QHBoxLayout,
    QListWidget,
    QCheckBox,
    QSlider,
    QComboBox,
    QPushButton,
    QLineEdit,
    QFrame,
    QScrollArea,
    QSpinBox,
    QDialog,
    QInputDialog,
    QTextEdit,
    QGroupBox,
    QGridLayout,
    QSizePolicy,
)

# --- AboutDialog definition ---
from cosedaUI.about_window import AboutWindow
from PyQt6.QtCore import Qt, pyqtSignal, QObject, QSize, QRect, QCoreApplication, QTimer, QStandardPaths
from PyQt6.QtCore import QThread, pyqtSignal
from cosedaUI.image_tools import ImageInspector
from cosedaUI.image_tools import export_raw_image, export_display_image
from PyQt6.QtGui import QPixmap, QImage, QIcon, QSurfaceFormat, QAction

from PIL.ImageQt import ImageQt
from cosedaUI.indexing_stream_manager import IndexingStreamManager
from cosedaUI.h5_metadata_loader import H5MetadataLoader
from cosedaUI.mainwindow_sections import (
    ClusterMixin,
    ImportMixin,
    IndexingMixin,
    ViewerMixin,
    WorkflowMixin,
    WorkspaceMixin,
)

try:
    from CFstreamparser import parse_stream_file
except ImportError:
    parse_stream_file = None


APP_NAME = "COSEDA"
ORGANIZATION_NAME = "Stockholm University"


[docs] def enable_opengl(): format = QSurfaceFormat() format.setRenderableType(QSurfaceFormat.RenderableType.OpenGL) format.setProfile(QSurfaceFormat.OpenGLContextProfile.CoreProfile) format.setVersion(4, 1) # Adjust the OpenGL version if needed QSurfaceFormat.setDefaultFormat(format)
[docs] def user_config_dir(): """ Returns the path to the user configuration directory. """ return QStandardPaths.writableLocation(QStandardPaths.StandardLocation.ConfigLocation)
# --- AspectRatioWidget definition ---
[docs] class AspectRatioWidget(QWidget): """ A container that maintains a fixed aspect ratio based on the latest image loaded. """ def __init__(self, aspect_ratio=1.0, parent=None): super().__init__(parent) self.aspect_ratio = aspect_ratio self._layout = QVBoxLayout(self) self._layout.setContentsMargins(0, 0, 0, 0)
[docs] def set_aspect_ratio(self, ratio: float): self.aspect_ratio = ratio self.updateGeometry()
[docs] def hasHeightForWidth(self): return True
[docs] def heightForWidth(self, width): if self.aspect_ratio > 0: return int(width / self.aspect_ratio) return super().heightForWidth(width)
[docs] def sizeHint(self): # Suggest a size based on current width and aspect ratio w = self.width() if self.width() > 0 else 100 h = int(w / self.aspect_ratio) if self.aspect_ratio > 0 else 100 return QSize(w, h)
[docs] def minimumSizeHint(self): # Allow the widget to shrink arbitrarily return QSize(0, 0)
[docs] class IniListWidget(QListWidget): """QListWidget that accepts dropped INI files and emits their paths.""" files_dropped = pyqtSignal(list) def __init__(self, parent=None): super().__init__(parent) self.setAcceptDrops(True) self.setToolTip("Drag .ini files here to add them to the workspace") def _has_ini_urls(self, event) -> bool: mime = event.mimeData() if mime and mime.hasUrls(): return any( url.isLocalFile() and url.toLocalFile().lower().endswith(".ini") for url in mime.urls() ) return False
[docs] def dragEnterEvent(self, event): if self._has_ini_urls(event): event.acceptProposedAction() else: event.ignore()
[docs] def dragMoveEvent(self, event): if self._has_ini_urls(event): event.acceptProposedAction() else: event.ignore()
[docs] def dropEvent(self, event): paths = [] if event.mimeData().hasUrls(): for url in event.mimeData().urls(): if url.isLocalFile(): local_path = url.toLocalFile() if local_path.lower().endswith(".ini"): paths.append(local_path) if paths: self.files_dropped.emit(paths) event.acceptProposedAction() else: event.ignore()
[docs] class cosedaUI( ClusterMixin, WorkflowMixin, ImportMixin, IndexingMixin, ViewerMixin, WorkspaceMixin, QMainWindow, ): """Primary UI shell: workspace browser, viewers, and workflow launchers.""" ini_selection_changed = pyqtSignal(str) # emits absolute INI path when selection changes in main window def __init__(self): super().__init__() # Load icon from filesystem instead of Qt resource base_dir = os.path.dirname(__file__) icon_file = os.path.join(base_dir, "resources", "app_icon_512.png") self.setWindowIcon(QIcon(icon_file)) # Workspace INI paths mapping: display name -> full path self.workspace_ini_paths = {} # Path to saved workspace file self.workspace_file_path = None # Initialize variables self.current_frame_index = 0 self.total_frames = 0 self.hdf5_path = None self.original_image = None self.ini_directory = None self.is_slider_moving = False self.live_preview_active = False self.current_peak_settings = {} self.full_ini_file_path = None self._peakfinding_active = False self.settings_window_instance = None self.centerfindersettings_window_instance = None self.centerrefinementsettings_window_instance = None self.aurora_plot_window = None self.strongest_frame_index = None self.log_file_path = None self.x_scale_factor = 1.0 self.y_scale_factor = 1.0 self.window_title = APP_NAME self.changelog = str(load_changelog('CHANGELOG')) self.theme = set_style() self.config = configparser.ConfigParser() self.config_file_path = os.path.join(user_config_dir(), 'cosedaUI_config.ini') self._pending_frame_restore = None self._h5_loader_active = False self.coordinate_selection_active = False self.coordinate_selection_field = None self._indexing_toggle_guard = False self._index_original_to_new = {} self._last_peak_mismatch_frame = None self._h5_width = None self._h5_height = None self._strong_peaks_order = [] self._strong_peaks_rank = {} self._maxres_order = [] self._maxres_rank = {} self._maxres_order_active = False self.indexing_manager = IndexingStreamManager( stream_parser=parse_stream_file, get_hdf5_path=lambda: self.hdf5_path, get_total_frames=lambda: self.total_frames, log_fn=self._indexing_debug, ) self.init_ui() # Initialize ImageInspector self._inspector = ImageInspector( self.image_label, lambda: self.original_image, self ) # Delegate mouse events to inspector and preserve original handler self._original_mouse_press = self.image_label.mousePressEvent self.image_label.mousePressEvent = ( lambda e: self._inspector.on_mouse_press(e) or self._original_mouse_press(e) ) self.image_label.mouseMoveEvent = lambda e: self._inspector.on_mouse_move(e) self.image_label.mouseReleaseEvent = lambda e: self._inspector.on_mouse_release(e) # Connect Inspect Image menu action to inspector self.inspect_image_action.triggered.connect(lambda checked=False: self._inspector.start_inspect()) # Initialize image display frame as square before loading any image self.image_aspect_widget.set_aspect_ratio(1.0) initial_width = self.image_aspect_widget.width() self.image_aspect_widget.setFixedHeight(initial_width) # Debounce resize updates to redraw image on window resize self.resize_timer = QTimer(self) self.resize_timer.setSingleShot(True) self.resize_timer.timeout.connect(self.update_image) self.resizeInterval = 200 # milliseconds debounce interval # Restore workspace state on startup self.load_last_workspace_state() # Auto-load the first INI file in the workspace if one exists if self.ini_listbox.count() > 0: self.ini_listbox.setCurrentRow(0)
[docs] def show_about_window(self): """ Opens the About dialog defined in about_window.py. """ dlg = AboutWindow(self.theme, self.changelog, parent=self) dlg.exec()
[docs] def init_ui(self): self.setWindowTitle(self.window_title) #self.setGeometry(100, 100, 1200, 1000) self.create_menu() self.create_widgets() self.create_layout() self.show()
[docs] def create_menu(self): menubar = self.menuBar() # File Menu file_menu = menubar.addMenu('File') open_action = QAction('Open', self) open_action.triggered.connect(self.open_file) file_menu.addAction(open_action) # Import Submenu import_menu = file_menu.addMenu('Import') #import_emi_action = QAction('Import EMI', self) #import_emi_action.triggered.connect(lambda: import_emi(self.theme)) #import_menu.addAction(import_emi_action) #import_velox_action = QAction('Import Velox', self) #import_velox_action.triggered.connect(lambda: import_velox(self.theme)) #import_menu.addAction(import_velox_action) import_raw_tiff_action = QAction('Import TIFF', self) import_raw_tiff_action.triggered.connect(self.open_import_tiff_window) import_menu.addAction(import_raw_tiff_action) import_mrc_action = QAction('Import MRC', self) import_mrc_action.triggered.connect(self.open_import_mrc_window) import_menu.addAction(import_mrc_action) import_emd_action = QAction("Import EMD", self) import_emd_action.triggered.connect(self.open_import_emd_window) import_menu.addAction(import_emd_action) import_h5_action = QAction("Import HDF5", self) import_h5_action.triggered.connect(self.open_import_h5_window) import_menu.addAction(import_h5_action) # Process Menu self.process_menu = menubar.addMenu('Process') preflight_check_action = QAction('Preflight Check', self) preflight_check_action.triggered.connect(self.open_check_metadata_window) self.process_menu.addAction(preflight_check_action) self.process_menu.addSeparator() find_peaks_action = QAction('Find peaks', self) find_peaks_action.triggered.connect(self.open_peakfinder_settings_window) self.process_menu.addAction(find_peaks_action) strip_files_action = QAction('Remove Empty Frames', self) strip_files_action.triggered.connect(self.open_strip_files_window) self.process_menu.addAction(strip_files_action) find_beam_center_action = QAction('Find beam center', self) find_beam_center_action.triggered.connect(self.open_centerfinder_settings_window) self.process_menu.addAction(find_beam_center_action) refine_beam_centers_action = QAction('Refine beam centers', self) refine_beam_centers_action.triggered.connect(self.open_centerrefinement_settings_window) self.process_menu.addAction(refine_beam_centers_action) radial_profiles_action = QAction('Calculate radial profiles', self) radial_profiles_action.triggered.connect(self.calculate_radial_profiles) self.process_menu.addAction(radial_profiles_action) self.process_menu.addSeparator() index_integrate_action = QAction('Index && Integrate', self) index_integrate_action.triggered.connect(self.open_index_integrate_window) self.process_menu.addAction(index_integrate_action) merge_action = QAction('Merge', self) merge_action.triggered.connect(self.open_merge_window) self.process_menu.addAction(merge_action) self.process_menu.addSeparator() export_action = QAction('Export', self) export_action.triggered.connect(self.open_export_window) self.process_menu.addAction(export_action) # View Menu self.view_menu = menubar.addMenu('View') display_log_action = QAction('Display log', self) display_log_action.triggered.connect(self.display_log_file) display_log_action.setEnabled(False) self.view_menu.addAction(display_log_action) self.show_indexing_results_action = QAction('Show indexing results', self) self.show_indexing_results_action.setCheckable(True) self.show_indexing_results_action.setEnabled(False) self.show_indexing_results_action.toggled.connect( lambda checked: self._handle_indexing_toggle_request(checked, source='action') ) self.view_menu.addAction(self.show_indexing_results_action) # Tools Menu self.tools_menu = menubar.addMenu('Tools') self.inspect_image_action = QAction('Inspect Image', self) self.inspect_image_action.setEnabled(False) self.tools_menu.addAction(self.inspect_image_action) self.aurora_plot_action = QAction('Aurora plot', self) self.aurora_plot_action.triggered.connect(self.open_aurora_plot) self.aurora_plot_action.setEnabled(False) self.tools_menu.addAction(self.aurora_plot_action) self.unit_cell_hist_action = QAction('Unit cell histograms', self) self.unit_cell_hist_action.setEnabled(False) self.unit_cell_hist_action.triggered.connect(self.open_unit_cell_hist_window) self.tools_menu.addAction(self.unit_cell_hist_action) self.workspace_unit_cell_fit_action = QAction('Workspace unit cell fit', self) self.workspace_unit_cell_fit_action.triggered.connect(self.open_workspace_unit_cell_fit_window) self.tools_menu.addAction(self.workspace_unit_cell_fit_action) self.tools_menu.addSeparator() self.export_current_frame_action = QAction('Export current frame', self) self.export_current_frame_action.triggered.connect(self.export_current_frame) self.export_current_frame_action.setEnabled(False) self.tools_menu.addAction(self.export_current_frame_action) self.export_current_frame_as_displayed_action = QAction('Export current frame as displayed', self) self.export_current_frame_as_displayed_action.triggered.connect(self.export_current_frame_as_shown) self.export_current_frame_as_displayed_action.setEnabled(False) self.tools_menu.addAction(self.export_current_frame_as_displayed_action) # Dask Menu self.dask_menu = menubar.addMenu('Dask') set_cluster_action = QAction('Set Cluster Address...', self) set_cluster_action.triggered.connect(self.on_set_cluster_address) self.dask_menu.addAction(set_cluster_action) kill_current_action = QAction('Kill Current Cluster', self) kill_current_action.triggered.connect(self.on_kill_current_cluster) self.dask_menu.addAction(kill_current_action) kill_all_action = QAction('Kill All Clusters', self) kill_all_action.triggered.connect(self.on_kill_all_clusters) self.dask_menu.addAction(kill_all_action) open_dashboard_action = QAction('Open Dashboard', self) open_dashboard_action.triggered.connect(self.on_open_dashboard) self.dask_menu.addAction(open_dashboard_action) # Help Menu help_menu = menubar.addMenu('Help') documentation_action = QAction('Documentation', self) documentation_action.setToolTip("Open online documentation") documentation_action.triggered.connect( lambda: webbrowser.open( "https://ssed-kristallorakel-db0e5da266e6665ffcccf95a1e71af68a674b5ead43.gitlab.io" ) ) help_menu.addAction(documentation_action) report_issue_action = QAction('Report an Issue', self) report_issue_action.triggered.connect(self.report_issue) help_menu.addAction(report_issue_action) check_updates_action = QAction('Check for Updates', self) check_updates_action.setToolTip("Refresh Homebrew metadata and check if a newer COSEDA version is available") help_menu.addAction(check_updates_action) check_updates_action.triggered.connect(lambda checked=False: check_for_updates(checked, parent=self)) # About Action about_action = QAction('About', self) about_action.triggered.connect(self.show_about_window) help_menu.addAction(about_action) # Check for updates on startup (Homebrew installs only); notify only when an update is available. QTimer.singleShot(0, lambda: check_for_updates(False, parent=self, silent=True, notify_only_on_update=True))
[docs] def create_widgets(self): # Left Frame Widgets self.ini_listbox = IniListWidget() self.ini_listbox.itemSelectionChanged.connect(self.on_ini_select) self.ini_listbox.files_dropped.connect(self.add_ini_paths) # Workspace management buttons self.add_ini_btn = QPushButton("Add INI", self) self.add_ini_btn.clicked.connect(self.add_ini_files) self.remove_ini_btn = QPushButton("Remove INI", self) self.remove_ini_btn.clicked.connect(self.remove_ini_files) self.reload_file_btn = QPushButton("Reload current file", self) self.reload_file_btn.clicked.connect(self.reload_current_file) self.reload_file_btn.setEnabled(False) # Save Workspace button self.save_workspace_btn = QPushButton("Save Workspace", self) self.save_workspace_btn.clicked.connect(self.save_workspace) # Open Workspace button self.open_workspace_btn = QPushButton("Open Workspace", self) self.open_workspace_btn.clicked.connect(self.open_workspace) # Refresh preview after workspace loads self.open_workspace_btn.clicked.connect(self.update_image) # New Workspace button self.new_workspace_btn = QPushButton("New Workspace", self) self.new_workspace_btn.clicked.connect(self.new_workspace) # Workspace name label self.workspace_label = QLabel("Workspace: -", self) # Right Frame Widgets # Control Frame self.frame_label = QLabel("Frame: -/-") # Make the frame label width fit its content only self.frame_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) self.n_peaks_label = QLabel("Number of Peaks: -") self.beam_center_label = QLabel("Beam Center: -,-") self.file_name_label = QLabel("File: -") # Image Frame self.image_label = QLabel() # self.image_label.setScaledContents(True) # self.image_label.setMinimumSize(0, 0) self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # Allow label to expand to full width/height self.image_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.image_label.mousePressEvent = self.show_pixel_index # Aspect-ratio container for image display self.image_aspect_widget = AspectRatioWidget() # Change size policy to Preferred vertical self.image_aspect_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) self.image_aspect_widget._layout.addWidget(self.image_label) # Slider Frame self.prev_button = QPushButton("Previous") self.prev_button.clicked.connect(self.previous_frame) self.prev_button.setEnabled(False) self.next_button = QPushButton("Next") self.next_button.clicked.connect(self.next_frame) self.next_button.setEnabled(False) self.frame_slider = QSlider(Qt.Orientation.Horizontal) self.frame_slider.valueChanged.connect(self.change_frame) self.frame_slider.sliderReleased.connect(self.update_image) self.frame_slider.setEnabled(False) self.frame_entry = QLineEdit() self.frame_entry.returnPressed.connect(self.change_frame_from_entry) self.frame_entry.setEnabled(False) # Limit frame entry to max 7 digits and size it accordingly self.frame_entry.setMaxLength(7) fm = self.frame_entry.fontMetrics() max_width = fm.horizontalAdvance('0' * 7) + 10 self.frame_entry.setMaximumWidth(max_width) # Indexed-frame navigation buttons (initially hidden/disabled) self.prev_indexed_frame_button = QPushButton("Previous indexed frame") self.prev_indexed_frame_button.clicked.connect(self.show_previous_indexed_frame) self.prev_indexed_frame_button.setEnabled(False) self.prev_indexed_frame_button.setVisible(False) self.next_indexed_frame_button = QPushButton("Next indexed frame") self.next_indexed_frame_button.clicked.connect(self.show_next_indexed_frame) self.next_indexed_frame_button.setEnabled(False) self.next_indexed_frame_button.setVisible(False) self.latest_indexed_frame_button = QPushButton("Latest indexed frame") self.latest_indexed_frame_button.clicked.connect(self.show_latest_indexed_frame) self.latest_indexed_frame_button.setEnabled(False) self.latest_indexed_frame_button.setVisible(False) self._indexed_navigation_buttons = [ self.prev_indexed_frame_button, self.next_indexed_frame_button, self.latest_indexed_frame_button, ] self.status_label = QLabel() self.status_label.setVisible(False) self.status_label.setStyleSheet("color: #888; font-style: italic;") self._status_base_message = "" self._status_anim_phase = 0 self._status_anim_timer = QTimer(self) self._status_anim_timer.setInterval(400) self._status_anim_timer.timeout.connect(self._advance_status_animation) self._h5_loader_thread = None self._h5_loader_worker = None self._h5_loader_token = None # View Options Frame self.show_point_checkbox = QCheckBox("Show Beam Center") self.show_point_checkbox.stateChanged.connect(self.update_image) # Persist workspace when toggling beam center display self.show_point_checkbox.stateChanged.connect(self.save_workspace) self.show_point_checkbox.setEnabled(False) self.show_peaks_checkbox = QCheckBox("Show Peaks") self.show_peaks_checkbox.stateChanged.connect(self.update_image) # Persist workspace when toggling peaks display self.show_peaks_checkbox.stateChanged.connect(self.save_workspace) self.show_peaks_checkbox.setEnabled(False) self.frame_order_combo = QComboBox() self.frame_order_combo.addItems(["Chronological", "nPeaks", "Highest resolution"]) self.frame_order_combo.setCurrentIndex(0) self.frame_order_combo.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) self.frame_order_combo.currentIndexChanged.connect(self.on_frame_order_changed) self.show_strong_peaks_checkbox = QCheckBox("Show nPeaks order") self.show_strong_peaks_checkbox.stateChanged.connect(self.on_show_strong_peaks_toggled) self.show_strong_peaks_checkbox.stateChanged.connect(self.save_workspace) self.show_strong_peaks_checkbox.stateChanged.connect(self._sync_frame_order_combo_with_mode) self.show_strong_peaks_checkbox.setEnabled(False) self._set_strong_peaks_option_available(False) self._set_maxres_option_available(False) self._sync_frame_order_combo_with_mode() self.show_indexing_checkbox = QCheckBox("Show indexing results") self.show_indexing_checkbox.setEnabled(False) self.show_indexing_checkbox.stateChanged.connect( lambda state: self._handle_indexing_toggle_request( state == Qt.CheckState.Checked, source='checkbox' ) ) self.show_resolution_rings_checkbox = QCheckBox("Show Resolution Rings") self.show_resolution_rings_checkbox.stateChanged.connect(self.update_image) self.show_resolution_rings_checkbox.stateChanged.connect(self.save_workspace) self.show_resolution_rings_checkbox.setEnabled(False) # Colormap selector self.cmap_combo = QComboBox() cmap_options = ['inferno', 'viridis', 'plasma', 'magma', 'cividis', 'rainbow', 'gray', 'gray_r'] self.cmap_combo.addItems(cmap_options) self.cmap_combo.setCurrentText('inferno') self.cmap_combo.currentTextChanged.connect(self.update_image) # Persist workspace when colormap changes self.cmap_combo.currentTextChanged.connect(self.save_workspace) # Resize dropdown to its contents rather than stretching self.cmap_combo.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) # Log normalization option self.log_checkbox = QCheckBox("Log Normalize") self.log_checkbox.stateChanged.connect(self.update_image) # Persist workspace when log normalization toggles self.log_checkbox.stateChanged.connect(self.save_workspace) self.log_checkbox.setEnabled(True) # Sliders self.gamma_slider = QSlider(Qt.Orientation.Horizontal) self.gamma_slider.setMinimum(1) self.gamma_slider.setMaximum(50) self.gamma_slider.setValue(10) self.gamma_slider.valueChanged.connect(self.adjust_gamma_value) self.gamma_slider.sliderPressed.connect(self.on_slider_start) self.gamma_slider.sliderReleased.connect(self.on_slider_release) # Save workspace when gamma adjustment is finished self.gamma_slider.sliderReleased.connect(self.save_workspace) self.gamma_label = QLabel(f"{self.gamma_slider.value()/10:.1f}") self.brightness_slider = QSlider(Qt.Orientation.Horizontal) self.brightness_slider.setMinimum(0) self.brightness_slider.setMaximum(30) self.brightness_slider.setValue(10) self.brightness_slider.valueChanged.connect(self.adjust_brightness_value) self.brightness_slider.sliderPressed.connect(self.on_slider_start) self.brightness_slider.sliderReleased.connect(self.on_slider_release) # Save workspace when brightness adjustment is finished self.brightness_slider.sliderReleased.connect(self.save_workspace) self.brightness_label = QLabel(f"{self.brightness_slider.value()/10:.1f}") self.contrast_slider = QSlider(Qt.Orientation.Horizontal) self.contrast_slider.setMinimum(0) self.contrast_slider.setMaximum(30) self.contrast_slider.setValue(10) self.contrast_slider.valueChanged.connect(self.adjust_contrast_value) self.contrast_slider.sliderPressed.connect(self.on_slider_start) self.contrast_slider.sliderReleased.connect(self.on_slider_release) # Save workspace when contrast adjustment is finished self.contrast_slider.sliderReleased.connect(self.save_workspace) self.contrast_label = QLabel(f"{self.contrast_slider.value()/10:.1f}") # Buttons self.open_map_button = QPushButton("Open Atlas Viewer") self.open_map_button.clicked.connect(self.open_map) self.open_map_button.setEnabled(False) self.open_map_button.setToolTip("Open the interactive atlas viewer for the selected file.") self.go_to_strongest_frame_button = QPushButton("Max nPeaks frame") self.go_to_strongest_frame_button.clicked.connect(self.go_to_strongest_frame) self.go_to_strongest_frame_button.setEnabled(False)
# Inspect Image button removed; now in View menu
[docs] def create_layout(self): central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QHBoxLayout(central_widget) # Left column container for workspace and view options left_container = QWidget() # Constrain left column to a maximum width left_container.setMaximumWidth(260) left_container_layout = QVBoxLayout(left_container) left_container_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Left GroupBox (Equivalent to LabelFrame) left_group = QGroupBox("Workspace") left_layout = QVBoxLayout() # Align workspace contents to top left_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Display current workspace name left_layout.addWidget(self.workspace_label) # Workspace controls: New, Open, Save left_layout.addWidget(self.new_workspace_btn) left_layout.addWidget(self.open_workspace_btn) left_layout.addWidget(self.save_workspace_btn) # Workspace list left_layout.addWidget(self.ini_listbox) # Limit the INI list height without constraining the entire column self.ini_listbox.setMaximumHeight(300) # Reload current file button sits above Add/Remove controls left_layout.addWidget(self.reload_file_btn) # Add/Remove INI buttons below list btn_layout = QHBoxLayout() btn_layout.addWidget(self.add_ini_btn) btn_layout.addWidget(self.remove_ini_btn) left_layout.addLayout(btn_layout) left_group.setLayout(left_layout) # Add workspace box into left container left_container_layout.addWidget(left_group) # Finally add the combined left column to the main layout main_layout.addWidget(left_container) main_layout.setAlignment(left_container, Qt.AlignmentFlag.AlignTop) # Right panel (Contains multiple sections) right_container = QWidget() right_layout = QVBoxLayout(right_container) # Control Frame Layout inside Right GroupBox control_group = QGroupBox("Info") control_layout = QVBoxLayout() info_layout = QHBoxLayout() info_layout.addWidget(self.frame_label) info_layout.addWidget(self.n_peaks_label) info_layout.addWidget(self.beam_center_label) control_layout.addLayout(info_layout) control_layout.addWidget(self.file_name_label) control_group.setLayout(control_layout) right_layout.addWidget(control_group) # Image Frame Layout inside Right GroupBox image_group = QGroupBox("Image Display") image_layout = QVBoxLayout() # Use the aspect-ratio widget (no scroll area) image_layout.addWidget(self.image_aspect_widget) image_group.setLayout(image_layout) # Add image display without forcing extra vertical stretch right_layout.addWidget(image_group) # Slider Frame Layout inside Right GroupBox slider_group = QGroupBox("Navigate Frames") slider_layout = QVBoxLayout() slider_row = QHBoxLayout() slider_row.addWidget(self.prev_button) slider_row.addWidget(self.next_button) slider_row.addWidget(self.frame_slider) slider_row.addWidget(self.frame_entry) slider_layout.addLayout(slider_row) self.indexed_navigation_widget = QWidget() indexed_layout = QHBoxLayout(self.indexed_navigation_widget) indexed_layout.setContentsMargins(0, 0, 0, 0) indexed_layout.setSpacing(6) indexed_layout.addWidget(self.prev_indexed_frame_button) indexed_layout.addWidget(self.next_indexed_frame_button) indexed_layout.addWidget(self.latest_indexed_frame_button) self.indexed_navigation_widget.setVisible(False) slider_layout.addWidget(self.indexed_navigation_widget) slider_group.setLayout(slider_layout) right_layout.addWidget(slider_group) # View Options Frame Layout inside Right GroupBox view_options_group = QGroupBox("View Options") view_options_layout = QVBoxLayout() # Checkboxes Layout checkboxes_layout = QVBoxLayout() checkboxes_layout.addWidget(self.show_point_checkbox) checkboxes_layout.addWidget(self.show_peaks_checkbox) checkboxes_layout.addWidget(self.show_indexing_checkbox) checkboxes_layout.addWidget(self.show_resolution_rings_checkbox) checkboxes_layout.addWidget(self.log_checkbox) # Colormap selector layout cmap_layout = QHBoxLayout() cmap_layout.addWidget(QLabel("Colormap")) cmap_layout.addWidget(self.cmap_combo) checkboxes_layout.addLayout(cmap_layout) view_options_layout.addLayout(checkboxes_layout) # Gamma adjustment row gamma_row = QHBoxLayout() gamma_row.addWidget(QLabel("Gamma")) gamma_row.addWidget(self.gamma_slider) gamma_row.addWidget(self.gamma_label) view_options_layout.addLayout(gamma_row) # Brightness adjustment row brightness_row = QHBoxLayout() brightness_row.addWidget(QLabel("Brightness")) brightness_row.addWidget(self.brightness_slider) brightness_row.addWidget(self.brightness_label) view_options_layout.addLayout(brightness_row) # Contrast adjustment row contrast_row = QHBoxLayout() contrast_row.addWidget(QLabel("Contrast")) contrast_row.addWidget(self.contrast_slider) contrast_row.addWidget(self.contrast_label) view_options_layout.addLayout(contrast_row) # Buttons at bottom buttons_layout = QHBoxLayout() buttons_layout.addWidget(self.go_to_strongest_frame_button) view_options_layout.addLayout(buttons_layout) # Frame order selector at the bottom of the settings group frame_order_layout = QHBoxLayout() frame_order_layout.addWidget(QLabel("Frame order")) frame_order_layout.addWidget(self.frame_order_combo) view_options_layout.addLayout(frame_order_layout) view_options_group.setLayout(view_options_layout) # Add view options below workspace in left container left_container_layout.addWidget(view_options_group) status_group = QGroupBox("Status") status_layout = QVBoxLayout() status_layout.addWidget(self.status_label) status_group.setLayout(status_layout) left_container_layout.addWidget(status_group) # Container already has its layout assigned via QVBoxLayout(right_container) # Add right column and top-align it main_layout.addWidget(right_container, 0, Qt.AlignmentFlag.AlignTop) # main_layout.setAlignment(right_container, Qt.AlignmentFlag.AlignTop) # (removed as now redundant) # --- Workflow Group (Rightmost Column) --- workflow_group = QGroupBox("Workflow") workflow_layout = QVBoxLayout() # Add Preflight Check button at the start of workflow self.workflow_check_metadata_btn = QPushButton("Preflight Check") self.workflow_check_metadata_btn.setEnabled(False) # Tooltip explaining preflight check purpose self.workflow_check_metadata_btn.setToolTip( "Before we start processing, we want to ensure we have all necessary metadata. " "Most of this information should be automatically obtained from the metadata. " "If anything is missing, you can fill it in manually." ) workflow_layout.addWidget(self.workflow_check_metadata_btn) self.workflow_check_metadata_btn.clicked.connect(self.open_check_metadata_window) arrow_label0 = QLabel("↓") arrow_label0.setAlignment(Qt.AlignmentFlag.AlignHCenter) arrow_label0.setStyleSheet("font-size: 22px; color: #888;") workflow_layout.addWidget(arrow_label0) # Add disabled buttons for each workflow step, with arrows between them self.workflow_find_peaks_btn = QPushButton("Find peaks") self.workflow_find_peaks_btn.setToolTip("Open peakfinder settings and find diffraction spots") self.workflow_find_peaks_btn.setEnabled(False) workflow_layout.addWidget(self.workflow_find_peaks_btn) self.workflow_find_peaks_btn.clicked.connect(self.open_peakfinder_settings_window) arrow_label1 = QLabel("↓") arrow_label1.setAlignment(Qt.AlignmentFlag.AlignHCenter) arrow_label1.setStyleSheet("font-size: 22px; color: #888;") workflow_layout.addWidget(arrow_label1) # Insert Strip Files button and arrow self.workflow_strip_files_btn = QPushButton("Remove Empty Frames") self.workflow_strip_files_btn.setToolTip("Remove frames without reflections before processing. This step is optional but recommended in order to reduce uneccessary disk space usage.") self.workflow_strip_files_btn.setEnabled(False) workflow_layout.addWidget(self.workflow_strip_files_btn) self.workflow_strip_files_btn.clicked.connect(self.open_strip_files_window) arrow_label_strip = QLabel("↓") arrow_label_strip.setAlignment(Qt.AlignmentFlag.AlignHCenter) arrow_label_strip.setStyleSheet("font-size: 22px; color: #888;") workflow_layout.addWidget(arrow_label_strip) self.workflow_find_centers_btn = QPushButton("Find Centers") self.workflow_find_centers_btn.setToolTip("Find beam center positions") self.workflow_find_centers_btn.setEnabled(False) workflow_layout.addWidget(self.workflow_find_centers_btn) self.workflow_find_centers_btn.clicked.connect(self.open_centerfinder_settings_window) arrow_label2 = QLabel("↓") arrow_label2.setAlignment(Qt.AlignmentFlag.AlignHCenter) arrow_label2.setStyleSheet("font-size: 22px; color: #888;") workflow_layout.addWidget(arrow_label2) self.workflow_refine_centers_btn = QPushButton("Refine Centers") self.workflow_refine_centers_btn.setToolTip("Refine beam center positions to improve indexing") self.workflow_refine_centers_btn.setEnabled(False) workflow_layout.addWidget(self.workflow_refine_centers_btn) self.workflow_refine_centers_btn.clicked.connect(self.open_centerrefinement_settings_window) arrow_label3 = QLabel("↓") arrow_label3.setAlignment(Qt.AlignmentFlag.AlignHCenter) arrow_label3.setStyleSheet("font-size: 22px; color: #888;") workflow_layout.addWidget(arrow_label3) # Index & Integrate self.workflow_index_integrate_btn = QPushButton("Index && Integrate") self.workflow_index_integrate_btn.setToolTip("Index reflections and integrate intensities") self.workflow_index_integrate_btn.setEnabled(False) workflow_layout.addWidget(self.workflow_index_integrate_btn) self.workflow_index_integrate_btn.clicked.connect(self.open_index_integrate_window) # Shared arrow down to Disambiguate arrow_label4 = QLabel("↓") arrow_label4.setAlignment(Qt.AlignmentFlag.AlignHCenter) arrow_label4.setStyleSheet("font-size: 22px; color: #888;") workflow_layout.addWidget(arrow_label4) self.workflow_disambiguate_btn = QPushButton("Disambiguate") self.workflow_disambiguate_btn.setToolTip("Resolve indexing ambiguities") self.workflow_disambiguate_btn.setEnabled(False) workflow_layout.addWidget(self.workflow_disambiguate_btn) # self.workflow_disambiguate_btn.clicked.connect(self.open_disambiguate_window) arrow_label_merge = QLabel("↓") arrow_label_merge.setAlignment(Qt.AlignmentFlag.AlignHCenter) arrow_label_merge.setStyleSheet("font-size: 22px; color: #888;") workflow_layout.addWidget(arrow_label_merge) self.workflow_merge_btn = QPushButton("Merge") self.workflow_merge_btn.setToolTip("Merge reflections") self.workflow_merge_btn.setEnabled(False) workflow_layout.addWidget(self.workflow_merge_btn) self.workflow_merge_btn.clicked.connect(self.open_merge_window) arrow_label7 = QLabel("↓") arrow_label7.setAlignment(Qt.AlignmentFlag.AlignHCenter) arrow_label7.setStyleSheet("font-size: 22px; color: #888;") workflow_layout.addWidget(arrow_label7) self.workflow_export_btn = QPushButton("Export") self.workflow_export_btn.setToolTip("Export the final results to files") self.workflow_export_btn.setEnabled(False) workflow_layout.addWidget(self.workflow_export_btn) self.workflow_export_btn.clicked.connect(self.open_export_window) # Only size the workflow panel to its content workflow_group.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) workflow_group.setLayout(workflow_layout) # Workflow column: the workflow group with a saved-map preview below it. self.workflow_group = workflow_group workflow_column = QVBoxLayout() workflow_column.setAlignment(Qt.AlignmentFlag.AlignTop) workflow_column.addWidget(workflow_group) self.map_preview_group = QGroupBox("Atlas") map_preview_layout = QVBoxLayout() self.map_thumbnail = QLabel() self.map_thumbnail.setAlignment(Qt.AlignmentFlag.AlignCenter) # Ignored width so the thumbnail never forces the column wider than the workflow. self.map_thumbnail.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred) map_preview_layout.addWidget(self.open_map_button) map_preview_layout.addWidget(self.map_thumbnail) self.map_preview_group.setLayout(map_preview_layout) self.map_preview_group.setVisible(False) workflow_column.addWidget(self.map_preview_group) workflow_column.addStretch(1) workflow_container = QWidget() workflow_container.setLayout(workflow_column) main_layout.addWidget(workflow_container) main_layout.setAlignment(workflow_container, Qt.AlignmentFlag.AlignTop)
# Entry point function for launching the cosedaUI application. def _configure_macos_app_identity(): """Set process metadata that macOS may read before QApplication exists.""" if sys.platform != "darwin": return try: import importlib foundation = importlib.import_module("Foundation") nsbundle = getattr(foundation, "NSBundle", None) if nsbundle is not None: bundle = nsbundle.mainBundle() if bundle is not None: info = bundle.localizedInfoDictionary() or bundle.infoDictionary() if info is not None: info["CFBundleName"] = APP_NAME info["CFBundleDisplayName"] = APP_NAME except Exception: pass try: from ctypes import cdll, c_char_p libc = cdll.LoadLibrary("libc.dylib") if hasattr(libc, "setprogname"): libc.setprogname(c_char_p(APP_NAME.encode("utf-8"))) except Exception: pass
[docs] def run_app(): """ Entry point for launching the cosedaUI application. """ # Silence noisy Qt QPA/Wayland debug output unless the user explicitly overrides it os.environ.setdefault("QT_LOGGING_RULES", "qt.qpa.*=false") enable_opengl() log_print("Starting the application...", level="debug") _configure_macos_app_identity() QCoreApplication.setApplicationName(APP_NAME) QCoreApplication.setOrganizationName(ORGANIZATION_NAME) app = QApplication(sys.argv) if sys.platform == "darwin": # Keep native style by default; allow override via env (e.g. Fusion). qt_style = os.getenv("COSEDA_QT_STYLE", "").strip() if qt_style: try: app.setStyle(qt_style) log_print(f"Qt style set to '{qt_style}'", level="debug") except Exception as exc: log_print(f"Failed to set Qt style '{qt_style}': {exc}", level="debug") # Load global icon from filesystem base_dir = os.path.dirname(__file__) icon_file = os.path.join(base_dir, "resources", "app_icon_512.png") app.setWindowIcon(QIcon(icon_file)) app.setApplicationName(APP_NAME) app.setApplicationDisplayName(APP_NAME) if sys.platform.startswith("linux"): app.setDesktopFileName("coseda") app.setOrganizationName(ORGANIZATION_NAME) log_print("Initializing the viewer...", level="debug") viewer = cosedaUI() log_print("Running the app...", level="debug") return app.exec()
if __name__ == '__main__': sys.exit(run_app())