mirror of https://github.com/Dioptas/Dioptas.git
368 lines
14 KiB
Python
Executable File
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
|