Esempio n. 1
0
 def _show_license(self):
     w = LicenseWidget()
     d = GreyedDialog(w,
                      title=_("TEXT_LICENSE_TITLE"),
                      parent=self,
                      width=1000)
     d.open()
Esempio n. 2
0
 def _show_settings(self):
     w = SettingsWidget(self.config, self.jobs_ctx, self.event_bus)
     d = GreyedDialog(w,
                      title=_("TEXT_SETTINGS_TITLE"),
                      parent=self,
                      width=1000)
     d.open()
Esempio n. 3
0
 def show_modal(
     cls,
     jobs_ctx,
     workspace_fs,
     path,
     reload_timestamped_signal,
     update_version_list,
     close_version_list,
     client,
     parent,
     on_finished,
 ):
     w = cls(
         jobs_ctx=jobs_ctx,
         workspace_fs=workspace_fs,
         path=path,
         reload_timestamped_signal=reload_timestamped_signal,
         update_version_list=update_version_list,
         close_version_list=close_version_list,
         client=client,
     )
     d = GreyedDialog(
         w,
         title=_("TEXT_FILE_HISTORY_TITLE_name").format(name=path.name),
         parent=parent)
     w.dialog = d
     if on_finished:
         d.finished.connect(on_finished)
     # Unlike exec_, show is asynchronous and works within the main Qt loop
     d.show()
     return w
Esempio n. 4
0
 def show_modal(cls, jobs_ctx, parent, on_finished):
     w = cls(jobs_ctx)
     d = GreyedDialog(w, _("TEXT_ORG_WIZARD_TITLE"), parent=parent, width=1000)
     d.closing.connect(w.on_close)
     w.accepted.connect(d.accept)
     w.finished.connect(on_finished)
     # Unlike exec_, show is asynchronous and works within the main Qt loop
     d.show()
Esempio n. 5
0
 def show_modal(cls, client, parent):
     w = cls(client=client)
     d = GreyedDialog(w,
                      title=_("TEXT_CHANGE_PASSWORD_TITLE"),
                      parent=parent)
     w.accepted.connect(d.accept)
     # Unlike exec_, show is asynchronous and works within the main Qt loop
     d.show()
    def show_modal(cls, user_fs, workspace_fs, client, jobs_ctx, parent, on_finished):
        w = cls(user_fs=user_fs, workspace_fs=workspace_fs, client=client, jobs_ctx=jobs_ctx)
        d = GreyedDialog(w, title=_("TEXT_WORKSPACE_SHARING_TITLE"), parent=parent, width=1000)

        d.closing.connect(w.on_close)
        w.closing.connect(on_finished)
        # Unlike exec_, show is asynchronous and works within the main Qt loop
        d.show()
        return w
Esempio n. 7
0
 def show_modal(cls, jobs_ctx, config, addr, parent, on_finished):
     w = cls(jobs_ctx=jobs_ctx, config=config, addr=addr)
     d = GreyedDialog(w,
                      _("TEXT_CLAIM_DEVICE_TITLE"),
                      parent=parent,
                      width=800)
     d.closing.connect(w.on_close)
     w.finished.connect(on_finished)
     w.accepted.connect(d.accept)
     w.rejected.connect(d.reject)
     # Unlike exec_, show is asynchronous and works within the main Qt loop
     d.show()
Esempio n. 8
0
 def show_modal(cls, client, jobs_ctx, token, parent, on_finished):
     w = cls(client=client, jobs_ctx=jobs_ctx, token=token)
     d = GreyedDialog(w,
                      _("TEXT_GREET_USER_TITLE"),
                      parent=parent,
                      width=950)
     d.closing.connect(w.on_close)
     w.finished.connect(on_finished)
     w.accepted.connect(d.accept)
     w.rejected.connect(d.reject)
     # Unlike exec_, show is asynchronous and works within the main Qt loop
     d.show()
     return w
Esempio n. 9
0
    def show_modal(cls, workspace_fs, jobs_ctx, parent, on_finished):
        w = cls(workspace_fs=workspace_fs, jobs_ctx=jobs_ctx)
        d = GreyedDialog(center_widget=w,
                         title=_("TEXT_WORKSPACE_TIMESTAMPED_TITLE"),
                         parent=parent,
                         width=600)
        w.dialog = d

        def _on_finished(result):
            if result == QDialog.Rejected:
                return on_finished(None, None)
            return on_finished(w.date, w.time)

        d.finished.connect(_on_finished)
        # Unlike exec_, show is asynchronous and works within the main Qt loop
        d.show()
        return w
