diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 8694e692f1..b1495432bd 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -10,7 +10,7 @@ import os import logging as log import shutil import tempfile -from typing import Union +from typing import Union, Callable from zipfile import ZipFile from appdirs import user_data_dir @@ -46,23 +46,40 @@ class BootstrapRepos(): """Get version of local Pype.""" return __version__ - def install_live_repos(self) -> Union[str, None]: + def install_live_repos(self, progress_callback=None) -> Union[str, None]: """Copy zip created from local repositories to user data dir. + Args: + progress_callback (callable): Optional callback method to report + progress. Returns: str: path of installed repository file. """ + # dummy progress reporter + def empty_progress(x: int): + return x + + if not progress_callback: + progress_callback = empty_progress + # create zip from repositories local_version = self.get_local_version() repo_dir = self.live_repo_dir + + # create destination directory + try: + os.makedirs(self.data_dir) + except OSError: + self._log.error("directory already exists") with tempfile.TemporaryDirectory() as temp_dir: temp_zip = os.path.join( - str(temp_dir), + temp_dir, f"pype-repositories-v{local_version}.zip" ) self._log.info(f"creating zip: {temp_zip}") # shutil.make_archive(temp_zip, "zip", repo_dir) - self._create_pype_zip(temp_zip, repo_dir) + self._create_pype_zip( + temp_zip, repo_dir, progress_callback=progress_callback) if not os.path.exists(temp_zip): self._log.error("make archive failed.") return None @@ -70,7 +87,8 @@ class BootstrapRepos(): return os.path.join(self.data_dir, os.path.basename(temp_zip)) def _create_pype_zip( - self, zip_path: str, dir: str, include_pype=True) -> None: + self, zip_path: str, dir: str, + progress_callback: Callable, include_pype: bool = True) -> None: """Pack repositories and Pype into zip. We are using `zipfile` instead :meth:`shutil.make_archive()` to later @@ -83,10 +101,20 @@ class BootstrapRepos(): Args: zip_path (str): path to zip file. dir: repo directories to inlcude. + progress_callback (Callable): callback to report progress back to + UI progress bar. include_pype (bool): add Pype module itelf. """ + repo_files = sum(len(files) for _, _, files in os.walk(dir)) + if include_pype: + pype_files = sum(len(files) for _, _, files in os.walk('pype')) + repo_inc = 48.0 / float(repo_files) + pype_inc = 48.0 / float(pype_files) + else: + repo_inc = 98.0 / float(repo_files) + progress = 0 with ZipFile(zip_path, "w") as zip: - for root, dirs, files in os.walk(dir): + for root, _, files in os.walk(dir): for file in files: zip.write( os.path.relpath(os.path.join(root, file), @@ -94,9 +122,11 @@ class BootstrapRepos(): os.path.relpath(os.path.join(root, file), os.path.join(dir)) ) + progress += repo_inc + progress_callback(int(progress)) # add pype itself if include_pype: - for root, dirs, files in os.walk("pype"): + for root, _, files in os.walk("pype"): for file in files: zip.write( os.path.relpath(os.path.join(root, file), @@ -106,7 +136,10 @@ class BootstrapRepos(): os.path.relpath(os.path.join(root, file), os.path.join('pype', '..'))) ) + progress += pype_inc + progress_callback(int(progress)) zip.testzip() + progress_callback(100) def add_paths_from_archive(self, archive: str) -> None: """Add first-level directories as paths to sys.path. diff --git a/igniter/install_dialog.py b/igniter/install_dialog.py index 5863ed4209..8327aaa8fc 100644 --- a/igniter/install_dialog.py +++ b/igniter/install_dialog.py @@ -11,6 +11,7 @@ class InstallDialog(QtWidgets.QDialog): _size_w = 400 _size_h = 300 _path = None + _controls_disabled = False def __init__(self, parent=None): super(InstallDialog, self).__init__(parent) @@ -30,6 +31,18 @@ class InstallDialog(QtWidgets.QDialog): self.setMaximumSize( QtCore.QSize(self._size_w + 100, self._size_h + 100)) + # style for normal console text + self.default_console_style = QtGui.QTextCharFormat() + self.default_console_style.setFontPointSize(0.1) + self.default_console_style.setForeground( + QtGui.QColor.fromRgb(72, 200, 150)) + + # style for error console text + self.error_console_style = QtGui.QTextCharFormat() + self.error_console_style.setFontPointSize(0.1) + self.error_console_style.setForeground( + QtGui.QColor.fromRgb(184, 54, 19)) + self._init_ui() def _init_ui(self): @@ -96,7 +109,6 @@ class InstallDialog(QtWidgets.QDialog): # Bottom button bar # -------------------------------------------------------------------- - bottom_widget = QtWidgets.QWidget() bottom_layout = QtWidgets.QHBoxLayout() pype_logo_label = QtWidgets.QLabel("pype logo") @@ -138,18 +150,22 @@ class InstallDialog(QtWidgets.QDialog): # Status label # -------------------------------------------------------------------- - self._status_label = QtWidgets.QLabel() + self._status_label = QtWidgets.QLabel("Console:") self._status_label.setContentsMargins(0, 10, 0, 10) self._status_label.setStyleSheet("color: rgb(61, 115, 97);") + # Console + # -------------------------------------------------------------------- self._status_box = QtWidgets.QPlainTextEdit() self._status_box.setReadOnly(True) + self._status_box.setCurrentCharFormat(self.default_console_style) self._status_box.setStyleSheet( """QPlainTextEdit { background-color: rgb(32, 32, 32); color: rgb(72, 200, 150); font-family: Courier; - font-size: .3em;} + font-size: 3pt; + } QScrollBar:vertical { border: 1px solid rgb(61, 115, 97); background: #000; @@ -181,12 +197,33 @@ class InstallDialog(QtWidgets.QDialog): """ ) + # Progress bar + # -------------------------------------------------------------------- + self._progress_bar = QtWidgets.QProgressBar() + self._progress_bar.setValue(0) + self._progress_bar.setAlignment(QtCore.Qt.AlignCenter) + self._progress_bar.setTextVisible(False) + # setting font and the size + self._progress_bar.setFont(QtGui.QFont('Arial', 7)) + self._progress_bar.setStyleSheet( + """QProgressBar:horizontal { + height: 5px; + border: 1px solid rgb(31, 62, 50); + color: rgb(72, 200, 150); + } + QProgressBar::chunk:horizontal { + background-color: rgb(72, 200, 150); + } + """ + ) # add all to main main.addWidget(self.main_label) main.addWidget(self.pype_path_label) main.addLayout(input_layout) main.addStretch(1) + main.addWidget(self._status_label) main.addWidget(self._status_box) + main.addWidget(self._progress_bar) main.addWidget(bottom_widget) self.setLayout(main) @@ -204,12 +241,13 @@ class InstallDialog(QtWidgets.QDialog): self._disable_buttons() self._install_thread = InstallThread(self) self._install_thread.message.connect(self._update_console) + self._install_thread.progress.connect(self._update_progress) self._install_thread.finished.connect(self._enable_buttons) self._install_thread.set_path(self._path) self._install_thread.start() - def _update_console(self, msg): - self._status_box.appendPlainText(msg) + def _update_progress(self, progress: int): + self._progress_bar.setValue(progress) def _on_exit_clicked(self): self.close() @@ -218,7 +256,7 @@ class InstallDialog(QtWidgets.QDialog): self._path = path self._status_label.setText(f"selected {path}") - def _update_status(self, msg: str, error: bool = False) -> None: + def _update_console(self, msg: str, error: bool = False) -> None: """Display message. Args: @@ -226,22 +264,22 @@ class InstallDialog(QtWidgets.QDialog): error (bool): if True, print it red. """ if not error: - self._status_label.setStyleSheet("color: rgb(72, 200, 150);") + self._status_box.setCurrentCharFormat(self.default_console_style) else: - self._status_label.setStyleSheet("color: rgb(189, 54, 32);") - self._status_label.setText(msg) + self._status_box.setCurrentCharFormat(self.error_console_style) + self._status_box.appendPlainText(msg) def _disable_buttons(self): self._btn_select.setEnabled(False) self._exit_button.setEnabled(False) self._ok_button.setEnabled(False) - self._controls_disabled(True) + self._controls_disabled = True def _enable_buttons(self): self._btn_select.setEnabled(True) self._exit_button.setEnabled(True) self._ok_button.setEnabled(True) - self._controls_disabled(False) + self._controls_disabled = False def closeEvent(self, event): if self._controls_disabled: diff --git a/igniter/install_thread.py b/igniter/install_thread.py index fc1865001c..0e1d7be6b4 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -8,14 +8,15 @@ from .bootstrap_repos import BootstrapRepos class InstallThread(QThread): - message = Signal(str) + progress = Signal(int) + message = Signal((str, bool)) _path = None def __init__(self, parent=None): QThread.__init__(self, parent) def run(self): - self.message.emit("Installing Pype ...") + self.message.emit("Installing Pype ...", False) bs = BootstrapRepos() local_version = bs.get_local_version() @@ -28,11 +29,17 @@ class InstallThread(QThread): # version to it. if not self._path: self.message.emit( - f"We will use local Pype version {local_version}") - repo_file = bs.install_live_repos() + f"We will use local Pype version {local_version}", False) + repo_file = bs.install_live_repos( + progress_callback=self.set_progress) if not repo_file: - self.message.emit(f"!!! install failed - {repo_file}") - self.message.emit(f"installed as {repo_file}") + self.message.emit( + f"!!! install failed - {repo_file}", True) + return + self.message.emit(f"installed as {repo_file}", False) def set_path(self, path: str) -> None: self._path = path + + def set_progress(self, progress: int): + self.progress.emit(progress)