# Copyright (C) 2024 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
from __future__ import annotations
import traceback
import uuid
from enum import Enum, auto
from logging import getLogger
from pathlib import Path
from typing import TYPE_CHECKING, Any, NamedTuple
from collections.abc import Iterable
import numpy as np
from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtGui import QFont, QPalette, QColor
from PyQt5.QtWidgets import QTabBar, QApplication, QTreeWidgetItem
from qt_material import apply_stylesheet
from mantidimaging.core.data import ImageStack
from mantidimaging.core.data.dataset import _get_stack_data_type, Dataset
from mantidimaging.core.io.loader.loader import create_loading_parameters_for_file_path
from mantidimaging.core.io.utility import find_projection_closest_to_180, THRESHOLD_180
from mantidimaging.core.utility.data_containers import ProjectionAngles
from mantidimaging.gui.dialogs.async_task import start_async_task_view
from mantidimaging.gui.mvp_base import BasePresenter
from mantidimaging.gui.windows.stack_visualiser.view import StackVisualiserView
from .model import MainWindowModel
from mantidimaging.gui.windows.main.image_save_dialog import ImageSaveDialog
if TYPE_CHECKING:
from mantidimaging.gui.windows.main import MainWindowView # pragma: no cover
from mantidimaging.gui.dialogs.async_task.task import TaskWorkerThread
RECON_TEXT = "Recon"
settings = QSettings('mantidproject', 'Mantid Imaging')
[docs]
class StackId(NamedTuple):
id: uuid.UUID
name: str
logger = getLogger(__name__)
[docs]
class Notification(Enum):
IMAGE_FILE_LOAD = auto()
IMAGE_FILE_SAVE = auto()
REMOVE_STACK = auto()
RENAME_STACK = auto()
NEXUS_LOAD = auto()
NEXUS_SAVE = auto()
FOCUS_TAB = auto()
ADD_RECON = auto()
SHOW_ADD_STACK_DIALOG = auto()
DATASET_ADD = auto()
TAB_CLICKED = auto()
SHOW_MOVE_STACK_DIALOG = auto()
MOVE_STACK = auto()
[docs]
class MainWindowPresenter(BasePresenter):
LOAD_ERROR_STRING = "Failed to load stack. Error: {}"
SAVE_ERROR_STRING = "Failed to save data. Error: {}"
view: MainWindowView
def __init__(self, view: MainWindowView):
super().__init__(view)
self.model = MainWindowModel()
self.stack_visualisers: dict[uuid.UUID, StackVisualiserView] = {}
[docs]
def notify(self, signal: Notification, **baggage):
try:
if signal == Notification.IMAGE_FILE_LOAD:
self.load_image_files()
elif signal == Notification.IMAGE_FILE_SAVE:
self.save_image_files()
elif signal == Notification.REMOVE_STACK:
self._delete_container(**baggage)
elif signal == Notification.RENAME_STACK:
self._do_rename_stack(**baggage)
elif signal == Notification.NEXUS_LOAD:
self.load_nexus_file()
elif signal == Notification.NEXUS_SAVE:
self.save_nexus_file()
elif signal == Notification.FOCUS_TAB:
self._restore_and_focus_tab(**baggage)
elif signal == Notification.ADD_RECON:
self._add_recon_to_dataset(**baggage)
elif signal == Notification.SHOW_ADD_STACK_DIALOG:
self._show_add_stack_to_dataset_dialog(**baggage)
elif signal == Notification.DATASET_ADD:
self.handle_add_images_to_existing_dataset_from_dialog()
elif signal == Notification.TAB_CLICKED:
self._on_tab_clicked(**baggage)
elif signal == Notification.SHOW_MOVE_STACK_DIALOG:
self._show_move_stack_dialog(**baggage)
elif signal == Notification.MOVE_STACK:
self._move_stack(**baggage)
except Exception as e:
self.show_error(e, traceback.format_exc())
getLogger(__name__).exception("Notification handler failed")
def _get_stack_visualiser_by_name(self, search_name: str) -> StackVisualiserView | None:
"""
Uses the stack name to retrieve the QDockWidget object.
:param search_name: The name of the stack widget to find.
:return: The QDockWidget if it could be found, None otherwise.
"""
for stack_id in self.stack_visualiser_list:
if stack_id.name == search_name:
return self.active_stacks[stack_id.id]
return None
[docs]
def get_stack_id_by_name(self, search_name: str) -> uuid.UUID | None:
for stack_id in self.stack_visualiser_list:
if stack_id.name == search_name:
return stack_id.id
return None
[docs]
def add_log_to_sample(self, stack_id: uuid.UUID, log_file: Path) -> None:
self.model.add_log_to_sample(stack_id, log_file)
[docs]
def add_shuttercounts_to_sample(self, stack_id: uuid.UUID, shuttercount_file: Path) -> None:
self.model.add_shutter_counts_to_sample(stack_id, shuttercount_file)
def _do_rename_stack(self, current_name: str, new_name: str) -> None:
dock = self._get_stack_visualiser_by_name(current_name)
if dock is not None:
dock.setWindowTitle(new_name)
self.view.model_changed.emit()
[docs]
def load_image_files(self) -> None:
assert self.view.image_load_dialog is not None
par = self.view.image_load_dialog.get_parameters()
start_async_task_view(self.view, self.model.do_load_dataset, self._on_dataset_load_done, {'parameters': par})
[docs]
def load_nexus_file(self) -> None:
assert self.view.nexus_load_dialog is not None
dataset, _ = self.view.nexus_load_dialog.presenter.get_dataset()
self.model.add_dataset_to_model(dataset)
self._add_dataset_to_view(dataset)
self.view.model_changed.emit()
[docs]
def save_nexus_file(self) -> None:
assert self.view.nexus_save_dialog is not None
dataset_id = self.view.nexus_save_dialog.selected_dataset
start_async_task_view(self.view,
self.model.do_nexus_saving,
self._on_save_done, {
'dataset_id': dataset_id,
'path': self.view.nexus_save_dialog.save_path(),
'sample_name': self.view.nexus_save_dialog.sample_name(),
'save_as_float': self.view.nexus_save_dialog.save_as_float
},
busy=True)
[docs]
def load_image_stack(self, file_path: str) -> None:
start_async_task_view(self.view, self.model.load_images_into_mixed_dataset, self._on_dataset_load_done,
{'file_path': file_path})
def _open_window_if_not_open(self) -> None:
"""
Launches windows that requires loaded data if the CLI flags are set.
Resets args after window has opened.
"""
if self.view.args.operation() != "" and self.view.filters is None:
self.show_operation(self.view.args.operation())
self.view.args.clear_window_args()
if self.view.args.recon() and self.view.recon is None:
self.view.show_recon_window()
self.view.args.clear_window_args()
if self.view.args.spectrum_viewer() and self.view.spectrum_viewer is None:
self.view.show_spectrum_viewer_window()
self.view.args.clear_window_args()
def _on_dataset_load_done(self, task: TaskWorkerThread) -> None:
if task.was_successful():
self._add_dataset_to_view(task.result)
self.view.model_changed.emit()
task.result = None
self._open_window_if_not_open()
else:
raise RuntimeError(self.LOAD_ERROR_STRING.format(task.error))
def _add_dataset_to_view(self, dataset: Dataset) -> None:
"""
Takes a loaded dataset and tries to find a substitute 180 projection (if required) then creates the stack window
and dataset tree view items.
:param dataset: The loaded dataset.
"""
self.update_dataset_tree()
self.create_dataset_stack_visualisers(dataset)
if dataset.sample:
self.add_alternative_180_if_required(dataset)
def _create_and_tabify_stack_window(self, images: ImageStack, sample_dock: StackVisualiserView) -> None:
"""
Creates a new stack window with a given ImageStack object then makes sure it is placed on top of a
sample/original stack window.
:param images: The ImageStack object for the new stack window.
:param sample_dock: The existing stack window that the new one should be placed on top of.
"""
stack_visualiser = self._create_lone_stack_window(images)
self._tabify_stack_window(stack_visualiser, sample_dock)
[docs]
def get_active_stack_visualisers(self) -> list[StackVisualiserView]:
return list(self.active_stacks.values())
[docs]
def get_all_stacks(self) -> list[ImageStack]:
return self.model.images
[docs]
def get_all_180_projections(self) -> list[ImageStack]:
return self.model.proj180s
[docs]
def add_alternative_180_if_required(self, dataset: Dataset) -> None:
"""
Checks if the dataset has a 180 projection and tries to find an alternative if one is missing.
:param dataset: The loaded dataset.
"""
assert dataset.sample is not None
if dataset.sample.has_proj180deg() and dataset.sample.proj180deg.filenames: # type: ignore
return
else:
closest_projection, diff = find_projection_closest_to_180(dataset.sample.projections,
dataset.sample.projection_angles().value)
if diff <= THRESHOLD_180 or self.view.ask_to_use_closest_to_180(diff):
_180_arr = np.reshape(closest_projection, (1, ) + closest_projection.shape).copy()
proj180deg = ImageStack(_180_arr, name=f"{dataset.name}_180")
self.add_images_to_existing_dataset(dataset.id, proj180deg, "proj_180")
[docs]
def create_dataset_stack_visualisers(self, dataset: Dataset) -> StackVisualiserView:
"""
Creates the StackVisualiserView widgets for a new dataset.
"""
stacks = dataset.all
first_stack_vis = self._create_lone_stack_window(stacks[0])
self._tabify_stack_window(first_stack_vis)
for stack in stacks[1:]:
self._create_and_tabify_stack_window(stack, first_stack_vis)
self._focus_on_newest_stack_tab()
return first_stack_vis
def _focus_on_newest_stack_tab(self) -> None:
"""
Focuses on the newest stack when there is more than one being displayed.
"""
n_stack_visualisers = len(self.get_active_stack_visualisers())
if n_stack_visualisers <= 1:
return
tab_bar = self.view.findChild(QTabBar)
if tab_bar is not None:
last_stack_pos = n_stack_visualisers
# make Qt process the addition of the dock onto the main window
QApplication.sendPostedEvents()
tab_bar.setCurrentIndex(last_stack_pos)
[docs]
def create_single_tabbed_images_stack(self, images: ImageStack) -> StackVisualiserView:
"""
Creates a stack for a single ImageStack object and focuses on it.
:param images: The ImageStack object for the new stack window.
:return: The new StackVisualiserView.
"""
stack_vis = self._create_lone_stack_window(images)
self._tabify_stack_window(stack_vis)
self._focus_on_newest_stack_tab()
return stack_vis
def _create_lone_stack_window(self, images: ImageStack) -> StackVisualiserView:
"""
Creates a stack window and adds it to the stack list without tabifying.
:param images: The ImageStack array for the stack window to display.
:return: The new stack window.
"""
stack_vis = self.view.create_stack_window(images)
self.stack_visualisers[stack_vis.id] = stack_vis
return stack_vis
def _tabify_stack_window(self,
stack_window: StackVisualiserView,
tabify_stack: StackVisualiserView | None = None) -> None:
"""
Places the newly created stack window into a tab.
:param stack_window: The new stack window.
:param tabify_stack: The optional existing stack tab that needs to be
"""
current_stack_visualisers = self.get_active_stack_visualisers()
if tabify_stack is None and len(current_stack_visualisers) > 0:
for stack in current_stack_visualisers:
if stack_window is not stack:
self.view.tabifyDockWidget(stack, stack_window)
return
if tabify_stack is not None:
self.view.tabifyDockWidget(tabify_stack, stack_window)
def _on_tab_clicked(self, stack: StackVisualiserView) -> None:
self._set_tree_view_selection_with_id(stack.id)
[docs]
def update_dataset_tree(self) -> None:
self.view.clear_dataset_tree_widget()
for dataset_id, dataset in self.model.datasets.items():
dataset_item = self.view.add_toplevel_item_to_dataset_tree_widget(dataset.name, dataset_id)
attributes = [("Projections", dataset.sample), ("Flat Before", dataset.flat_before),
("Flat After", dataset.flat_after), ("Dark Before", dataset.dark_before),
("Dark After", dataset.dark_after), ("180", dataset.proj180deg),
("Sinograms", dataset.sinograms)]
for label, item in attributes:
if item:
self.view.add_item_to_dataset_tree_widget(label, item.id, dataset_item)
if dataset.recons:
recon_item = self.view.add_item_to_dataset_tree_widget("Recons", dataset.recons.id, dataset_item)
for recon in dataset.recons:
self.view.add_item_to_dataset_tree_widget(recon.name, recon.id, recon_item)
if dataset.stacks:
for image_stack in dataset.stacks:
self.view.add_item_to_dataset_tree_widget(image_stack.name, image_stack.id, dataset_item)
[docs]
def save_image_files(self) -> None:
assert isinstance(self.view.image_save_dialog, ImageSaveDialog)
kwargs = {
'images_id': self.view.image_save_dialog.selected_stack,
'output_dir': self.view.image_save_dialog.save_path(),
'name_prefix': self.view.image_save_dialog.name_prefix(),
'image_format': self.view.image_save_dialog.image_format(),
'overwrite': self.view.image_save_dialog.overwrite(),
'pixel_depth': self.view.image_save_dialog.pixel_depth()
}
start_async_task_view(self.view, self.model.do_images_saving, self._on_save_done, kwargs)
def _on_save_done(self, task: TaskWorkerThread) -> None:
if not task.was_successful():
raise RuntimeError(self.SAVE_ERROR_STRING.format(task.error))
@property
def stack_visualiser_list(self) -> list[StackId]:
stacks = [StackId(stack_id, widget.windowTitle()) for stack_id, widget in self.active_stacks.items()]
return sorted(stacks, key=lambda x: x.name)
@property
def datasets(self) -> Iterable[Dataset]:
return self.model.datasets.values()
@property
def all_dataset_ids(self) -> Iterable[uuid.UUID]:
return self.model.datasets.keys()
@property
def all_stack_ids(self) -> Iterable[uuid.UUID]:
stack_ids = []
for ds in self.model.datasets.values():
stack_ids += ds.all_image_ids
return stack_ids
@property
def stack_visualiser_names(self) -> list[str]:
return [widget.windowTitle() for widget in self.stack_visualisers.values()]
[docs]
def get_dataset(self, dataset_id: uuid.UUID) -> Dataset | None:
return self.model.datasets.get(dataset_id)
[docs]
def get_stack_visualiser(self, stack_id: uuid.UUID) -> StackVisualiserView:
return self.stack_visualisers[stack_id]
[docs]
def get_stack(self, stack_id: uuid.UUID) -> ImageStack:
images = self.model.get_images_by_uuid(stack_id)
if images is None:
raise RuntimeError(f"Stack not found: {stack_id}")
return images
[docs]
def get_stack_visualiser_history(self, stack_id: uuid.UUID) -> dict[str, Any]:
return self.get_stack_visualiser(stack_id).presenter.images.metadata
[docs]
def get_dataset_id_for_stack(self, stack_id: uuid.UUID) -> uuid.UUID:
return self.model.get_parent_dataset(stack_id)
@property
def active_stacks(self) -> dict[uuid.UUID, StackVisualiserView]:
return {stack_id: stack for (stack_id, stack) in self.stack_visualisers.items() if stack.isVisible()}
@property
def have_active_stacks(self) -> bool:
return len(self.active_stacks) > 0
[docs]
def get_stack_with_images(self, images: ImageStack) -> StackVisualiserView:
for _, sv in self.stack_visualisers.items():
if images is sv.presenter.images:
return sv
raise RuntimeError(f"Did not find stack {images} in stacks! Stacks: {self.stack_visualisers.items()}")
[docs]
def add_180_deg_file_to_dataset(self, dataset_id: uuid.UUID, _180_deg_file: str) -> None:
"""
Loads a 180 file then adds it to the dataset, creates a stack window, and updates the dataset tree view.
:param dataset_id: The ID of the dataset to update.
:param _180_deg_file: The filename for the 180 file.
"""
proj180deg = self.model.add_180_deg_to_dataset(dataset_id, _180_deg_file)
self.add_images_to_existing_dataset(dataset_id, proj180deg, "proj_180")
[docs]
def add_projection_angles_to_sample(self, stack_id: uuid.UUID, proj_angles: ProjectionAngles) -> None:
self.model.add_projection_angles_to_sample(stack_id, proj_angles)
[docs]
def load_stacks_from_folder(self, file_path: str) -> bool:
loading_params = create_loading_parameters_for_file_path(Path(file_path))
if loading_params is None:
return False
start_async_task_view(self.view, self.model.do_load_dataset, self._on_dataset_load_done,
{'parameters': loading_params})
return True
[docs]
def wizard_action_load(self) -> None:
self.view.show_image_load_dialog()
[docs]
def show_operation(self, operation_name: str) -> None:
self.view.show_filters_window()
self.view.filters.presenter.set_filter_by_name(operation_name) # type:ignore[union-attr]
[docs]
def wizard_action_show_reconstruction(self) -> None:
self.view.show_recon_window()
[docs]
def remove_item_from_tree_view(self, uuid_remove: uuid.UUID) -> None:
"""
Removes an item from the tree view using a given ID.
:param uuid_remove: The ID of the item to remove.
"""
for i in range(self.view.dataset_tree_widget.topLevelItemCount()):
top_level_item = self.view.dataset_tree_widget.topLevelItem(i)
if top_level_item.id == uuid_remove:
self.view.dataset_tree_widget.takeTopLevelItem(i)
return
for j in range(top_level_item.childCount()):
child_item = top_level_item.child(j)
if child_item.id == uuid_remove:
top_level_item.takeChild(j)
return
if child_item.childCount() > 0:
if self._remove_recon_item_from_tree_view(child_item, uuid_remove):
if child_item.childCount() == 0:
# Delete recon group when last recon item has been removed
top_level_item.takeChild(j)
return
def _set_tree_view_selection_with_id(self, uuid_select: uuid.UUID) -> None:
"""
Selects an item on the tree view using the given ID.
:param uuid_select: The ID of the item to select.
"""
for i in range(self.view.dataset_tree_widget.topLevelItemCount()):
top_level_item = self.view.dataset_tree_widget.topLevelItem(i)
if top_level_item.id == uuid_select:
self._select_tree_widget_item(top_level_item)
return
for j in range(top_level_item.childCount()):
child_item = top_level_item.child(j)
if child_item.id == uuid_select:
self._select_tree_widget_item(child_item)
return
if child_item.childCount() > 0:
for k in range(child_item.childCount()):
recon_item = child_item.child(k)
if recon_item.id == uuid_select:
self._select_tree_widget_item(recon_item)
return
def _select_tree_widget_item(self, tree_widget_item: QTreeWidgetItem) -> None:
"""
Clears the existing selection on the dataset tree view and selects a given item.
:param tree_widget_item: The item to select.
"""
self.view.dataset_tree_widget.clearSelection()
tree_widget_item.setSelected(True)
@staticmethod
def _remove_recon_item_from_tree_view(recon_group, uuid_remove: uuid.UUID) -> bool:
"""
Removes a recon item from the recon group in the tree view.
:param recon_group: The recon group.
:param uuid_remove: The ID of the recon data to remove.
:return: True if a recon with a matching ID was removed, False otherwise.
"""
recon_count = recon_group.childCount()
for i in range(recon_count):
recon_item = recon_group.child(i)
if recon_item.id == uuid_remove:
recon_group.takeChild(i)
return True
return False
[docs]
def add_stack_to_dictionary(self, stack: StackVisualiserView) -> None:
self.stack_visualisers[stack.id] = stack
def _delete_container(self, container_id: uuid.UUID) -> None:
"""
Informs the model to delete a container, then updates the view elements.
:param container_id: The ID of the container to delete.
"""
# We need the ids of the stacks that have been deleted to tidy up the stack visualiser tabs
removed_stack_ids = self.model.remove_container(container_id)
for stack_id in removed_stack_ids:
if stack_id in self.stack_visualisers:
self._delete_stack_visualiser(stack_id)
# If the container_id provided is not a stack id then we remove the entire container from the tree view,
# otherwise we remove the individual stacks that were deleted
tree_view_items_to_remove = [container_id] if container_id not in removed_stack_ids else removed_stack_ids
for item_id in tree_view_items_to_remove:
self.remove_item_from_tree_view(item_id)
self.view.model_changed.emit()
def _delete_stack_visualiser(self, stack_id: uuid.UUID) -> None:
"""
Deletes a stack and frees memory.
:param stack_id: The ID of the stack to delete.
"""
self.stack_visualisers[stack_id].image_view.close()
self.stack_visualisers[stack_id].presenter.delete_data()
self.stack_visualisers[stack_id].deleteLater()
del self.stack_visualisers[stack_id]
def _restore_and_focus_tab(self, stack_id: uuid.UUID) -> None:
"""
Makes a stack tab visible and brings it to the front. If dataset ID is given then nothing happens.
:param stack_id: The ID of the stack tab to focus on.
"""
if stack_id in self.model.datasets:
return
if stack_id in self.model.recon_list_ids:
return
if stack_id in self.model.image_ids:
self.stack_visualisers[stack_id].setVisible(True)
self.stack_visualisers[stack_id].raise_()
else:
raise RuntimeError(f"Unable to find stack with ID {stack_id}")
def _add_recon_to_dataset(self, recon_data: ImageStack, stack_id: uuid.UUID) -> None:
"""
Adds a recon to the dataset and tree view and creates a stack image view.
:param recon_data: The recon data.
:param stack_id: The ID of one of the stacks in the dataset that the recon data should be added to.
"""
parent_id = self.model.get_parent_dataset(stack_id)
self.add_images_to_existing_dataset(parent_id, recon_data, "Recon")
[docs]
def add_sinograms_to_dataset_and_update_view(self, sino_stack: ImageStack, original_stack_id: uuid.UUID) -> None:
"""
Adds sinograms to a dataset or replaces an existing one.
:param sino_stack: The sinogram stack.
:param original_stack_id: The ID of a stack in the dataset.
"""
parent_id = self.model.get_parent_dataset(original_stack_id)
self.add_images_to_existing_dataset(parent_id, sino_stack, "Sinograms")
def _show_add_stack_to_dataset_dialog(self, container_id: uuid.UUID) -> None:
"""
Asks the user to add a stack to a given dataset.
:param container_id: The ID of the dataset or stack.
"""
if container_id not in self.all_dataset_ids:
# get parent ID if selected item is a stack
container_id = self.get_dataset_id_for_stack(container_id)
self.view.show_add_stack_to_existing_dataset_dialog(container_id)
def _show_move_stack_dialog(self, stack_id: uuid.UUID) -> None:
"""
Shows the move stack dialog.
:param stack_id: The ID of the stack to move.
"""
dataset_id = self.get_dataset_id_for_stack(stack_id)
dataset = self.get_dataset(dataset_id)
if dataset is None:
raise RuntimeError(f"Failed to find dataset with ID {dataset_id}")
stack_data_type = _get_stack_data_type(stack_id, dataset)
self.view.show_move_stack_dialog(dataset_id, stack_id, dataset.name, stack_data_type)
[docs]
def handle_add_images_to_existing_dataset_from_dialog(self) -> None:
"""
Adds / replaces images to an existing dataset. Updates the tree view and deletes the previous stack if
necessary.
"""
assert self.view.add_to_dataset_dialog is not None
dataset_id = self.view.add_to_dataset_dialog.dataset_id
new_images = self.view.add_to_dataset_dialog.presenter.images
images_type = self.view.add_to_dataset_dialog.images_type
self.add_images_to_existing_dataset(dataset_id, new_images, images_type)
[docs]
def add_images_to_existing_dataset(self, dataset_id: uuid.UUID, new_images: ImageStack, images_type: str):
dataset = self.get_dataset(dataset_id)
assert dataset is not None
dataset.set_stack_by_type_name(images_type, new_images)
self.create_single_tabbed_images_stack(new_images)
self.update_dataset_tree()
self._close_unused_visualisers()
self.view.model_changed.emit()
def _close_unused_visualisers(self):
visualisers = set(self.stack_visualisers.keys())
stacks = {stack.id for stack in self.get_all_stacks()}
removed = visualisers - stacks
for stack_id in removed:
self._delete_stack_visualiser(stack_id)
def _move_stack(self, origin_dataset_id: uuid.UUID, stack_id: uuid.UUID, destination_stack_type: str,
destination_dataset_id: uuid.UUID) -> None:
"""
Moves a stack from one dataset to another.
:param origin_dataset_id: The ID of the origin dataset.
:param stack_id: The ID of the stack to move.
:param destination_stack_type: The data type the dataset should be when moved.
:param destination_dataset_id: The ID of the destination dataset.
"""
origin_dataset = self.get_dataset(origin_dataset_id)
destination_dataset = self.get_dataset(destination_dataset_id)
if origin_dataset is None:
raise RuntimeError(
f"Unable to find origin dataset with ID {origin_dataset_id} when attempting to move stack")
if destination_dataset is None:
raise RuntimeError(
f"Unable to find destination dataset with ID {destination_dataset_id} when attempting to move stack")
stack_to_move = self.get_stack(stack_id)
stack_to_move.name = self._create_strict_dataset_stack_name(destination_stack_type, destination_dataset.name)
origin_dataset.delete_stack(stack_id)
self.add_images_to_existing_dataset(destination_dataset_id, stack_to_move, destination_stack_type)
@staticmethod
def _create_strict_dataset_stack_name(stack_type: str, dataset_name: str) -> str:
"""
Creates a name for strict dataset stacks by using the dataset name and the image type.
:param stack_type: The type of stack in the StrictDataset.
:param dataset_name: The name of the dataset.
:return: A string for the stack name.
"""
return f"{stack_type} {dataset_name}"
[docs]
def do_update_UI(self) -> None:
if settings.value('use_os_defaults', defaultValue='True') == 'True':
extra_style = settings.value('extra_style_default')
theme = 'Fusion'
override_os_theme = 'False'
else:
extra_style = settings.value('extra_style')
use_dark_mode = settings.value('use_dark_mode')
theme = settings.value('theme_selection')
override_os_theme = settings.value('override_os_theme')
os_theme = settings.value('os_theme')
font = QFont(settings.value('default_font_family'), int(extra_style['font_size'].replace('px', '')))
app = QApplication.instance()
app.setFont(font)
if theme == 'Fusion':
if override_os_theme == 'False':
if os_theme == 'Light':
self.use_fusion_light_mode()
elif os_theme == 'Dark':
self.use_fusion_dark_mode()
else:
if use_dark_mode == 'True':
self.use_fusion_dark_mode()
else:
self.use_fusion_light_mode()
app.setStyle(theme)
app.setStyleSheet('')
else:
apply_stylesheet(app, theme=theme, invert_secondary=False, extra=extra_style)
[docs]
@staticmethod
def use_fusion_dark_mode() -> None:
palette = QPalette()
palette.setColor(QPalette.Window, QColor(53, 53, 53))
palette.setColor(QPalette.WindowText, Qt.white)
palette.setColor(QPalette.Base, QColor(25, 25, 25))
palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
palette.setColor(QPalette.ToolTipBase, Qt.black)
palette.setColor(QPalette.ToolTipText, Qt.white)
palette.setColor(QPalette.Text, Qt.white)
palette.setColor(QPalette.Button, QColor(53, 53, 53))
palette.setColor(QPalette.ButtonText, Qt.white)
palette.setColor(QPalette.BrightText, Qt.red)
palette.setColor(QPalette.Link, QColor(42, 130, 218))
palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
palette.setColor(QPalette.HighlightedText, Qt.black)
QApplication.instance().setPalette(palette)
[docs]
@staticmethod
def use_fusion_light_mode() -> None:
palette = QPalette()
QApplication.instance().setPalette(palette)