Esempio n. 10
0
    def show_modal(cls, jobs_ctx, config, addr, parent, on_finished):
        w = cls(jobs_ctx=jobs_ctx, config=config, addr=addr)
        d = GreyedDialog(w,
                         _("TEXT_BOOTSTRAP_ORG_TITLE"),
                         parent=parent,
                         width=1000)
        w.dialog = d
        w.line_edit_login.setFocus()

        def _on_finished(result):
            if result == QDialog.Accepted:
                return on_finished(w.status)
            return on_finished(None)

        d.finished.connect(_on_finished)
        # Unlike exec_, show is asynchronous and works within the main Qt loop
        d.show()
        return w
Esempio n. 11
0
    def import_all(self, files, total_size):
        assert not self.import_job

        wl = LoadingWidget(total_size=total_size + len(files))
        self.loading_dialog = GreyedDialog(wl,
                                           _("TEXT_FILE_IMPORT_LOADING_TITLE"),
                                           parent=self)
        wl.cancelled.connect(self.cancel_import)
        self.loading_dialog.show()

        self.import_job = self.jobs_ctx.submit_job(
            ThreadSafeQtSignal(self, "import_success", QtToTrioJob),
            ThreadSafeQtSignal(self, "import_error", QtToTrioJob),
            _do_import,
            workspace_fs=self.workspace_fs,
            files=files,
            total_size=total_size,
            progress_signal=ThreadSafeQtSignal(self, "import_progress", str,
                                               int),
        )
Esempio n. 12
0
 def _show_about(self):
     w = AboutWidget()
     d = GreyedDialog(w, title="", parent=self, width=1000)
     d.open()
