Dioptas/dioptas/controller/MainController.py

368 lines
14 KiB
Python
Executable File

# -*- coding: utf-8 -*-
# Dioptas - GUI program for fast processing of 2D X-ray diffraction data
# Principal author: Clemens Prescher (clemens.prescher@gmail.com)
# Copyright (C) 2014-2019 GSECARS, University of Chicago, USA
# Copyright (C) 2015-2018 Institute for Geology and Mineralogy, University of Cologne, Germany
# Copyright (C) 2019-2020 DESY, Hamburg, Germany
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import json
import datetime
import threading
import subprocess
import shlex
from functools import partial
from sys import platform as _platform
from qtpy import QtWidgets, QtCore
from ..widgets.MainWidget import MainWidget
from ..model.DioptasModel import DioptasModel
from ..widgets.UtilityWidgets import save_file_dialog, open_file_dialog
from . import CalibrationController
from .integration import IntegrationController
from .MaskController import MaskController
from .ConfigurationController import ConfigurationController
from .MapController import MapController
from dioptas import __version__
class MainController(object):
"""
Creates the main controller for Dioptas. Creates all the data objects and connects them with the other controllers
"""
def __init__(self, use_settings=True, settings_directory="default", config_file=None):
"""
:param use_settings: whether to use previously auto saved state of dioptas
:param settings_directory: directory where the settings are saved
:param config_file: a json file path with configuration, currently only used for quick_actions
"""
self.use_settings = use_settings
self.widget = MainWidget()
# create data
if settings_directory == "default":
self.settings_directory = os.path.join(os.path.expanduser("~"), ".Dioptas")
else:
self.settings_directory = settings_directory
self.model = DioptasModel()
self.calibration_controller = CalibrationController(
self.widget.calibration_widget, self.model
)
self.mask_controller = MaskController(self.widget.mask_widget, self.model)
self.integration_controller = IntegrationController(
self.widget.integration_widget, self.model
)
self.map_controller = MapController(self.widget.map_widget, self.model)
self.calibration_controller.activate()
self.integration_controller.image_controller.deactivate()
self.map_controller.deactivate()
self.mask_controller.deactivate()
self.configuration_controller = ConfigurationController(
configuration_widget=self.widget.configuration_widget,
dioptas_model=self.model,
controllers=[
self.calibration_controller,
self.mask_controller,
self.integration_controller,
self,
],
)
self.create_signals()
self.update_title()
if use_settings:
QtCore.QTimer.singleShot(0, self.load_default_settings)
self.setup_backup_timer()
if config_file is not None:
self.configuration = json.load(open(config_file, "r"))
self.create_external_actions()
self.current_tab_index = 0
def show_window(self):
"""
Displays the main window on the screen and makes it active.
"""
self.widget.show()
if _platform == "darwin":
self.widget.setWindowState(
self.widget.windowState() & ~QtCore.Qt.WindowMinimized
| QtCore.Qt.WindowActive
)
self.widget.activateWindow()
self.widget.raise_()
def create_signals(self):
"""
Creates subscriptions for changing tabs and also newly loaded files which will update the title of the main
window.
"""
self.widget.closeEvent = self.close_event
self.widget.show_configuration_menu_btn.toggled.connect(
self.widget.configuration_widget.setVisible
)
self.widget.calibration_mode_btn.toggled.connect(
self.widget.calibration_widget.setVisible
)
self.widget.mask_mode_btn.toggled.connect(self.widget.mask_widget.setVisible)
self.widget.integration_mode_btn.toggled.connect(
self.widget.integration_widget.setVisible
)
self.widget.map_mode_btn.toggled.connect(self.widget.map_widget.setVisible)
self.widget.mode_btn_group.buttonToggled.connect(self.tab_changed)
self.model.img_changed.connect(self.update_title)
self.model.pattern_changed.connect(self.update_title)
self.widget.save_btn.clicked.connect(self.save_btn_clicked)
self.widget.load_btn.clicked.connect(self.load_btn_clicked)
self.widget.reset_btn.clicked.connect(self.reset_btn_clicked)
def tab_changed(self):
"""
Function which is called when a tab has been selected (calibration, mask, or integration). Performs
needed initialization tasks.
:return:
"""
if self.widget.calibration_mode_btn.isChecked():
ind = 0
elif self.widget.mask_mode_btn.isChecked():
ind = 1
elif self.widget.integration_mode_btn.isChecked():
ind = 2
elif self.widget.map_mode_btn.isChecked():
ind = 3
else:
return
if ind == self.current_tab_index:
return
old_index = self.current_tab_index
self.current_tab_index = ind
# changing from mask tab will reintegrate the image
if old_index == 1: # mask tab
if self.model.use_mask and self.model.calibration_model.is_calibrated:
self.model.current_configuration.integrate_image_1d()
if self.model.current_configuration.auto_integrate_cake:
self.model.current_configuration.integrate_image_2d()
# update the GUI
if ind == 2: # integration tab
self.integration_controller.image_controller.update_image()
self.activate_mode(ind)
self.update_image_display_state(old_index, ind)
def activate_mode(self, mode_ind):
controllers = [
self.calibration_controller,
self.mask_controller,
self.integration_controller.image_controller,
self.map_controller
]
for i, controller in enumerate(controllers):
if i == mode_ind:
controller.activate()
else:
controller.deactivate()
def update_image_display_state(self, old_index, new_index):
img_widgets = [
self.widget.calibration_widget.img_widget,
self.widget.mask_widget.img_widget,
self.widget.integration_widget.img_widget,
self.widget.map_widget.img_plot_widget
]
old_display_state = img_widgets[old_index].get_display_state()
img_widgets[new_index].set_display_state(*old_display_state)
def update_title(self):
"""
Updates the title bar of the main window. The title bar will always show the current version of Dioptas, the
image or pattern filenames loaded and the current calibration name.
"""
img_filename = os.path.basename(self.model.img_model.filename)
pattern_filename = os.path.basename(self.model.pattern.filename)
calibration_name = self.model.calibration_model.calibration_name
year = datetime.datetime.now().year
dioptas_str = "Dioptas " + __version__ + " - © {} C. Prescher".format(year)
if img_filename == "" and pattern_filename == "":
self.widget.setWindowTitle(dioptas_str)
self.widget.integration_widget.img_frame.setWindowTitle(dioptas_str)
return
str = ""
if img_filename != "":
str += img_filename
elif img_filename == "" and pattern_filename != "":
str += pattern_filename
if not img_filename == pattern_filename and pattern_filename != "":
str += ", " + pattern_filename
if calibration_name != "":
str += ", calibration: " + calibration_name
str += " | " + dioptas_str
self.widget.setWindowTitle(str)
self.widget.integration_widget.img_frame.setWindowTitle(str)
def save_default_settings(self):
if not os.path.exists(self.settings_directory):
os.mkdir(self.settings_directory)
self.model.save(os.path.join(self.settings_directory, "config.dio"))
def load_default_settings(self):
config_path = os.path.join(self.settings_directory, "config.dio")
if os.path.isfile(config_path):
self.show_window()
if QtWidgets.QMessageBox.Yes == QtWidgets.QMessageBox.question(
self.widget,
"Recovering previous state.",
"Should Dioptas recover your previous Work?",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No,
):
self.model.load(os.path.join(self.settings_directory, "config.dio"))
else:
self.load_directories()
def setup_backup_timer(self):
self.backup_timer = QtCore.QTimer(self.widget)
self.backup_timer.timeout.connect(self.save_default_settings)
self.backup_timer.setInterval(600000) # every 10 minutes
self.backup_timer.start()
def save_directories(self):
"""
Currently used working directories for images, spectra, etc. are saved as csv file in the users directory for
reuse when Dioptas is started again without loading a configuration
"""
working_directories_path = os.path.join(
self.settings_directory, "working_directories.json"
)
json.dump(self.model.working_directories, open(working_directories_path, "w"))
def load_directories(self):
"""
Loads previously used Dioptas directory paths.
"""
working_directories_path = os.path.join(
self.settings_directory, "working_directories.json"
)
if os.path.exists(working_directories_path):
self.model.working_directories = json.load(
open(working_directories_path, "r")
)
def close_event(self, ev):
"""
Intervention of the Dioptas close event to save settings before closing the Program.
"""
if self.use_settings:
self.save_default_settings()
self.save_directories()
QtWidgets.QApplication.closeAllWindows()
ev.accept()
def save_btn_clicked(self):
try:
default_file_name = os.path.join(
self.model.working_directories["project"], "config.dio"
)
except (TypeError, KeyError):
default_file_name = "."
filename = save_file_dialog(
self.widget,
"Save Current Dioptas Project",
default_file_name,
filter="Dioptas Project (*.dio)",
)
if filename is not None and filename != "":
self.model.save(filename)
self.model.working_directories["project"] = os.path.dirname(filename)
def load_btn_clicked(self):
try:
default_file_name = os.path.join(
self.model.working_directories["project"], "config.dio"
)
except (TypeError, KeyError):
default_file_name = "."
filename = open_file_dialog(
self.widget,
"Load a Dioptas Project",
default_file_name,
filter="Dioptas Project (*.dio)",
)
if filename is not None and filename != "":
self.model.load(filename)
self.model.working_directories["project"] = os.path.dirname(filename)
def reset_btn_clicked(self):
if QtWidgets.QMessageBox.Yes == QtWidgets.QMessageBox.question(
self.widget,
"Resetting Dioptas.",
"Do you really want to reset Dioptas?\nAll unsaved work will be lost!",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No,
):
self.model.reset()
def create_external_actions(self):
self.widget.create_external_actions(self.configuration["external_actions"])
for action in self.configuration["external_actions"]:
self.widget.external_action_btns[action["name"]].clicked.connect(
partial(
self.execute_action,
action
)
)
def execute_action(self, action):
command = format(action["command"])
arguments = action["arguments"]
img_path = self.model.img_model.filename
frame_index = self.model.img_model.series_pos
combined_arguments = f"{arguments} \"{img_path}\" {frame_index}"
command_str = " ".join([command, combined_arguments])
# prepare command_str for Popen
args = shlex.split(command_str)
def run_command():
"""Run the command with arguments pulse the image file path."""
subprocess.Popen(args, shell=True)
threading.Thread(target=run_command).start()
return command_str