class SettingsDialog(QDialog, BaseUI): """ Dialog which allows the user to modify their preferences for PyMODA. """ def __init__(self): self.settings = Settings() self.checkbox_default: QCheckBox = None self.line_cache_loc: QLineEdit = None self.btn_browse: QPushButton = None self.btn_open_logs: QPushButton = None super().__init__() def setup_ui(self) -> None: uic.loadUi(resources.get("layout:dialog_settings.ui"), self) self.btn_browse.clicked.connect(self.browse_for_folder) self.btn_open_logs.clicked.connect(self.on_open_logs_clicked) cache_loc = self.settings.get_pymodalib_cache() self.line_cache_loc.setText( cache_loc if cache_loc != "None" else cache_loc) def run(self) -> None: if QDialog.Accepted == self.exec(): self.settings.set_pymodalib_cache(self.get_location()) print("Settings saved.") else: print("Settings not saved.") def browse_for_folder(self) -> None: dialog = QFileDialog() dialog.setFileMode(QFileDialog.DirectoryOnly) if QDialog.Accepted == dialog.exec(): cache_location = dialog.selectedFiles()[0] self.line_cache_loc.setText(cache_location) def get_location(self) -> Optional[str]: text = self.line_cache_loc.text() return text if text != "None" else None @staticmethod def on_open_logs_clicked() -> None: location = file_utils.pymoda_path if OS.is_windows(): os.startfile(location) elif OS.is_linux(): subprocess.Popen(["xdg-open", location]) elif OS.is_mac_os(): subprocess.Popen(["open", location])
class LauncherWindow(CentredWindow): """ The first window that opens, providing buttons to open the important windows. """ def __init__(self, parent): self.settings = Settings() self.lbl_update_status = None self.cleanup_thread = CleanupThread() self.cleanup_thread.start() self.update_thread = UpdateThread() self.update_thread.downloading_signal.connect( self.on_download_finished) self.update_thread.extracting_signal.connect(self.on_extract_finished) self.update_thread.copy_finished_signal.connect(self.on_copy_finished) self.update_thread.error_signal.connect(self.on_update_error) self.update_thread.download_progress_signal.connect( self.on_download_progress) super(LauncherWindow, self).__init__(parent) print( f"Welcome to PyMODA. Please do not close this terminal window while PyMODA is running." f"\n\nPyMODA is running under Python {'.'.join([f'{i}' for i in sys.version_info[:3]])}.\n" f"Python interpreter: {sys.executable}.\n") # Hidden shortcuts to trigger a check for updates. self.update_shortcut = QShortcut(QKeySequence("Ctrl+U"), self) self.update_shortcut.activated.connect(self.force_check_updates) self.update_shortcut_force = QShortcut(QKeySequence("Ctrl+Shift+U"), self) self.update_shortcut_force.activated.connect(self.force_show_update) self.updating = False self.current_update_status = UpdateStatus.NOT_AVAILABLE self.update_status_ui() self.pymoda_has_set_cache_var = False self.reload_settings() import main logging.info(f"PyMODA version == 'v{main.__version__}'") logging.info(f"PyMODAlib version == 'v{pymodalib.__version__}'") logging.info(f"Opened via launcher: {args.launcher()}") self.setWindowTitle(f"PyMODA v{main.__version__}") asyncio.ensure_future(self.check_if_updated()) def setup_ui(self) -> None: uic.loadUi(get("layout:window_launcher.ui"), self) self.load_banner_images() self.btn_time_freq.clicked.connect( self.application.start_time_frequency) self.btn_wavelet_phase.clicked.connect( self.application.start_phase_coherence) self.btn_group_coherence.clicked.connect( self.application.start_group_coherence) self.btn_ridge_extraction.clicked.connect( self.application.start_ridge_extraction) self.btn_wavelet_bispectrum.clicked.connect( self.application.start_bispectrum) self.btn_dynamical_bayesian.clicked.connect( self.application.start_bayesian) self.btn_harmonics.clicked.connect(self.application.start_harmonics) self.btn_create_shortcut.clicked.connect(self.create_shortcut) # TODO: check for Matlab runtime, but show less intrusive message. # self.check_matlab_runtime() self.lbl_updating.setVisible(False) self.progress_updating.setVisible(False) self.lbl_update_complete.setVisible(False) self.btn_settings.clicked.connect(self.on_settings_clicked) self.lbl_update_status = QLabel("") self.statusBar().addPermanentWidget(self.lbl_update_status) if args.create_shortcut(): logging.info("Creating desktop shortcut silently.") self.create_shortcut(silent=True) if args.post_update(): asyncio.ensure_future(self.post_update()) else: asyncio.ensure_future(self.check_for_updates()) def closeEvent(self, e: QtGui.QCloseEvent) -> None: loop = asyncio.get_event_loop() loop.stop() async def check_if_updated(self) -> None: await asyncio.sleep(0.5) import main old_version = self.settings.get_pymoda_version() if not old_version: return self.settings.set_pymoda_version(main.__version__) if old_version != main.__version__ and is_version_newer( main.__version__, old_version): logging.info( f"Showing dialog for update: {old_version} -> {main.__version__}" ) self.settings.set_pymoda_version(main.__version__) msgbox = QMessageBox() msgbox.setWindowTitle("Updated") msgbox.setIcon(QMessageBox.Information) msgbox.setText( f"PyMODA successfully updated from v{old_version} to v{main.__version__}." ) msgbox.exec() def update_status_ui(self) -> None: status = self.current_update_status if status is UpdateStatus.NOT_AVAILABLE: self.lbl_update_status.setText("No updates available.") self.progress_updating.setVisible(False) self.lbl_updating.setVisible(False) self.lbl_update_complete.setVisible(False) elif status is UpdateStatus.DOWNLOADING: self.lbl_update_status.setText("Downloading update...") self.lbl_updating.setVisible(True) self.progress_updating.setVisible(True) elif status is UpdateStatus.FINISHED: self.lbl_update_status.setText( "Update installed, will activate on next launch.") self.lbl_updating.setVisible(False) self.progress_updating.setVisible(False) self.lbl_update_complete.setVisible(True) elif status is UpdateStatus.ERROR: self.lbl_update_status.setText( "Error while updating; see log for details.") self.lbl_updating.setVisible(False) self.progress_updating.setVisible(False) def on_download_progress(self, value: float) -> None: """ Called to update the download progress bar. """ progress: QProgressBar = self.progress_updating progress.setVisible(True) progress.setRange(0, 100) progress.setValue(value) if value > 100: progress.setRange(0, 0) progress.setValue(0) def check_matlab_runtime(self) -> None: """ Checks whether the LD_LIBRARY_PATH for the MATLAB Runtime is correctly passed to the program, and shows a dialog if appropriate. """ from pymodalib.utils.matlab_runtime import get_runtime_status status = get_runtime_status() if (not OS.is_windows() and status is RuntimeStatus.NOT_EXISTS and not args.matlab_runtime() and self.settings.is_runtime_warning_enabled()): dialog = MatlabRuntimeDialog() dialog.exec() self.settings.set_runtime_warning_enabled( not dialog.dont_show_again) if not dialog.dont_show_again: sys.exit(0) def reload_settings(self) -> None: self.settings = Settings() cache_var = "PYMODALIB_CACHE" cache = self.settings.get_pymodalib_cache() logging.info( f"PyMODAlib cache is {os.environ.get(cache_var)} in environment, {cache} in settings." ) if ((not os.environ.get(cache_var) or self.pymoda_has_set_cache_var) and cache and cache != "None"): self.pymoda_has_set_cache_var = True os.environ[cache_var] = cache logging.info(f"Set {cache_var} = {os.environ.get(cache_var)}") def load_banner_images(self) -> None: """ Loads the banner images and displays them at the top of the window. """ image = QPixmap(resources.get("image:physicslogo.png")) self.lbl_physics.setPixmap(image.scaled(600, 300, Qt.KeepAspectRatio)) @staticmethod def create_shortcut(silent: bool = False) -> None: """ Calls `create_shortcut()` and shows a message box with the result. """ status = create_shortcut() if not silent: QMessageBox(text=status).exec() def on_update_source_changed(self, branch: str) -> None: """ Called when the selection in the "Update source" ComboBox is changed. :param branch: the branch name selected as the update source """ branch = branch.lower() self.settings.set_update_source(branch) asyncio.ensure_future(self.check_for_updates(force=True)) def on_settings_clicked(self) -> None: """ Called when the "Settings" button is clicked. """ branch = self.settings.get_update_branch() SettingsDialog().run() self.reload_settings() if branch != self.settings.get_update_branch(): self.on_update_source_changed(self.settings.get_update_branch()) def on_update_clicked(self) -> None: """ Called when the button to update is clicked. """ if utils.is_frozen: import webbrowser webbrowser.open_new_tab( "https://github.com/luphysics/PyMODA/releases/latest") return response = QMessageBox().question( self, "Update", "PyMODA will close and update. Please close other PyMODA windows and " "ensure you have no unsaved work. \n\n" "The current version of PyMODA will be saved in a folder named 'backup'. " "Old backups will be deleted.\n\n" "Continue?", ) if QMessageBox.Yes == response: cwd = os.getcwd() root = Path(cwd).parent upd.start_update(root) async def post_update(self) -> None: """ Called when the program launches after a successful update. """ upd.cleanup() self.settings.set_update_available(False) # Set the latest commit to the current commit on GitHub. self.settings.set_latest_commit(await get_latest_commit()) # Remove the `--post-update` argument to prevent confusion in other parts of the program. sys.argv.remove("--post-update") args.set_post_update(False) # After updating, we want the relaunched window to be obvious. # On Windows, this will make the taskbar icon flash orange. self.activateWindow() await asyncio.sleep(0.2) # Prevent jarring animations. QMessageBox.information(self, "Update", "Update completed.") await self.check_for_updates() def force_check_updates(self) -> None: """ Forces a check for updates. Called when the hidden shortcut is triggered. """ asyncio.ensure_future(self.check_for_updates(force=True)) print("Forcing a check for updates...") def force_show_update(self) -> None: """ Forces PyMODA to show an available update, even if one does not exist. """ self.settings.set_latest_commit("dummy-commit-hash") asyncio.ensure_future(self.check_for_updates(force=True)) print("Double-forcing a check for updates...") async def check_for_updates(self, force=False) -> None: """ Checks for updates, and takes appropriate action such as saving to the settings. :param force: whether to force an update check, even if there was a recent check """ if self.updating: return if (".git" in os.listdir(".") and not force) or ( not self.settings.get_update_in_progress() and not force and time.time() - self.settings.get_last_update_check() < 3600): return available, tag = check.is_update_available() if available: logging.info(f"Found new release: {tag}") self.update_thread.release_tag = tag self.start_background_update() else: logging.info("No updates available.") self.settings.set_last_update_check(time.time()) def show_update_available(self, show: bool) -> None: """ Shows that an update is available by making the button and label visible. """ for item in (self.lbl_update, self.btn_update): item.setStyleSheet("color: blue;") item.setVisible(show) def start_background_update(self) -> None: try: print("Starting background update...") self.update_thread.start() self.updating = True self.settings.set_update_in_progress(True) self.update_status_ui() except: warnings.warn("Cannot start update thread more than once.", RuntimeWarning) def on_download_finished(self) -> None: self.current_update_status = UpdateStatus.DOWNLOADING self.update_status_ui() def on_extract_finished(self) -> None: self.update_status_ui() def on_copy_finished(self) -> None: self.current_update_status = UpdateStatus.FINISHED self.settings.set_update_in_progress(False) self.updating = False self.update_status_ui() def on_update_error(self) -> None: self.updating = False self.settings.set_update_in_progress(False) self.current_update_status = UpdateStatus.ERROR self.update_status_ui()