Esempio n. 13
0
class FilesWidget(QWidget, Ui_FilesWidget):
    fs_updated_qt = pyqtSignal(ClientEvent, UUID)
    fs_synced_qt = pyqtSignal(ClientEvent, UUID)
    entry_downsynced_qt = pyqtSignal(UUID, UUID)
    global_clipboard_updated_qt = pyqtSignal(object)

    sharing_updated_qt = pyqtSignal(WorkspaceEntry, object)
    back_clicked = pyqtSignal()

    rename_success = pyqtSignal(QtToTrioJob)
    rename_error = pyqtSignal(QtToTrioJob)
    delete_success = pyqtSignal(QtToTrioJob)
    delete_error = pyqtSignal(QtToTrioJob)
    folder_stat_success = pyqtSignal(QtToTrioJob)
    folder_stat_error = pyqtSignal(QtToTrioJob)
    folder_create_success = pyqtSignal(QtToTrioJob)
    folder_create_error = pyqtSignal(QtToTrioJob)
    import_success = pyqtSignal(QtToTrioJob)
    import_error = pyqtSignal(QtToTrioJob)

    import_progress = pyqtSignal(str, int)

    reload_timestamped_requested = pyqtSignal(DateTime, FsPath, FileType, bool,
                                              bool, bool)
    reload_timestamped_success = pyqtSignal(QtToTrioJob)
    reload_timestamped_error = pyqtSignal(QtToTrioJob)
    update_version_list = pyqtSignal(WorkspaceFS, FsPath)
    close_version_list = pyqtSignal()

    folder_changed = pyqtSignal(str, str)

    def __init__(self, client, jobs_ctx, event_bus, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)
        self.spinner = CenteredSpinnerWidget(parent=self.table_files)
        self.table_files_layout.addWidget(self.spinner, 0, 0)
        self.spinner.hide()
        self.client = client
        self.jobs_ctx = jobs_ctx
        self.event_bus = event_bus
        self.workspace_fs = None
        self.import_job = None
        self.clipboard = None

        self.button_back.clicked.connect(self.back_clicked)
        self.button_back.apply_style()
        self.button_import_folder.clicked.connect(self.import_folder_clicked)
        self.button_import_folder.apply_style()
        self.button_import_files.clicked.connect(self.import_files_clicked)
        self.button_import_files.apply_style()
        self.button_create_folder.clicked.connect(self.create_folder_clicked)
        self.button_create_folder.apply_style()
        self.line_edit_search.textChanged.connect(self.filter_files)
        self.line_edit_search.hide()
        self.current_directory = FsPath("/")
        self.current_directory_uuid = None
        self.fs_updated_qt.connect(self._on_fs_updated_qt)
        self.fs_synced_qt.connect(self._on_fs_synced_qt)
        self.entry_downsynced_qt.connect(self._on_entry_downsynced_qt)
        self.update_timer = QTimer()
        self.update_timer.setInterval(1000)
        self.update_timer.setSingleShot(True)
        self.update_timer.timeout.connect(self.reload)
        self.default_import_path = str(pathlib.Path.home())
        self.table_files.config = self.client.config
        self.table_files.file_moved.connect(self.on_file_moved)
        self.table_files.item_activated.connect(self.item_activated)
        self.table_files.rename_clicked.connect(self.rename_files)
        self.table_files.delete_clicked.connect(self.delete_files)
        self.table_files.open_clicked.connect(self.open_files)
        self.table_files.files_dropped.connect(self.on_files_dropped)
        self.table_files.show_history_clicked.connect(self.show_history)
        self.table_files.paste_clicked.connect(self.on_paste_clicked)
        self.table_files.copy_clicked.connect(self.on_copy_clicked)
        self.table_files.cut_clicked.connect(self.on_cut_clicked)
        self.table_files.file_path_clicked.connect(
            self.on_get_file_path_clicked)
        self.table_files.open_current_dir_clicked.connect(
            self.on_open_current_dir_clicked)

        self.sharing_updated_qt.connect(self._on_sharing_updated_qt)
        self.rename_success.connect(self._on_rename_success)
        self.rename_error.connect(self._on_rename_error)
        self.delete_success.connect(self._on_delete_success)
        self.delete_error.connect(self._on_delete_error)
        self.folder_stat_success.connect(self._on_folder_stat_success)
        self.folder_stat_error.connect(self._on_folder_stat_error)
        self.folder_create_success.connect(self._on_folder_create_success)
        self.folder_create_error.connect(self._on_folder_create_error)
        self.import_success.connect(self._on_import_success)
        self.import_error.connect(self._on_import_error)

        self.reload_timestamped_requested.connect(
            self._on_reload_timestamped_requested)
        self.reload_timestamped_success.connect(
            self._on_reload_timestamped_success)
        self.reload_timestamped_error.connect(
            self._on_reload_timestamped_error)

        self.loading_dialog = None
        self.import_progress.connect(self._on_import_progress)

        self.event_bus.connect(ClientEvent.FS_ENTRY_UPDATED,
                               self._on_fs_entry_updated_trio)
        self.event_bus.connect(ClientEvent.FS_ENTRY_SYNCED,
                               self._on_fs_entry_synced_trio)
        self.event_bus.connect(ClientEvent.SHARING_UPDATED,
                               self._on_sharing_updated_trio)
        self.event_bus.connect(ClientEvent.FS_ENTRY_DOWNSYNCED,
                               self._on_entry_downsynced_trio)

    def disconnect_all(self):
        pass

    def set_workspace_fs(self,
                         wk_fs,
                         current_directory=FsPath("/"),
                         default_selection=None,
                         clipboard=None):
        self.current_directory = current_directory
        self.workspace_fs = wk_fs
        ws_entry = self.jobs_ctx.run_sync(
            self.workspace_fs.get_workspace_entry)
        self.current_user_role = ws_entry.role
        self.label_role.setText(get_role_translation(self.current_user_role))
        self.table_files.current_user_role = self.current_user_role
        if self.current_user_role == WorkspaceRole.READER:
            self.button_import_folder.hide()
            self.button_import_files.hide()
            self.button_create_folder.hide()
        else:
            self.button_import_folder.show()
            self.button_import_files.show()
            self.button_create_folder.show()
        self.clipboard = clipboard
        if not self.clipboard:
            self.table_files.paste_status = PasteStatus(
                status=PasteStatus.Status.Disabled)
        else:
            if self.clipboard.source_workspace == self.workspace_fs:
                self.table_files.paste_status = PasteStatus(
                    status=PasteStatus.Status.Enabled)
            else:
                # Sending the source_workspace name for paste text
                self.table_files.paste_status = PasteStatus(
                    status=PasteStatus.Status.Enabled,
                    source_workspace=str(
                        self.jobs_ctx.run_sync(self.clipboard.source_workspace.
                                               get_workspace_name)),
                )
        self.reset(default_selection)

    def reset(self, default_selection=None):
        workspace_name = self.jobs_ctx.run_sync(
            self.workspace_fs.get_workspace_name)
        self.load(self.current_directory, default_selection)
        self.table_files.sortItems(0)
        self.folder_changed.emit(str(workspace_name),
                                 str(self.current_directory))

    def on_get_file_path_clicked(self):
        files = self.table_files.selected_files()
        if len(files) != 1:
            return
        url = BackendOrganizationFileLinkAddr.build(
            self.client.device.organization_addr,
            self.workspace_fs.workspace_id,
            self.current_directory / files[0].name,
        )
        desktop.copy_to_clipboard(str(url))
        show_info(self, _("TEXT_FILE_LINK_COPIED_TO_CLIPBOARD"))

    def on_copy_clicked(self):
        files = self.table_files.selected_files()
        files_to_copy = []
        for f in files:
            if f.type != FileType.Folder and f.type != FileType.File:
                continue
            files_to_copy.append((self.current_directory / f.name, f.type))
        self.clipboard = Clipboard(files=files_to_copy,
                                   status=Clipboard.Status.Copied,
                                   source_workspace=self.workspace_fs)
        self.global_clipboard_updated_qt.emit(self.clipboard)
        self.table_files.paste_status = PasteStatus(
            status=PasteStatus.Status.Enabled)

    def on_cut_clicked(self):
        files = self.table_files.selected_files()
        files_to_cut = []
        rows = []
        for f in files:
            if f.type != FileType.Folder and f.type != FileType.File:
                continue
            rows.append(f.row)
            files_to_cut.append((self.current_directory / f.name, f.type))
        self.table_files.set_rows_cut(rows)
        self.clipboard = Clipboard(files=files_to_cut,
                                   status=Clipboard.Status.Cut,
                                   source_workspace=self.workspace_fs)
        self.global_clipboard_updated_qt.emit(self.clipboard)
        self.table_files.paste_status = PasteStatus(
            status=PasteStatus.Status.Enabled)

    def on_paste_clicked(self):
        if not self.clipboard:
            return
        error_count = 0
        last_exc = None
        for f in self.clipboard.files:
            src = f[0]
            src_type = f[1]
            file_name = src.name
            base_name = pathlib.Path(src.name)
            # In order to be able to rename the file if a file of the same name already exists
            # we need the name without extensions.
            # .stem only removes the first extension, so we loop over it.
            while str(base_name) != base_name.stem:
                base_name = pathlib.Path(base_name.stem)
            count = 2
            base_name = str(base_name)
            while True:
                try:
                    dst = self.current_directory / file_name
                    if self.clipboard.status == Clipboard.Status.Cut:
                        self.jobs_ctx.run(self.workspace_fs.move, src, dst,
                                          self.clipboard.source_workspace)
                    else:
                        if src_type == FileType.Folder:
                            self.jobs_ctx.run(
                                self.workspace_fs.copytree,
                                src,
                                dst,
                                self.clipboard.source_workspace,
                            )
                        else:
                            self.jobs_ctx.run(
                                self.workspace_fs.copyfile,
                                src,
                                dst,
                                self.clipboard.source_workspace,
                            )
                    break
                except FileExistsError:
                    # File already exists, we append a counter at the end of its name
                    file_name = "{} ({}){}".format(
                        base_name, count,
                        "".join(pathlib.Path(src.name).suffixes))
                    count += 1
                except FSInvalidArgumentError as exc:
                    # Move a file onto itself
                    # Not a big deal for files, we just do nothing and pretend we
                    # actually did something
                    # For folders we have to warn the user
                    if src_type == FileType.Folder:
                        error_count += 1
                        last_exc = exc
                    break
                except Exception as exc:
                    # No idea what happened, we'll just warn the user that we encountered an
                    # unexcepted error and log it
                    error_count += 1
                    last_exc = exc
                    logger.exception("Unhandled error while cut/copy file",
                                     exc_info=exc)
                    break
        if self.clipboard.status == Clipboard.Status.Cut:
            self.clipboard = None
            self.global_clipboard_updated_qt.emit(None)
            self.table_files.paste_status = PasteStatus(
                status=PasteStatus.Status.Disabled)

        if last_exc:
            # Multiple errors, we'll just display a generic error message
            if error_count > 1:
                show_error(self, _("TEXT_FILE_PASTE_ERROR"))
            else:
                # Folder moved into itself
                if type(last_exc) == FSInvalidArgumentError:
                    show_error(self,
                               _("TEXT_FILE_FOLDER_MOVED_INTO_ITSELF_ERROR"))
                else:
                    show_error(self, _("TEXT_FILE_PASTE_ERROR"))

        self.reset()

    def show_history(self):
        files = self.table_files.selected_files()
        if len(files) == 0:
            return
        if len(files) > 1:
            show_error(self,
                       _("TEXT_FILE_HISTORY_MULTIPLE_FILES_SELECTED_ERROR"))
            return
        selected_path = self.current_directory / files[0].name
        FileHistoryWidget.show_modal(
            jobs_ctx=self.jobs_ctx,
            workspace_fs=self.workspace_fs,
            path=selected_path,
            reload_timestamped_signal=self.reload_timestamped_requested,
            update_version_list=self.update_version_list,
            close_version_list=self.close_version_list,
            client=self.client,
            parent=self,
            on_finished=None,
        )

    def rename_files(self):
        files = self.table_files.selected_files()
        if len(files) == 1:

            def _on_rename_one_file_finished(return_code, new_name):
                if return_code and new_name:
                    self.jobs_ctx.submit_job(
                        ThreadSafeQtSignal(self, "rename_success",
                                           QtToTrioJob),
                        ThreadSafeQtSignal(self, "rename_error", QtToTrioJob),
                        _do_rename,
                        workspace_fs=self.workspace_fs,
                        paths=[(
                            self.current_directory / files[0].name,
                            self.current_directory / new_name,
                            files[0].uuid,
                        )],
                    )

            get_text_input(
                self,
                _("TEXT_FILE_RENAME_TITLE"),
                _("TEXT_FILE_RENAME_INSTRUCTIONS"),
                placeholder=_("TEXT_FILE_RENAME_PLACEHOLDER"),
                default_text=files[0].name,
                button_text=_("ACTION_FILE_RENAME"),
                on_finished=_on_rename_one_file_finished,
            )
        else:

            def _on_rename_multiple_files_finished(return_code, new_name):
                if return_code and new_name:
                    self.jobs_ctx.submit_job(
                        ThreadSafeQtSignal(self, "rename_success",
                                           QtToTrioJob),
                        ThreadSafeQtSignal(self, "rename_error", QtToTrioJob),
                        _do_rename,
                        workspace_fs=self.workspace_fs,
                        paths=[(
                            self.current_directory / f.name,
                            self.current_directory / "{}_{}{}".format(
                                new_name, i, ".".join(
                                    pathlib.Path(f.name).suffixes)),
                            f.uuid,
                        ) for i, f in enumerate(files, 1)],
                    )

            get_text_input(
                self,
                _("TEXT_FILE_RENAME_MULTIPLE_TITLE_count").format(
                    count=len(files)),
                _("TEXT_FILE_RENAME_MULTIPLE_INSTRUCTIONS_count").format(
                    count=len(files)),
                placeholder=_("TEXT_FILE_RENAME_MULTIPLE_PLACEHOLDER"),
                button_text=_("ACTION_FILE_RENAME_MULTIPLE"),
                on_finished=_on_rename_multiple_files_finished,
            )

    def delete_files(self):
        files = self.table_files.selected_files()

        def _on_delete_file_question_finished(return_code, answer):
            if return_code and (answer == _("ACTION_FILE_DELETE")
                                or answer == _("ACTION_FILE_DELETE_MULTIPLE")):
                self.jobs_ctx.submit_job(
                    ThreadSafeQtSignal(self, "delete_success", QtToTrioJob),
                    ThreadSafeQtSignal(self, "delete_error", QtToTrioJob),
                    _do_delete,
                    workspace_fs=self.workspace_fs,
                    files=[(self.current_directory / f.name, f.type)
                           for f in files],
                )

        if len(files) == 1:
            ask_question(
                self,
                _("TEXT_FILE_DELETE_TITLE"),
                _("TEXT_FILE_DELETE_INSTRUCTIONS_name").format(
                    name=files[0].name),
                [_("ACTION_FILE_DELETE"),
                 _("ACTION_CANCEL")],
                on_finished=_on_delete_file_question_finished,
            )
        else:
            ask_question(
                self,
                _("TEXT_FILE_DELETE_MULTIPLE_TITLE_count").format(
                    count=len(files)),
                _("TEXT_FILE_DELETE_MULTIPLE_INSTRUCTIONS_count").format(
                    count=len(files)),
                [_("ACTION_FILE_DELETE_MULTIPLE"),
                 _("ACTION_CANCEL")],
                on_finished=_on_delete_file_question_finished,
            )

    def on_open_current_dir_clicked(self):
        self.open_file(None)

    def open_files(self):
        files = self.table_files.selected_files()
        if len(files) == 1:
            if not self.open_file(files[0][2]):
                show_error(
                    self,
                    _("TEXT_FILE_OPEN_ERROR_file").format(file=files[0][2]))
        else:

            def _on_open_file_question_finished(return_code, answer):
                if return_code and answer == _("ACTION_FILE_OPEN_MULTIPLE"):
                    success = True
                    for f in files:
                        success &= self.open_file(f[2])
                    if not success:
                        show_error(self, _("TEXT_FILE_OPEN_MULTIPLE_ERROR"))

            ask_question(
                self,
                _("TEXT_FILE_OPEN_MULTIPLE_TITLE_count").format(
                    count=len(files)),
                _("TEXT_FILE_OPEN_MULTIPLE_INSTRUCTIONS_count").format(
                    count=len(files)),
                [_("ACTION_FILE_OPEN_MULTIPLE"),
                 _("ACTION_CANCEL")],
                on_finished=_on_open_file_question_finished,
            )

    def open_file(self, file_name):
        # The Qt thread should never hit the client directly.
        # Synchronous calls can run directly in the job system
        # as they won't block the Qt loop for long
        path = self.jobs_ctx.run_sync(
            self.client.mountpoint_manager.get_path_in_mountpoint,
            self.workspace_fs.workspace_id,
            self.current_directory /
            file_name if file_name else self.current_directory,
            self.workspace_fs.timestamp
            if isinstance(self.workspace_fs, WorkspaceFSTimestamped) else None,
        )
        return desktop.open_file(str(path))

    def item_activated(self, file_type, file_name):
        if file_type == FileType.ParentFolder:
            self.load(self.current_directory.parent)
        elif file_type == FileType.ParentWorkspace:
            self.back_clicked.emit()
        elif file_type == FileType.File:
            if not self.open_file(file_name):
                show_error(
                    self,
                    _("TEXT_FILE_OPEN_ERROR_file").format(file=file_name))
        elif file_type == FileType.Folder:
            self.load(self.current_directory / file_name)

    def reload(self):
        self.load(self.current_directory)

    def load(self, directory, default_selection=None):
        self.table_files.clear()
        self.table_files.setStyleSheet("background-color: #EFEFEF;")
        self.spinner.spinner_movie.start()
        self.spinner.show()
        self.jobs_ctx.submit_job(
            ThreadSafeQtSignal(self, "folder_stat_success", QtToTrioJob),
            ThreadSafeQtSignal(self, "folder_stat_error", QtToTrioJob),
            _do_folder_stat,
            workspace_fs=self.workspace_fs,
            path=directory,
            default_selection=default_selection,
        )

    def import_all(self, files, total_size):
        assert not self.import_job

        wl = LoadingWidget(total_size=total_size + len(files))
        self.loading_dialog = GreyedDialog(wl,
                                           _("TEXT_FILE_IMPORT_LOADING_TITLE"),
                                           parent=self)
        wl.cancelled.connect(self.cancel_import)
        self.loading_dialog.show()

        self.import_job = self.jobs_ctx.submit_job(
            ThreadSafeQtSignal(self, "import_success", QtToTrioJob),
            ThreadSafeQtSignal(self, "import_error", QtToTrioJob),
            _do_import,
            workspace_fs=self.workspace_fs,
            files=files,
            total_size=total_size,
            progress_signal=ThreadSafeQtSignal(self, "import_progress", str,
                                               int),
        )

    def cancel_import(self):
        assert self.import_job
        assert self.loading_dialog

        self.import_job.cancel_and_join()

    def _on_import_progress(self, file_name, progress):
        if not self.loading_dialog:
            return
        self.loading_dialog.center_widget.set_progress(progress)
        self.loading_dialog.center_widget.set_current_file(file_name)

    def get_files(self, paths, dst_dir=None):
        files = []
        total_size = 0
        for path in paths:
            p = pathlib.Path(path)
            if dst_dir is not None:
                dst = dst_dir / p.name
            else:
                dst = self.current_directory / p.name
            files.append((p, dst))
            total_size += p.stat().st_size
        return files, total_size

    def get_folder(self, src, dst_dir=None):
        files = []
        total_size = 0
        if dst_dir is None:
            dst = self.current_directory / src.name
        else:
            dst = dst_dir / src.name
        for f in src.iterdir():
            if f.is_dir():
                new_files, new_size = self.get_folder(f, dst_dir=dst)
                files.extend(new_files)
                total_size += new_size
            elif f.is_file():
                new_dst = dst / f.name
                files.append((f, new_dst))
                total_size += f.stat().st_size
        return files, total_size

    def import_files_clicked(self):
        paths, x = QFileDialog.getOpenFileNames(self,
                                                _("TEXT_FILE_IMPORT_FILES"),
                                                self.default_import_path)
        if not paths:
            return
        files, total_size = self.get_files(paths)
        f = files[0][0]
        self.default_import_path = str(f.parent)
        self.import_all(files, total_size)

    def import_folder_clicked(self):
        path = QFileDialog.getExistingDirectory(self,
                                                _("TEXT_FILE_IMPORT_FOLDER"),
                                                self.default_import_path)
        if not path:
            return
        p = pathlib.Path(path)
        files, total_size = self.get_folder(p)
        self.default_import_path = str(p)
        self.import_all(files, total_size)

    def on_files_dropped(self, srcs, dst):
        files = []
        total_size = 0

        if dst == "..":
            dst_dir = self.current_directory.parent
        elif dst == ".":
            dst_dir = self.current_directory
        else:
            dst_dir = self.current_directory / dst

        for src in srcs:
            if src.is_dir():
                tmp_files, tmp_total_size = self.get_folder(src,
                                                            dst_dir=dst_dir)
                files.extend(tmp_files)
                total_size += tmp_total_size
            elif src.is_file():
                tmp_files, tmp_total_size = self.get_files([src],
                                                           dst_dir=dst_dir)
                files.extend(tmp_files)
                total_size += tmp_total_size
        self.import_all(files, total_size)

    def on_file_moved(self, src, dst):
        src_path = self.current_directory / src
        dst_path = ""
        if dst == "..":
            dst_path = self.current_directory.parent / src
        else:
            dst_path = self.current_directory / dst / src
        self.jobs_ctx.run(self.workspace_fs.move, src_path, dst_path)

    def filter_files(self, pattern):
        pattern = pattern.lower()
        for i in range(self.table_files.rowCount()):
            file_type = self.table_files.item(i, 0).data(TYPE_DATA_INDEX)
            name_item = self.table_files.item(i, 1)
            if file_type != FileType.ParentFolder and file_type != FileType.ParentWorkspace:
                if pattern not in name_item.text().lower():
                    self.table_files.setRowHidden(i, True)
                else:
                    self.table_files.setRowHidden(i, False)

    def create_folder_clicked(self):
        def _on_folder_name_finished(return_code, folder_name):
            if return_code and folder_name:
                self.jobs_ctx.submit_job(
                    ThreadSafeQtSignal(self, "folder_create_success",
                                       QtToTrioJob),
                    ThreadSafeQtSignal(self, "folder_create_error",
                                       QtToTrioJob),
                    _do_folder_create,
                    workspace_fs=self.workspace_fs,
                    path=self.current_directory / folder_name,
                )

        get_text_input(
            self,
            _("TEXT_FILE_CREATE_FOLDER_TITLE"),
            _("TEXT_FILE_CREATE_FOLDER_INSTRUCTIONS"),
            placeholder=_("TEXT_FILE_CREATE_FOLDER_PLACEHOLDER"),
            button_text=_("ACTION_FILE_CREATE_FOLDER"),
            on_finished=_on_folder_name_finished,
        )

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Delete:
            self.delete_files()

    def _on_rename_success(self, job):
        self.reload()

    def _on_rename_error(self, job):
        if job.exc.params.get("multi"):
            show_error(self,
                       _("TEXT_FILE_RENAME_MULTIPLE_ERROR"),
                       exception=job.exc)
        else:
            show_error(self, _("TEXT_FILE_RENAME_ERROR"), exception=job.exc)

    def _on_delete_success(self, job):
        self.reload()

    def _on_delete_error(self, job):
        if not getattr(job.exc, "params", None):
            return
        if job.exc.params.get("multi"):
            show_error(self,
                       _("TEXT_FILE_DELETE_MULTIPLE_ERROR"),
                       exception=job.exc)
        else:
            show_error(self, _("TEXT_FILE_DELETE_ERROR"), exception=job.exc)

    def _on_folder_stat_success(self, job):
        (
            self.current_directory,
            self.current_directory_uuid,
            files_stats,
            default_selection,
        ) = job.ret
        self.table_files.clear()
        self.table_files.setStyleSheet("background-color: #FFFFFF;")
        self.spinner.spinner_movie.stop()
        self.spinner.hide()
        old_sort = self.table_files.horizontalHeader().sortIndicatorSection()
        old_order = self.table_files.horizontalHeader().sortIndicatorOrder()
        self.table_files.setSortingEnabled(False)
        if self.current_directory == FsPath("/"):
            self.table_files.add_parent_workspace()
        else:
            self.table_files.add_parent_folder()
        file_found = False
        for path, stats in files_stats.items():
            selected = False
            if default_selection and str(path) == default_selection:
                selected = True
                file_found = True
            if stats["type"] == "inconsistency":
                self.table_files.add_inconsistency(str(path), stats["id"])
            elif stats["type"] == "folder":
                self.table_files.add_folder(str(path), stats["id"],
                                            not stats["need_sync"],
                                            stats["confined"], selected)
            else:
                self.table_files.add_file(
                    str(path),
                    stats["id"],
                    stats["size"],
                    stats["created"],
                    stats["updated"],
                    not stats["need_sync"],
                    stats["confined"],
                    selected,
                )
        self.table_files.sortItems(old_sort, old_order)
        self.table_files.setSortingEnabled(True)
        if self.line_edit_search.text():
            self.filter_files(self.line_edit_search.text())
        if default_selection and not file_found:
            show_error(self, _("TEXT_FILE_GOTO_LINK_NOT_FOUND"))
        workspace_name = self.jobs_ctx.run_sync(
            self.workspace_fs.get_workspace_name)
        self.folder_changed.emit(str(workspace_name),
                                 str(self.current_directory))

    def _on_folder_stat_error(self, job):
        self.table_files.clear()
        self.table_files.setStyleSheet("background-color: #FFFFFF;")
        self.spinner.spinner_movie.stop()
        self.spinner.hide()
        if isinstance(job.exc, FSFileNotFoundError):
            show_error(self, _("TEXT_FILE_FOLDER_NOT_FOUND"))
            self.table_files.add_parent_workspace()
            return
        if self.current_directory == FsPath("/"):
            self.table_files.add_parent_workspace()
        else:
            self.table_files.add_parent_folder()

    def _on_folder_create_success(self, job):
        pass

    def _on_folder_create_error(self, job):
        if job.status == "already-exists":
            show_error(self, _("TEXT_FILE_FOLDER_CREATE_ERROR_ALREADY_EXISTS"))
        else:
            show_error(self, _("TEXT_FILE_FOLDER_CREATE_ERROR_UNKNOWN"))

    def _on_import_success(self):
        assert self.loading_dialog
        self.loading_dialog.hide()
        self.loading_dialog.setParent(None)
        self.loading_dialog = None
        self.import_job = None

    def _on_import_error(self):
        assert self.loading_dialog
        if hasattr(self.import_job.exc,
                   "status") and self.import_job.exc.status == "cancelled":
            self.jobs_ctx.submit_job(
                ThreadSafeQtSignal(self, "delete_success", QtToTrioJob),
                ThreadSafeQtSignal(self, "delete_error", QtToTrioJob),
                _do_delete,
                workspace_fs=self.workspace_fs,
                files=[(self.import_job.exc.params["last_file"], FileType.File)
                       ],
                silent=True,
            )
        else:
            show_error(self,
                       _("TEXT_FILE_IMPORT_ERROR"),
                       exception=self.import_job.exc)
        self.loading_dialog.hide()
        self.loading_dialog.setParent(None)
        self.loading_dialog = None
        self.import_job = None

    def _on_fs_entry_synced_trio(self, event, id, workspace_id=None):
        self.fs_synced_qt.emit(event, id)

    def _on_fs_entry_updated_trio(self, event, workspace_id=None, id=None):
        assert id is not None
        if workspace_id is None or (self.workspace_fs is not None
                                    and workspace_id
                                    == self.workspace_fs.workspace_id):
            self.fs_updated_qt.emit(event, id)

    def _on_entry_downsynced_trio(self, event, workspace_id=None, id=None):
        self.entry_downsynced_qt.emit(workspace_id, id)

    def _on_entry_downsynced_qt(self, workspace_id, id):
        if not self.workspace_fs:
            return
        ws_id = self.workspace_fs.workspace_id
        if ws_id != workspace_id:
            return
        if id == self.current_directory_uuid:
            if not self.update_timer.isActive():
                self.update_timer.start()
                self.reload()

    def _on_fs_synced_qt(self, event, uuid):
        if not self.workspace_fs:
            return

        if self.current_directory_uuid == uuid:
            return

        for i in range(1, self.table_files.rowCount()):
            item = self.table_files.item(i, 0)
            if item and item.data(UUID_DATA_INDEX) == uuid:
                if (item.data(TYPE_DATA_INDEX) == FileType.File
                        or item.data(TYPE_DATA_INDEX) == FileType.Folder):
                    item.confined = False
                    item.is_synced = True

    def _on_fs_updated_qt(self, event, uuid):
        if not self.workspace_fs:
            return

        if self.current_directory_uuid == uuid or self.table_files.has_file(
                uuid):
            if not self.update_timer.isActive():
                self.update_timer.start()
                self.reload()

    def _on_sharing_updated_trio(self, event, new_entry, previous_entry):
        self.sharing_updated_qt.emit(new_entry, previous_entry)

    def _on_sharing_updated_qt(self, new_entry, previous_entry):
        if new_entry is None or new_entry.role is None:
            # Sharing revoked
            show_error(self, _("TEXT_FILE_SHARING_REVOKED"))
            self.back_clicked.emit()

        elif previous_entry is not None and previous_entry.role is not None:
            self.current_user_role = new_entry.role
            self.label_role.setText(
                get_role_translation(self.current_user_role))
            if (previous_entry.role != WorkspaceRole.READER
                    and new_entry.role == WorkspaceRole.READER):
                show_error(self, _("TEXT_FILE_SHARING_DEMOTED_TO_READER"))

    def _on_reload_timestamped_requested(self, timestamp, path, file_type,
                                         open_after_load, close_after_remount,
                                         reload_after_remount):
        self.jobs_ctx.submit_job(
            ThreadSafeQtSignal(self, "reload_timestamped_success",
                               QtToTrioJob),
            ThreadSafeQtSignal(self, "reload_timestamped_error", QtToTrioJob),
            _do_remount_timestamped,
            mountpoint_manager=self.client.mountpoint_manager,
            workspace_fs=self.workspace_fs,
            timestamp=timestamp,
            path=path if path is not None else self.current_directory,
            file_type=file_type,
            open_after_load=open_after_load,
            close_after_load=close_after_remount,
            reload_after_remount=reload_after_remount,
        )

    def _on_reload_timestamped_success(self, job):
        (
            workspace_fs,
            path,
            file_type,
            open_after_load,
            close_after_load,
            reload_after_remount,
        ) = job.ret
        self.set_workspace_fs(
            workspace_fs, path.parent if file_type == FileType.File else path)
        # TODO : Select element if possible?
        if close_after_load:
            self.close_version_list.emit()
        if reload_after_remount:
            self.update_version_list.emit(self.workspace_fs, path)
        if open_after_load:
            self.open_file(path.name)

    def _on_reload_timestamped_error(self, job):
        raise job.exc