Source code for als.ui.windows

"""
Holds all windows used in the app
"""
import logging

from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtGui import QPixmap, QBrush, QColor
from PyQt5.QtWidgets import QMainWindow, QGraphicsScene, QGraphicsPixmapItem, QDialog
from qimage2ndarray import array2qimage

import als.model.data
from als import config
from als.config import CouldNotSaveConfig
from als.logic import Controller, SessionError, CriticalFolderMissing, WebServerStartFailure
from als.code_utilities import log
from als.model.data import STACKING_MODE_SUM, STACKING_MODE_MEAN, DYNAMIC_DATA
from als.ui.dialogs import PreferencesDialog, AboutDialog, error_box, warning_box, SaveWaitDialog, question, message_box
from als.ui.params_utils import update_controls_from_params, update_params_from_controls, reset_params, \
    set_sliders_defaults
from generated.als_ui import Ui_stack_window

_LOGGER = logging.getLogger(__name__)


# pylint: disable=R0904, R0902
[docs]class MainWindow(QMainWindow): """ ALS main window. """ _LOG_DOCK_INITIAL_HEIGHT = 150 @log def __init__(self, controller: Controller, parent=None): super().__init__(parent) self._controller = controller self._ui = Ui_stack_window() self._ui.setupUi(self) self.setWindowTitle("Astro Live Stacker") # populate stacking mode combo box= self._ui.cb_stacking_mode.blockSignals(True) stacking_modes = [STACKING_MODE_SUM, STACKING_MODE_MEAN] for stacking_mode in stacking_modes: self._ui.cb_stacking_mode.addItem(stacking_mode) self._ui.cb_stacking_mode.setCurrentIndex(stacking_modes.index(self._controller.get_stacking_mode())) self._ui.cb_stacking_mode.blockSignals(False) # update align checkbox self._ui.chk_align.setChecked(self._controller.get_align_before_stack()) # update save every frame checkbox self._ui.chk_save_every_image.setChecked(self._controller.get_save_every_image()) # prevent log dock to be too tall self.resizeDocks([self._ui.log_dock], [MainWindow._LOG_DOCK_INITIAL_HEIGHT], Qt.Vertical) # setup rgb controls and params self._rgb_controls = [ self._ui.chk_rgb_active, self._ui.sld_rgb_r, self._ui.sld_rgb_g, self._ui.sld_rgb_b, ] self._rgb_parameters = self._controller.get_rgb_parameters() set_sliders_defaults( [self._rgb_parameters[1], self._rgb_parameters[2], self._rgb_parameters[3]], [self._ui.sld_rgb_r, self._ui.sld_rgb_g, self._ui.sld_rgb_b] ) self._reset_rgb() # setup autostretch controls and params self._autostretch_controls = [ self._ui.chk_stretch_active, self._ui.cb_stretch_method, self._ui.sld_stretch_strength ] self._autostretch_parameters = self._controller.get_autostretch_parameters() set_sliders_defaults( [self._autostretch_parameters[2]], [self._ui.sld_stretch_strength] ) for label in self._autostretch_parameters[1].choices: self._ui.cb_stretch_method.addItem(label) self._reset_autostretch() # setup levels controls and parameters self._levels_controls = [ self._ui.chk_levels_active, self._ui.sld_black, self._ui.sld_midtones, self._ui.sld_white, ] self._levels_parameters = self._controller.get_levels_parameters() set_sliders_defaults( [self._levels_parameters[1], self._levels_parameters[2], self._levels_parameters[3]], [self._ui.sld_black, self._ui.sld_midtones, self._ui.sld_white] ) self._reset_levels() # setup exchanges with dynamic data self._controller.add_model_observer(self) self.update_display() config.register_log_receiver(self) self.setGeometry(*config.get_window_geometry()) # manage docks restoration out of 'image only' mode self._restore_log_dock = False self._restore_session_dock = False self._restore_processing_dock = False # setup image display self._scene = QGraphicsScene(self) self._ui.image_view.setScene(self._scene) self._image_item = None self.reset_image_view() if config.get_full_screen_active(): self._ui.action_full_screen.setChecked(True) else: self.show()
[docs] @log @pyqtSlot(bool) def on_chk_stretch_active_clicked(self, checked: bool): """ Qt slot executed when autostretch 'active' checkbox is clicked :param checked: is the box now checked ? :type: bool """ self._ui.btn_stretch_reload.setEnabled(checked) self._ui.btn_stretch_reset.setEnabled(checked) self._ui.btn_stretch_apply.setEnabled(checked) self._ui.cb_stretch_method.setEnabled(checked) self._ui.sld_stretch_strength.setEnabled(checked) self._apply_autostretch()
[docs] @log @pyqtSlot(bool) def on_chk_levels_active_clicked(self, checked: bool): """ Qt slot executed when levels 'active' checkbox is clicked :param checked: is the box now checked ? :type: bool """ self._ui.btn_levels_reload.setEnabled(checked) self._ui.btn_levels_reset.setEnabled(checked) self._ui.btn_levels_apply.setEnabled(checked) self._ui.sld_black.setEnabled(checked) self._ui.sld_midtones.setEnabled(checked) self._ui.sld_white.setEnabled(checked) self._apply_levels()
[docs] @log @pyqtSlot(bool) def on_chk_rgb_active_clicked(self, checked: bool): """ Qt slot executed when RGB 'active' checkbox is clicked :param checked: is the box now checked ? :type: bool """ self._ui.btn_rgb_reload.setEnabled(checked) self._ui.btn_rgb_reset.setEnabled(checked) self._ui.btn_rgb_apply.setEnabled(checked) self._ui.sld_rgb_r.setEnabled(checked) self._ui.sld_rgb_g.setEnabled(checked) self._ui.sld_rgb_b.setEnabled(checked) self._apply_rgb()
@log @pyqtSlot(name="on_btn_stretch_apply_clicked") def _apply_autostretch(self): """ Apply autostretch processing """ update_params_from_controls(self._autostretch_parameters, self._autostretch_controls) self._controller.apply_processing() @log @pyqtSlot(name="on_btn_rgb_apply_clicked") def _apply_rgb(self): """ Apply rgb processing """ update_params_from_controls(self._rgb_parameters, self._rgb_controls) self._controller.apply_processing() @log @pyqtSlot(name="on_btn_levels_apply_clicked") def _apply_levels(self): """ Apply levels processing """ update_params_from_controls(self._levels_parameters, self._levels_controls) self._controller.apply_processing() @log @pyqtSlot(name="on_btn_stretch_reset_clicked") def _reset_autostretch(self): """ Resets autostretch controls to their defaults """ reset_params(self._autostretch_parameters, self._autostretch_controls) @log @pyqtSlot(name="on_btn_rgb_reset_clicked") def _reset_rgb(self): """ Resets rgb controls to their defaults """ reset_params(self._rgb_parameters, self._rgb_controls) @log @pyqtSlot(name="on_btn_levels_reset_clicked") def _reset_levels(self): """ Resets levels processing controls to their defaults """ reset_params(self._levels_parameters, self._levels_controls) @log @pyqtSlot(name="on_btn_rgb_reload_clicked") def _reload_rgb(self): """ Sets rgb controls to their previously recorded values (last apply) """ update_controls_from_params(self._rgb_parameters, self._rgb_controls) @log @pyqtSlot(name="on_btn_stretch_reload_clicked") def _reload_autostretch(self): """ Sets autostretch controls to their previously recorded values (last apply) """ update_controls_from_params(self._autostretch_parameters, self._autostretch_controls) @log @pyqtSlot(name="on_btn_levels_reload_clicked") def _reload_levels(self): """ Sets levels processing controls to their previously recorded values (last apply) """ update_controls_from_params(self._levels_parameters, self._levels_controls)
[docs] @log def reset_image_view(self): """ Reset image viewer to its initial state """ for item in self._scene.items(): self._scene.removeItem(item) self._image_item = QGraphicsPixmapItem(QPixmap(":/icons/dslr-camera.svg")) self._ui.image_view.setBackgroundBrush(QBrush(QColor("#222222"), Qt.SolidPattern)) self._scene.addItem(self._image_item)
[docs] @log def closeEvent(self, event): """Handles window close events.""" # pylint: disable=C0103 if not self.isFullScreen(): window_rect = self.geometry() config.set_window_geometry((window_rect.x(), window_rect.y(), window_rect.width(), window_rect.height())) config.set_full_screen_active(self.isFullScreen()) MainWindow._save_config() self._stop_session() if DYNAMIC_DATA.session.is_stopped: image_waiter = SaveWaitDialog(self._controller, self) if image_waiter.count_remaining_images() > 0: image_waiter.exec() event.accept() else: event.ignore()
[docs] @pyqtSlot(name="on_pbSave_clicked") @log def cb_save(self): """ Qt slot for mouse clicks on the 'save' button. This saves the processed image using user chosen format """ image_to_save = DYNAMIC_DATA.post_processor_result if image_to_save is not None: self._controller.save_image(image_to_save, config.get_image_save_format(), config.get_work_folder_path(), als.model.data.STACKED_IMAGE_FILE_NAME_BASE, add_timestamp=True)
[docs] @pyqtSlot(name="on_action_quit_triggered") @log def cb_quit(self): """ Qt slot for activation of the 'quit' action""" super().close()
[docs] @pyqtSlot(name="on_action_prefs_triggered") @log def cb_prefs(self): """ Qt slot for activation of the 'preferences' action""" self._open_preferences()
[docs] @pyqtSlot(name="on_action_about_als_triggered") @log def cb_about(self): """ Qt slot for activation of the 'about' action""" dialog = AboutDialog(self) dialog.exec()
# pylint: disable=C0103
[docs] @log def on_cb_stacking_mode_currentTextChanged(self, stacking_mode: str): """ Qt slot executed when stacking mode comb box changed :param stacking_mode: new stacking mode :type stacking_mode: str """ self._controller.set_stacking_mode(stacking_mode)
[docs] @log def on_chk_align_toggled(self, checked: bool): """ Qt slot executed when 'align' check box is changed :param checked: is checkbox checked ? :type checked: bool """ self._controller.set_align_before_stack(checked)
[docs] @log def on_chk_save_every_image_toggled(self, checked: bool): """ Qt slot executed when 'save ever image' check box is changed :param checked: is checkbox checked ? :type checked: bool """ self._controller.set_save_every_image(checked)
[docs] @pyqtSlot() @log def on_btn_web_start_clicked(self): """ Qt slot executed when START web button is clicked """ self._start_www()
[docs] @pyqtSlot() @log def on_btn_web_stop_clicked(self): """ Qt slot executed when START web button is clicked """ self._stop_www()
[docs] @log def on_action_full_screen_toggled(self, checked): """ Qt slot executed when action 'Full screen' is toggled :param checked: is the action active ? :type checked: bool """ if checked: self.showFullScreen() else: self.showNormal()
[docs] @pyqtSlot() @log def on_action_image_only_triggered(self): """ Qt slot executed when 'image only' action is triggered """ actions_restore_mapping = { self._ui.action_show_processing_panel: self._restore_processing_dock, self._ui.action_show_session_controls: self._restore_session_dock, self._ui.action_show_session_log: self._restore_log_dock, } checked = self._ui.action_image_only.isChecked() if checked: self._restore_session_dock = self._ui.session_dock.isVisible() self._restore_log_dock = self._ui.log_dock.isVisible() self._restore_processing_dock = self._ui.processing_dock.isVisible() for action in actions_restore_mapping: if action.isChecked(): action.trigger() else: for action, restore in actions_restore_mapping.items(): if restore: action.trigger()
[docs] @log def on_processing_dock_visibilityChanged(self, visible): """ Qt slot executed when prcessing dock visibility changed :param visible: is it now visible ? :type visible: bool """ if visible: self._cancel_image_only_mode()
[docs] @log def on_log_dock_visibilityChanged(self, visible): """ Qt slot executed when log dock visibility changed :param visible: is it now visible ? :type visible: bool """ if visible: self._cancel_image_only_mode()
[docs] @log def on_session_dock_visibilityChanged(self, visible): """ Qt slot executed when session dock visibility changed :param visible: is it now visible ? :type visible: bool """ if visible: self._cancel_image_only_mode()
@log def _cancel_image_only_mode(self): """ Untick 'image only' menu entry """ self._ui.action_image_only.setChecked(False) @log def _update_image(self): """ Update central image display. """ image_raw_data = DYNAMIC_DATA.post_processor_result.data.copy() image = array2qimage(image_raw_data, normalize=(2 ** 16 - 1)) self._image_item.setPixmap(QPixmap.fromImage(image))
[docs] @pyqtSlot(name="on_pbPlay_clicked") @log def cb_play(self): """Qt slot for mouse clicks on the 'play' button""" self._start_session()
[docs] def on_log_message(self, message): """ print received log message to GUI log window :param message: the log message :type message: str """ self._ui.log.addItem(message) self._ui.log.scrollToBottom()
[docs] @log def update_display(self, image_only: bool = False): """ Updates all displays and controls depending on DataStore held data """ if image_only: self._update_image() self._ui.histogram_view.update() else: web_server_is_running = DYNAMIC_DATA.web_server_is_running session = DYNAMIC_DATA.session session_is_running = session.is_running session_is_stopped = session.is_stopped session_is_paused = session.is_paused # update running statuses scanner_status_message = f"Scanner on {config.get_scan_folder_path()} : " scanner_status_message += f"Running" if session_is_running else "Stopped" self._ui.lbl_scanner_status.setText(scanner_status_message) if web_server_is_running: url = f"http://{DYNAMIC_DATA.web_server_ip}:{config.get_www_server_port_number()}" webserver_status = f'Started, reachable at <a href="{url}" style="color: #CC0000">{url}</a>' else: webserver_status = "Stopped" self._ui.lbl_web_server_status.setText(f"Web server : {webserver_status}") if session_is_stopped: session_status = "Stopped" elif session_is_paused: session_status = "Paused" elif session_is_running: session_status = "Running" else: # this should never happen, that's why we check ;) session_status = "### BUG !" self._ui.lbl_session_status.setText(f"{session_status}") # update preferences accessibility according to session and web server status self._ui.action_prefs.setEnabled(not web_server_is_running and session_is_stopped) # handle Start / Pause / Stop buttons self._ui.pbPlay.setEnabled(session_is_stopped or session_is_paused) self._ui.pbStop.setEnabled(session_is_running or session_is_paused) self._ui.pbPause.setEnabled(session_is_running) # handle align + stack mode buttons self._ui.chk_align.setEnabled(session_is_stopped) self._ui.cb_stacking_mode.setEnabled(session_is_stopped) # handle web stop start buttons self._ui.btn_web_start.setEnabled(not web_server_is_running) self._ui.btn_web_stop.setEnabled(web_server_is_running) # update stack size self._ui.lbl_stack_size.setText(str(DYNAMIC_DATA.stack_size)) # update queues sizes self._ui.lbl_pre_process_queue_size.setText(str(DYNAMIC_DATA.pre_process_queue.qsize())) self._ui.lbl_stack_queue_size.setText(str(DYNAMIC_DATA.stacker_queue.qsize())) self._ui.lbl_process_queue_size.setText(str(DYNAMIC_DATA.process_queue.qsize())) self._ui.lbl_save_queue_size.setText(str(DYNAMIC_DATA.save_queue.qsize())) # handle component statuses self._ui.lbl_pre_processor_status.setText(DYNAMIC_DATA.pre_processor_status) self._ui.lbl_stacker_status.setText(DYNAMIC_DATA.stacker_status) self._ui.lbl_post_processor_status.setText(DYNAMIC_DATA.post_processor_status) self._ui.lbl_saver_status.setText(DYNAMIC_DATA.saver_status)
[docs] @pyqtSlot(name="on_pbStop_clicked") @log def cb_stop(self): """Qt slot for mouse clicks on the 'Stop' button""" self._stop_session()
[docs] @pyqtSlot(name="on_pbPause_clicked") @log def cb_pause(self): """Qt slot for mouse clicks on the 'Pause' button""" self._controller.pause_session()
@log def _start_www(self): """Starts web server""" try: self._controller.start_www() if DYNAMIC_DATA.web_server_ip == "127.0.0.1": title = "Web server access is limited" message = "Web server IP address is 127.0.0.1.\n\nServer won't be reachable by other " \ "machines. Please check your network connection" warning_box(title, message) except WebServerStartFailure as start_failure: error_box(start_failure.message, start_failure.details) @log def _stop_www(self): """Stops web server""" self._controller.stop_www() @log def _start_session(self, is_retry: bool = False): """ Stars session :param is_retry: is this a retry ? :type is_retry: bool """ try: self._controller.start_session() if is_retry: message_box("Session started", "Session successfully started after retry") except CriticalFolderMissing as folder_missing: text = folder_missing.details text += "\n\n Would you like to open the preferences box ?" if question(folder_missing.message, text) and self._open_preferences(): self._start_session(is_retry=True) except SessionError as session_error: error_box(session_error.message, str(session_error.details) + "\n\nSession start aborted") @log def _stop_session(self, ask_confirmation: bool = True): """ Stops sessions :param ask_confirmation: do we ask user for confirmation ? :type ask_confirmation: bool """ if not DYNAMIC_DATA.session.is_stopped: do_stop_session = True if ask_confirmation and DYNAMIC_DATA.stack_size > 0: message = ( "Stopping the current session will reset the stack and all image enhancements.\n\n" "Are you sure you want to stop the current session ?") do_stop_session = question("Really stop session ?", message, default_yes=False) if do_stop_session: self._controller.stop_session() @log def _open_preferences(self): """ Opens preferences dialog box and return True if dilaog was closed using "OK" :return: Was the dilaog closed with "OK" ? :rtype: bool """ accepted = PreferencesDialog(self).exec() == QDialog.Accepted if accepted: self.update_display() return accepted @staticmethod @log def _save_config(): try: config.save() except CouldNotSaveConfig as save_error: error_box(save_error.message, f"Your settings could not be saved\n\nDetails : {save_error.details}")