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()
class LauncherWindow(CentredWindow): """ The first window that opens, providing buttons to open the important windows. """ def __init__(self, parent): self.settings = Settings() super(LauncherWindow, self).__init__(parent) # Hidden shortcut to trigger a check for updates. self.update_shortcut = QShortcut(QKeySequence("Ctrl+U"), self) self.update_shortcut.activated.connect(self.force_check_updates) 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_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_create_shortcut.clicked.connect(self.create_shortcut) self.check_matlab_runtime() self.lbl_update.hide() self.btn_update.hide() self.btn_update.clicked.connect(self.on_update_clicked) if args.post_update(): asyncio.ensure_future(self.post_update()) else: asyncio.ensure_future(self.check_for_updates()) 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. """ if (OS.is_linux() 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 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() -> None: """ Calls `create_shortcut()` and shows a message box with the result. """ status = create_shortcut() QMessageBox(text=status).exec() def on_update_clicked(self) -> None: """ Called when the button to update is clicked. """ 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) await asyncio.sleep(0.2) # Prevent jarring animations. QMessageBox.information(self, "Update", "Update completed.") # After updating, we want the relaunched window to be obvious. # On Windows, this will make the taskbar icon flash orange. self.activateWindow() # Set the latest commit to the current commit on GitHub. self.settings.set_latest_commit(await get_latest_commit()) 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...") 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 there was an update found the last time we checked. if self.settings.get_update_available(): self.show_update_available() print("Update is available.") return # If we should check for updates again now. elif not self.settings.should_check_updates() and not force: return # Retrieve the latest commit from the GitHub API. new_commit = await get_latest_commit() if new_commit is None: return # If it's the first ever check for updates. elif self.settings.get_latest_commit() is None: self.settings.set_latest_commit(new_commit) # If there's an update available. elif new_commit != self.settings.get_latest_commit(): self.settings.set_update_available(True) self.show_update_available() print(f"Found new update. Commit hash: {new_commit}") # No updates available. else: self.settings.set_update_available(False) print("No updates available.") # Set now as the last time at which an update check occurred. self.settings.set_last_update_check(time.time()) def show_update_available(self) -> 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.show()