Beispiel #1
0
class LostFolderDialog(object):
    def __init__(self, parent, path, restoreFolder, dialog_id=0):
        super(LostFolderDialog, self).__init__()
        self._path = path
        self._restoreFolder = restoreFolder
        self._dialog_id = dialog_id

        self._dialog = QDialog(
            parent, Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint)
        self._dialog.setAttribute(Qt.WA_MacFrameworkScaled)
        self._dialog.setWindowIcon(QIcon(':/images/icon.png'))
        self._ui = lost_folder_dialog.Ui_Dialog()
        self._ui.setupUi(self._dialog)

        self._ui.textLabel.setText(self._ui.textLabel.text().replace(
            '{PATH}', path))

        self._connect_slots()

    def _connect_slots(self):
        ui = self._ui
        ui.tryAgainButton.clicked.connect(self._on_tryAgain)
        ui.restoreFolderButton.clicked.connect(self._on_restoreFolder)

    def show(self):
        self._dialog.raise_()
        self._dialog.exec_()

    def _on_tryAgain(self):
        if op.isdir(self._path):
            self._dialog.accept()

    def _on_restoreFolder(self):
        self._dialog.accept()
        self._restoreFolder(self._dialog_id, 0)
class NotificationsDialog(object):
    def __init__(self, parent, parent_window, notifications, dp=None):
        self._dialog = QDialog(parent_window)
        self._dp = dp

        self._notifications = notifications
        self._parent = parent
        self._parent_window = parent_window

        self._dialog.setWindowIcon(QIcon(':/images/icon.png'))
        self._ui = Ui_Dialog()
        self._ui.setupUi(self._dialog)

        self._init_ui()

    def _init_ui(self):
        self._dialog.setWindowFlags(Qt.Dialog)
        self._dialog.setAttribute(Qt.WA_TranslucentBackground)
        self._dialog.setAttribute(Qt.WA_MacFrameworkScaled)

        self._notifications_list = NotificationsList(self._dp)
        self._ui.notifications_area.setWidget(self._notifications_list)
        self._ui.notifications_area.verticalScrollBar().valueChanged.connect(
            self._on_list_scroll_changed)

        self._old_main_resize_event = self._ui.centralwidget.resizeEvent
        self._ui.centralwidget.resizeEvent = self._main_resize_event

        self._loader_movie = QMovie(":/images/loader.gif")
        self._ui.loader_label.setMovie(self._loader_movie)

    def show(self, on_finished):
        def finished():
            self.show_cursor_normal()
            self._dialog.finished.disconnect(finished)
            on_finished()

        logger.debug("Opening notifications dialog")

        screen_width = QApplication.desktop().width()
        parent_x = self._dialog.parent().x()
        parent_width = self._dialog.parent().width()
        width = self._dialog.width()
        offset = 16
        if parent_x + parent_width / 2 > screen_width / 2:
            x = parent_x - width - offset
            if x < 0:
                x = 0
        else:
            x = parent_x + parent_width + offset
            diff = x + width - screen_width
            if diff > 0:
                x -= diff
        self._dialog.move(x, self._dialog.parent().y())

        # Execute dialog
        self._dialog.finished.connect(finished)
        if not self._parent.load_notifications(show_loading=True):
            self.show_notifications()
        self._dialog.raise_()
        self._dialog.show()

    def raise_dialog(self):
        self._dialog.raise_()

    def close(self):
        self._dialog.reject()

    def show_cursor_loading(self, show_movie=False):
        if show_movie:
            self._ui.notifications_pages.setCurrentIndex(2)
            self._loader_movie.start()
        else:
            self._dialog.setCursor(Qt.WaitCursor)
            self._parent_window.setCursor(Qt.WaitCursor)

    def show_cursor_normal(self):
        self._dialog.setCursor(Qt.ArrowCursor)
        self._parent_window.setCursor(Qt.ArrowCursor)
        if self._loader_movie.state() == QMovie.Running:
            self._loader_movie.stop()

    def show_notifications(self):
        if not self._notifications:
            self._ui.notifications_pages.setCurrentIndex(1)
        else:
            self._ui.notifications_pages.setCurrentIndex(0)
            self._notifications_list.show_notifications(self._notifications)

        self.show_cursor_normal()

    def _on_list_scroll_changed(self, *args, **kwargs):
        # value = self._ui.notifications_area.verticalScrollBar().value()
        # logger.debug("Scroll value %s", value)
        if self._parent.all_loaded or self._parent.is_querying:
            return

        if self._notifications_list.loading_needed(self._parent.limit):
            logger.debug("Loading notifications")
            self._parent.load_notifications()

    def _main_resize_event(self, e):
        self._old_main_resize_event(e)
        self._notifications_list.setFixedWidth(
            self._ui.notifications_pages.width() - 8)

        self._on_list_scroll_changed()
Beispiel #3
0
class FieldWidget(QFrame):
    # Used for deletion of field
    something_clicked = Signal()
    enable_3d_value_spinbox = Signal(bool)

    def dataset_type_changed(self, _):
        self.value_line_edit.validator(
        ).dataset_type_combo = self.value_type_combo
        self.value_line_edit.validator(
        ).field_type_combo = self.field_type_combo
        self.value_line_edit.validator().validate(self.value_line_edit.text(),
                                                  0)

    def __init__(
        self,
        node_parent,
        possible_fields=None,
        parent: QListWidget = None,
        parent_dataset: Dataset = None,
        hide_name_field: bool = False,
        show_only_f142_stream: bool = False,
    ):
        super(FieldWidget, self).__init__(parent)

        possible_field_names = []
        self.default_field_types_dict = {}
        self.streams_widget: StreamFieldsWidget = None
        if possible_fields:
            possible_field_names, default_field_types = zip(*possible_fields)
            self.default_field_types_dict = dict(
                zip(possible_field_names, default_field_types))
        self._show_only_f142_stream = show_only_f142_stream
        self._node_parent = node_parent

        self.edit_dialog = QDialog(parent=self)
        self.attrs_dialog = FieldAttrsDialog(parent=self)
        if self.parent() is not None and self.parent().parent() is not None:
            self.parent().parent().destroyed.connect(self.edit_dialog.close)
            self.parent().parent().destroyed.connect(self.attrs_dialog.close)

        self.field_name_edit = FieldNameLineEdit(possible_field_names)
        if self.default_field_types_dict:
            self.field_name_edit.textChanged.connect(self.update_default_type)
        self.hide_name_field = hide_name_field
        if hide_name_field:
            self.name = str(uuid.uuid4())

        self.units_line_edit = QLineEdit()
        self.unit_validator = UnitValidator()
        self.units_line_edit.setValidator(self.unit_validator)
        self.units_line_edit.setMinimumWidth(20)
        self.units_line_edit.setMaximumWidth(50)
        unit_size_policy = QSizePolicy()
        unit_size_policy.setHorizontalPolicy(QSizePolicy.Preferred)
        unit_size_policy.setHorizontalStretch(1)
        self.units_line_edit.setSizePolicy(unit_size_policy)

        self.unit_validator.is_valid.connect(
            partial(validate_line_edit, self.units_line_edit))
        self.units_line_edit.setPlaceholderText(CommonAttrs.UNITS)

        self.field_type_combo: QComboBox = QComboBox()
        self.field_type_combo.addItems([item.value for item in FieldType])
        self.field_type_combo.currentIndexChanged.connect(
            self.field_type_changed)

        fix_horizontal_size = QSizePolicy()
        fix_horizontal_size.setHorizontalPolicy(QSizePolicy.Fixed)
        self.field_type_combo.setSizePolicy(fix_horizontal_size)

        self.value_type_combo: QComboBox = QComboBox()
        self.value_type_combo.addItems(list(VALUE_TYPE_TO_NP))
        for i, item in enumerate(VALUE_TYPE_TO_NP.keys()):
            if item == ValueTypes.DOUBLE:
                self.value_type_combo.setCurrentIndex(i)
                break
        self.value_type_combo.currentIndexChanged.connect(
            self.dataset_type_changed)

        self.value_line_edit: QLineEdit = QLineEdit()
        self.value_line_edit.setPlaceholderText("value")

        value_size_policy = QSizePolicy()
        value_size_policy.setHorizontalPolicy(QSizePolicy.Preferred)
        value_size_policy.setHorizontalStretch(2)
        self.value_line_edit.setSizePolicy(value_size_policy)

        self._set_up_value_validator(False)
        self.dataset_type_changed(0)

        self.nx_class_combo = QComboBox()

        self.edit_button = QPushButton("Edit")

        edit_button_size = 50
        self.edit_button.setMaximumSize(edit_button_size, edit_button_size)
        self.edit_button.setSizePolicy(fix_horizontal_size)
        self.edit_button.clicked.connect(self.show_edit_dialog)

        self.attrs_button = QPushButton("Attrs")
        self.attrs_button.setMaximumSize(edit_button_size, edit_button_size)
        self.attrs_button.setSizePolicy(fix_horizontal_size)
        self.attrs_button.clicked.connect(self.show_attrs_dialog)

        self.layout = QHBoxLayout()
        self.layout.addWidget(self.field_name_edit)
        self.layout.addWidget(self.field_type_combo)
        self.layout.addWidget(self.value_line_edit)
        self.layout.addWidget(self.nx_class_combo)
        self.layout.addWidget(self.edit_button)
        self.layout.addWidget(self.value_type_combo)
        self.layout.addWidget(self.units_line_edit)
        self.layout.addWidget(self.attrs_button)

        self.layout.setAlignment(Qt.AlignLeft)
        self.setLayout(self.layout)

        self.setFrameShadow(QFrame.Raised)
        self.setFrameShape(QFrame.StyledPanel)

        # Allow selecting this field widget in a list by clicking on it's contents
        self.field_name_edit.installEventFilter(self)
        existing_objects = []
        emit = False
        if isinstance(parent, QListWidget):
            for i in range(self.parent().count()):
                new_field_widget = self.parent().itemWidget(
                    self.parent().item(i))
                if new_field_widget is not self and hasattr(
                        new_field_widget, "name"):
                    existing_objects.append(new_field_widget)
        elif isinstance(self._node_parent, Group):
            for child in self._node_parent.children:
                if child is not parent_dataset and hasattr(child, "name"):
                    existing_objects.append(child)
            emit = True
        self._set_up_name_validator(existing_objects=existing_objects)
        self.field_name_edit.validator().is_valid.emit(emit)

        self.value_line_edit.installEventFilter(self)
        self.nx_class_combo.installEventFilter(self)

        # These cause odd double-clicking behaviour when using an event filter so just connecting to the clicked() signals instead.
        self.edit_button.clicked.connect(self.something_clicked)
        self.value_type_combo.highlighted.connect(self.something_clicked)
        self.field_type_combo.highlighted.connect(self.something_clicked)

        # Set the layout for the default field type
        self.field_type_changed()

    def _set_up_name_validator(
            self, existing_objects: List[Union["FieldWidget",
                                               FileWriterModule]]):
        self.field_name_edit.setValidator(
            NameValidator(existing_objects, invalid_names=INVALID_FIELD_NAMES))
        self.field_name_edit.validator().is_valid.connect(
            partial(
                validate_line_edit,
                self.field_name_edit,
                tooltip_on_accept="Field name is valid.",
                tooltip_on_reject="Field name is not valid",
            ))

    @property
    def field_type(self) -> FieldType:
        return FieldType(self.field_type_combo.currentText())

    @field_type.setter
    def field_type(self, field_type: FieldType):
        self.field_type_combo.setCurrentText(field_type.value)
        self.field_type_changed()

    @property
    def name(self) -> str:
        return self.field_name_edit.text()

    @name.setter
    def name(self, name: str):
        self.field_name_edit.setText(name)

    @property
    def dtype(self) -> str:
        return self.value_type_combo.currentText()

    @dtype.setter
    def dtype(self, dtype: str):
        self.value_type_combo.setCurrentText(dtype)

    @property
    def attrs(self):
        return self.value.attributes

    @attrs.setter
    def attrs(self, field: Dataset):
        self.attrs_dialog.fill_existing_attrs(field)

    @property
    def value(self) -> Union[FileWriterModule, None]:
        dtype = self.value_type_combo.currentText()
        return_object: FileWriterModule
        if self.field_type == FieldType.scalar_dataset:
            val = self.value_line_edit.text()
            return_object = Dataset(
                parent_node=self._node_parent,
                name=self.name,
                type=dtype,
                values=val,
            )
        elif self.field_type == FieldType.array_dataset:
            # Squeeze the array so 1D arrays can exist. Should not affect dimensional arrays.
            array = np.squeeze(self.table_view.model.array)
            return_object = Dataset(
                parent_node=self._node_parent,
                name=self.name,
                type=dtype,
                values=array,
            )
        elif self.field_type == FieldType.kafka_stream:
            return_object = self.streams_widget.get_stream_module(
                self._node_parent)
        elif self.field_type == FieldType.link:
            return_object = Link(
                parent_node=self._node_parent,
                name=self.name,
                source=self.value_line_edit.text(),
            )
        else:
            logging.error(f"unknown field type: {self.name}")
            return None

        if self.field_type != FieldType.link:
            for name, value, dtype in self.attrs_dialog.get_attrs():
                return_object.attributes.set_attribute_value(
                    attribute_name=name,
                    attribute_value=value,
                    attribute_type=dtype,
                )
            if self.units and self.units is not None:
                return_object.attributes.set_attribute_value(
                    CommonAttrs.UNITS, self.units)
        return return_object

    @value.setter
    def value(self, value):
        if self.field_type == FieldType.scalar_dataset:
            self.value_line_edit.setText(to_string(value))
        elif self.field_type == FieldType.array_dataset:
            self.table_view.model.array = value
        elif self.field_type == FieldType.link:
            self.value_line_edit.setText(value)

    @property
    def units(self) -> str:
        return self.units_line_edit.text()

    @units.setter
    def units(self, new_units: str):
        self.units_line_edit.setText(new_units)

    def update_default_type(self):
        self.value_type_combo.setCurrentText(
            self.default_field_types_dict.get(self.field_name_edit.text(),
                                              "double"))

    def eventFilter(self, watched: QObject, event: QEvent) -> bool:
        if event.type() == QEvent.MouseButtonPress:
            self.something_clicked.emit()
            return True
        else:
            return False

    def field_type_is_scalar(self) -> bool:
        return self.field_type == FieldType.scalar_dataset

    def field_type_changed(self):
        self.edit_dialog = QDialog(parent=self)
        self.edit_dialog.setModal(True)
        self._set_up_value_validator(False)
        self.enable_3d_value_spinbox.emit(not self.field_type_is_scalar())

        if self.field_type == FieldType.scalar_dataset:
            self.set_visibility(True, False, False, True)
        elif self.field_type == FieldType.array_dataset:
            self.set_visibility(False, False, True, True)
            self.table_view = ArrayDatasetTableWidget()
        elif self.field_type == FieldType.kafka_stream:
            self.set_visibility(False,
                                False,
                                True,
                                False,
                                show_name_line_edit=True)
            self.streams_widget = StreamFieldsWidget(
                self.edit_dialog,
                show_only_f142_stream=self._show_only_f142_stream)
        elif self.field_type == FieldType.link:
            self.set_visibility(
                True,
                False,
                False,
                False,
                show_unit_line_edit=False,
                show_attrs_edit=False,
            )
            self._set_up_value_validator(False)

    def _set_up_value_validator(self, is_link: bool):
        self.value_line_edit.setValidator(None)
        if is_link:
            return
        else:
            self.value_line_edit.setValidator(
                FieldValueValidator(
                    self.field_type_combo,
                    self.value_type_combo,
                    FieldType.scalar_dataset.value,
                ))
            tooltip_on_accept = "Value is cast-able to numpy type."
            tooltip_on_reject = "Value is not cast-able to selected numpy type."

        self.value_line_edit.validator().is_valid.connect(
            partial(
                validate_line_edit,
                self.value_line_edit,
                tooltip_on_accept=tooltip_on_accept,
                tooltip_on_reject=tooltip_on_reject,
            ))
        self.value_line_edit.validator().validate(self.value_line_edit.text(),
                                                  None)

    def set_visibility(
        self,
        show_value_line_edit: bool,
        show_nx_class_combo: bool,
        show_edit_button: bool,
        show_value_type_combo: bool,
        show_name_line_edit: bool = True,
        show_attrs_edit: bool = True,
        show_unit_line_edit: bool = True,
    ):
        self.value_line_edit.setVisible(show_value_line_edit)
        self.nx_class_combo.setVisible(show_nx_class_combo)
        self.edit_button.setVisible(show_edit_button)
        self.value_type_combo.setVisible(show_value_type_combo)
        self.units_line_edit.setVisible(show_unit_line_edit)
        self.attrs_button.setVisible(show_attrs_edit)
        self.field_name_edit.setVisible(show_name_line_edit
                                        and not self.hide_name_field)

    def show_edit_dialog(self):
        if self.field_type == FieldType.array_dataset:
            self.edit_dialog.setLayout(QGridLayout())
            self.table_view.model.update_array_dtype(
                VALUE_TYPE_TO_NP[self.value_type_combo.currentText()])
            self.edit_dialog.layout().addWidget(self.table_view)
            self.edit_dialog.setWindowTitle(
                f"Edit {self.value_type_combo.currentText()} Array field")
        elif self.field_type == FieldType.kafka_stream:
            self.edit_dialog.setLayout(QFormLayout())
            self.edit_dialog.layout().addWidget(self.streams_widget)
        if self.edit_dialog.isVisible():
            self.edit_dialog.raise_()
        else:
            self.edit_dialog.show()

    def show_attrs_dialog(self):
        self.attrs_dialog.show()
Beispiel #4
0
class Settings(object):
    class _MigrationFailed(ExpectedError):
        pass

    def __init__(self,
                 cfg,
                 main_cfg,
                 start_service,
                 exit_service,
                 parent=None,
                 size=None,
                 migrate=False,
                 dp=1,
                 get_offline_dirs=lambda: None,
                 set_offline_dirs=lambda o, no: None):
        super(Settings, self).__init__()
        self._cfg = cfg
        self._main_cfg = main_cfg
        self._start_service = start_service
        self._exit_service = exit_service
        self._parent = parent
        self._size = size
        self._dp = dp
        self._get_offline_dirs = get_offline_dirs
        self._set_offline_dirs = set_offline_dirs

        self._dialog = QDialog(parent)
        self._dialog.setWindowIcon(QIcon(':/images/icon.png'))
        self._dialog.setAttribute(Qt.WA_MacFrameworkScaled)
        self._ui = settings.Ui_Dialog()
        self._ui.setupUi(self._dialog)
        self._max_root_len = get_max_root_len(self._cfg)
        self._migrate = migrate
        self._migration = None
        self._migration_cancelled = False

        try:
            self._ui.account_type.setText(
                license_display_name_from_constant(self._cfg.license_type))
            self._ui.account_type.setVisible(True)
            self._ui.account_type_header.setVisible(True)
            self._ui.account_upgrade.setVisible(True)
        except KeyError:
            pass
        upgrade_license_types = (FREE_LICENSE, FREE_TRIAL_LICENSE)
        if self._cfg.license_type in upgrade_license_types:
            self._ui.account_upgrade.setText('<a href="{}">{}</a>'.format(
                GET_PRO_URI.format(self._cfg.host), tr('Upgrade')))
            self._ui.account_upgrade.setTextFormat(Qt.RichText)
            self._ui.account_upgrade.setTextInteractionFlags(
                Qt.TextBrowserInteraction)
            self._ui.account_upgrade.setOpenExternalLinks(True)
            self._ui.account_upgrade.setAlignment(Qt.AlignLeft)
        else:
            self._ui.account_upgrade.setText("")

        self._ui.centralWidget.setFrameShape(QFrame.NoFrame)
        self._ui.centralWidget.setLineWidth(1)

        self._ui.language_comboBox.addItem(tr('English'))
        self._ui.language_comboBox.setEnabled(False)

        self._connect_slots()
        self._set_fonts()
        self._ui.tabWidget.setCurrentIndex(0)

        self._smart_sync_dialog = None

        self.logged_out = Signal(bool)
        self.logging_disabled_changed = Signal(bool)

        # FIXMe: without line below app crashes on exit after settings opened
        self._dialog.mousePressEvent = self.on_mouse_press_event

    def on_mouse_press_event(self, ev):
        pass

    def _connect_slots(self):
        ui = self._ui

        ui.logout_button.clicked.connect(self._logout)

        ui.download_auto_radioButton.clicked.connect(
            lambda: ui.download_limit_edit.setEnabled(
                False) or ui.download_limit_edit.clear())
        ui.download_limit_radioButton.clicked.connect(
            lambda: ui.download_limit_edit.setEnabled(True))

        ui.upload_auto_radioButton.clicked.connect(
            lambda: ui.upload_limit_edit.setEnabled(
                False) or ui.upload_limit_edit.clear())
        ui.upload_limit_radioButton.clicked.connect(
            lambda: ui.upload_limit_edit.setEnabled(True))

        ui.buttonBox.accepted.connect(self._dialog.accept)
        ui.buttonBox.rejected.connect(self._dialog.reject)

        ui.smart_sync_button.clicked.connect(
            self._on_smart_sync_button_clicked)

        ui.location_button.clicked.connect(
            self._on_sync_folder_location_button_clicked)

        ui.location_button.enterEvent = lambda _: \
            ui.location_button.setIcon(QIcon(
                ':/images/settings/pencil_hovered.svg'))
        ui.location_button.leaveEvent = lambda _: \
            ui.location_button.setIcon(QIcon(
                ':/images/settings/pencil.svg'))
        ui.smart_sync_button.enterEvent = lambda _: \
            ui.smart_sync_button.setIcon(QIcon(
                ':/images/settings/folder_sync_hovered.svg'))
        ui.smart_sync_button.leaveEvent = lambda _: \
            ui.smart_sync_button.setIcon(QIcon(
                ':/images/settings/folder_sync.svg'))
        ui.logout_button.enterEvent = lambda _: \
            ui.logout_button.setIcon(QIcon(
                ':/images/settings/logout_hovered.svg'))
        ui.logout_button.leaveEvent = lambda _: \
            ui.logout_button.setIcon(QIcon(
                ':/images/settings/logout.svg'))

    def _set_fonts(self):
        ui = self._ui
        controls = [ui.tabWidget, ui.language_comboBox]
        controls.extend([c for c in ui.tabWidget.findChildren(QLabel)])
        controls.extend([c for c in ui.tabWidget.findChildren(QLineEdit)])
        controls.extend([c for c in ui.tabWidget.findChildren(QPushButton)])
        controls.extend([c for c in ui.tabWidget.findChildren(QCheckBox)])
        controls.extend([c for c in ui.tabWidget.findChildren(QRadioButton)])

        for control in controls:
            font = control.font()
            font_size = control.font().pointSize() * self._dp
            if font_size > 0:
                control_font = QFont(font.family(), font_size)
                control_font.setBold(font.bold())
                control.setFont(control_font)

    def _logout(self):
        userAnswer = msgbox(tr('Keep local files on device?'),
                            buttons=[
                                (tr('Clear all'), 'Wipe'),
                                (tr('Keep'), 'Keep'),
                            ],
                            parent=self._dialog,
                            default_index=1,
                            enable_close_button=True)

        if userAnswer == '':
            return

        wipe_all = userAnswer == 'Wipe'
        if not wipe_all:
            self._cfg.set_settings({'user_password_hash': ""})

        self.logged_out.emit(wipe_all)

        self._dialog.reject()

    def show(self, on_finished):
        def finished():
            if self._dialog.result() == QDialog.Accepted:
                self._apply_settings()
            self._dialog.finished.disconnect(finished)
            on_finished()

        self._setup_to_ui()
        if self._migrate:
            self._ui.tabWidget.setCurrentIndex(1)  # Account page
            QTimer.singleShot(100,
                              self._on_sync_folder_location_button_clicked)
        self._dialog.finished.connect(finished)
        self._dialog.raise_()
        self._dialog.setModal(True)
        self._dialog.show()

    def _setup_to_ui(self):
        ui = self._ui
        cfg = self._cfg

        portable = is_portable()

        if cfg.get_setting('lang', None) is None:
            self._ui.language_comboBox.setCurrentIndex(0)
        else:
            lang = cfg.lang if cfg.lang in get_available_languages() else 'en'
            assert lang in get_available_languages()
            for i in range(1, ui.language_comboBox.count()):
                if ui.language_comboBox.itemText(i) == lang:
                    ui.language_comboBox.setCurrentIndex(i)
                    break

        ui.location_edit.setText(
            FilePath(cfg.sync_directory) if cfg.sync_directory else '')
        ui.location_button.setEnabled(not portable)
        if portable:
            ui.location_button.setToolTip(tr("Disabled in portable version"))
        ui.email_label.setText(cfg.user_email if cfg.user_email else '')

        def set_limit(limit, auto_btn, manual_btn, edit):
            edit.setValidator(QRegExpValidator(QRegExp("\\d{1,9}")))
            if limit:
                manual_btn.setChecked(True)
                edit.setText(str(limit))
            else:
                auto_btn.setChecked(True)
                auto_btn.click()

        set_limit(limit=cfg.download_limit,
                  auto_btn=ui.download_auto_radioButton,
                  manual_btn=ui.download_limit_radioButton,
                  edit=ui.download_limit_edit)
        set_limit(limit=cfg.upload_limit,
                  auto_btn=ui.upload_auto_radioButton,
                  manual_btn=ui.upload_limit_radioButton,
                  edit=ui.upload_limit_edit)

        ui.autologin_checkbox.setChecked(self._main_cfg.autologin)
        ui.autologin_checkbox.setEnabled(not portable)
        if portable:
            ui.autologin_checkbox.setToolTip(
                tr("Disabled in portable version"))
        ui.tracking_checkbox.setChecked(cfg.send_statistics)
        ui.autoupdate_checkbox.setChecked(self._main_cfg.autoupdate)
        ui.download_backups_checkBox.setChecked(cfg.download_backups)
        ui.is_smart_sync_checkBox.setChecked(cfg.smart_sync)
        ui.disable_logging_checkBox.setChecked(self._main_cfg.logging_disabled)

        # Disable smart sync for free license
        if not cfg.license_type or cfg.license_type == FREE_LICENSE:
            ui.is_smart_sync_checkBox.setText(
                tr("SmartSync+ is not available for your license"))
            ui.is_smart_sync_checkBox.setChecked(False)
            ui.is_smart_sync_checkBox.setCheckable(False)
            ui.smart_sync_button.setEnabled(False)

        ui.startup_checkbox.setChecked(is_in_system_startup())
        ui.startup_checkbox.setEnabled(not portable)
        if portable:
            ui.startup_checkbox.setToolTip(tr("Disabled in portable version"))

    def _apply_settings(self):
        service_settings, main_settings = self._get_configs_from_ui()
        if main_settings['logging_disabled'] != \
                self._main_cfg.logging_disabled:
            self.logging_disabled_changed.emit(
                main_settings['logging_disabled'])
        self._cfg.set_settings(service_settings)
        self._main_cfg.set_settings(main_settings)
        if self._ui.startup_checkbox.isChecked():
            if not is_in_system_startup():
                add_to_system_startup()
        else:
            if is_in_system_startup():
                remove_from_system_startup()

    def _config_is_changed(self):
        service_settings, main_settings = self._get_configs_from_ui()
        for param, value in service_settings.items():
            if self._cfg.get_setting(param) != value:
                return True
        for param, value in main_settings.items():
            if self._main_cfg.get_setting(param) != value:
                return True

        return False

    def _get_configs_from_ui(self):
        ui = self._ui
        return {
            'lang': (str(ui.language_comboBox.currentText())
                     if ui.language_comboBox.currentIndex() > 0 else None),
            'upload_limit': (0 if ui.upload_auto_radioButton.isChecked()
                             or not ui.upload_limit_edit.text() else int(
                                 ui.upload_limit_edit.text())),
            'download_limit': (0 if ui.download_auto_radioButton.isChecked()
                               or not ui.download_limit_edit.text() else int(
                                   ui.download_limit_edit.text())),
            'send_statistics':
            bool(ui.tracking_checkbox.isChecked()),
            'download_backups':
            bool(ui.download_backups_checkBox.isChecked()),
            'smart_sync':
            bool(ui.is_smart_sync_checkBox.isChecked()),
            'autologin':
            bool(ui.autologin_checkbox.isChecked()),
        }, {
            'autologin': bool(ui.autologin_checkbox.isChecked()),
            'autoupdate': bool(ui.autoupdate_checkbox.isChecked()),
            'logging_disabled': bool(ui.disable_logging_checkBox.isChecked()),
            'download_backups': bool(ui.download_backups_checkBox.isChecked()),
        }

    def _on_smart_sync_button_clicked(self):
        self._get_offline_dirs()
        root = str(self._ui.location_edit.text())
        self._smart_sync_dialog = SmartSyncDialog(self._dialog)
        offline, online = self._smart_sync_dialog.show(root_path=root,
                                                       hide_dotted=True)
        if offline or online:
            logger.info("Directories set to be offline: (%s)",
                        ", ".join(map(lambda s: u"'%s'" % s, offline)))
            self._set_offline_dirs(offline, online)

    def offline_dirs(self, offline_dirs):
        root = str(self._ui.location_edit.text())
        pc = PathConverter(root)
        offline_dirs_abs_paths = set(
            map(lambda p: pc.create_abspath(p), offline_dirs))
        if self._smart_sync_dialog:
            self._smart_sync_dialog.set_offline_paths(offline_dirs_abs_paths)

    def _on_sync_folder_location_button_clicked(self):
        selected_folder = QFileDialog.getExistingDirectory(
            self._dialog, tr('Choose Pvtbox folder location'),
            get_parent_dir(FilePath(self._cfg.sync_directory)))
        selected_folder = ensure_unicode(selected_folder)

        try:
            if not selected_folder:
                raise self._MigrationFailed("Folder is not selected")

            if len(selected_folder + "/Pvtbox") > self._max_root_len:
                if not self._migrate:
                    msgbox(tr("Destination path too long. "
                              "Please select shorter path."),
                           tr("Path too long"),
                           parent=self._dialog)
                raise self._MigrationFailed("Destination path too long")

            free_space = get_free_space(selected_folder)
            selected_folder = get_data_dir(dir_parent=selected_folder,
                                           create=False)
            if FilePath(selected_folder) == FilePath(self._cfg.sync_directory):
                raise self._MigrationFailed("Same path selected")

            if FilePath(selected_folder) in FilePath(self._cfg.sync_directory):
                msgbox(tr("Can't migrate into existing Pvtbox folder.\n"
                          "Please choose other location"),
                       tr("Invalid Pvtbox folder location"),
                       parent=self._dialog)
                raise self._MigrationFailed(
                    "Can't migrate into existing Pvtbox folder")

            if self._size and free_space < self._size:
                logger.debug(
                    "No disk space in %s. Free space: %s. Needed: %s.",
                    selected_folder, free_space, self._size)
                msgbox(tr(
                    "Insufficient disk space for migration to\n{}.\n"
                    "Please clean disk", selected_folder),
                       tr("No disk space"),
                       parent=self._dialog)
                raise self._MigrationFailed(
                    "Insufficient disk space for migration")

            self._migration_cancelled = False
            dialog = QProgressDialog(self._dialog)
            dialog.setWindowTitle(tr('Migrating to new Pvtbox folder'))
            dialog.setWindowIcon(QIcon(':/images/icon.svg'))
            dialog.setModal(True)
            dialog.setMinimum(0)
            dialog.setMaximum(100)
            dialog.setMinimumSize(400, 80)
            dialog.setAutoClose(False)

            def progress(value):
                logger.debug("Migration dialog progress received: %s", value)
                dialog.setValue(value)

            def migration_failed(error):
                logger.warning("Migration failed with error: %s", error)
                msgbox(error,
                       tr('Migration to new Pvtbox folder error'),
                       parent=dialog)
                dialog.cancel()
                self._migration_cancelled = True
                done()

            def cancel():
                logger.debug("Migration dialog cancelled")
                self._migration_cancelled = True
                self._migration.cancel()

            def done():
                logger.debug("Migration done")
                try:
                    self._migration.progress.disconnect(progress)
                    self._migration.failed.disconnect(migration_failed)
                    self._migration.done.disconnect(done)
                    dialog.canceled.disconnect(cancel)
                except Exception as e:
                    logger.warning("Can't disconnect signal %s", e)
                dialog.hide()
                dialog.done(QDialog.Accepted)
                dialog.close()

            self._migration = SyncDirMigration(self._cfg, parent=self._dialog)
            self._migration.progress.connect(progress, Qt.QueuedConnection)
            self._migration.failed.connect(migration_failed,
                                           Qt.QueuedConnection)
            self._migration.done.connect(done, Qt.QueuedConnection)
            dialog.canceled.connect(cancel)
            self._exit_service()
            old_dir = self._cfg.sync_directory
            self._migration.migrate(old_dir, selected_folder)

            def on_finished():
                logger.info("Migration dialog closed")
                if not self._migration_cancelled:
                    logger.debug("Setting new location")
                    self._ui.location_edit.setText(FilePath(selected_folder))

                    disable_file_logging(logger)
                    shutil.rmtree(op.join(old_dir, '.pvtbox'),
                                  ignore_errors=True)
                    set_root_directory(FilePath(selected_folder))
                    enable_file_logging(logger)

                    make_dir_hidden(get_patches_dir(selected_folder))

                self._start_service()

            dialog.finished.connect(on_finished)
            dialog.show()

        except self._MigrationFailed as e:
            logger.warning("Sync dir migration failed. Reason: %s", e)
        finally:
            if self._migrate:
                self._dialog.accept()
class DeviceListDialog(QObject):
    show_tray_notification = Signal(str)
    management_action = Signal(
        str,  # action name
        str,  # action type
        str,  # node id
        bool)  # is_itself
    start_transfers = Signal()
    _update = Signal(list)

    def __init__(self,
                 parent=None,
                 initial_data=(),
                 disk_usage=0,
                 node_status=SS_STATUS_SYNCED,
                 node_substatus=None,
                 dp=1,
                 nodes_actions=(),
                 license_type=None):
        QObject.__init__(self)
        self._update.connect(self._update_data, Qt.QueuedConnection)
        self._dialog = QDialog(parent)
        self._dialog.setWindowIcon(QIcon(':/images/icon.png'))
        self._ui = Ui_Dialog()
        self._ui.setupUi(self._dialog)
        self._dialog.setAttribute(Qt.WA_MacFrameworkScaled)
        self._ui.device_list_view.setFont(QFont('Nano', 10 * dp))
        self._license_type = license_type

        self._model = TableModel(disk_usage, node_status, node_substatus)
        QTimer.singleShot(100, lambda: self.update(initial_data))

        self._view = self._ui.device_list_view
        self._view.setModel(self._model)
        self._view.setSelectionMode(QAbstractItemView.NoSelection)

        self._ui.centralWidget.setFrameShape(QFrame.NoFrame)
        self._ui.centralWidget.setLineWidth(1)

        self._nodes_actions = nodes_actions

    def show(self, on_finished):
        def finished():
            self._dialog.finished.disconnect(finished)
            self._view.resizeRowsToContents()
            self._model.beginResetModel()
            on_finished()

        screen_width = QApplication.desktop().width()
        parent_x = self._dialog.parent().x()
        parent_width = self._dialog.parent().width()
        width = self._dialog.width()
        offset = 16
        if parent_x + parent_width / 2 > screen_width / 2:
            x = parent_x - width - offset
            if x < 0:
                x = 0
        else:
            x = parent_x + parent_width + offset
            diff = x + width - screen_width
            if diff > 0:
                x -= diff
        self._dialog.move(x, self._dialog.parent().y())
        if width > screen_width - offset:
            self._dialog.resize(screen_width - offset, self._dialog.height())

        self._view.setMouseTracking(True)
        self._old_mouse_move_event = self._view.mouseMoveEvent
        self._view.mouseMoveEvent = self._mouse_moved
        self._old_mouse_release_event = self._view.mouseMoveEvent
        self._view.mouseReleaseEvent = self._mouse_released

        logger.info("Opening device list dialog...")
        # Execute dialog
        self._dialog.finished.connect(finished)
        self._dialog.raise_()
        self._dialog.show()

    def update(self, nodes_info):
        self._update.emit(nodes_info)

    def _update_data(self, nodes_info):
        changed_nodes, deleted_nodes = self._model.update(nodes_info)
        for node_id in changed_nodes | deleted_nodes:
            self._nodes_actions.pop(node_id, None)
        self._view.resizeRowsToContents()

    def update_download_speed(self, value):
        self._model.update_node_download_speed(value)
        self._view.resizeRowsToContents()

    def update_upload_speed(self, value):
        self._model.update_node_upload_speed(value)
        self._view.resizeRowsToContents()

    def update_sync_dir_size(self, value):
        self._model.update_node_sync_dir_size(int(value))
        self._view.resizeRowsToContents()

    def update_node_status(self, value, substatus):
        self._model.update_node_status(value, substatus)
        self._view.resizeRowsToContents()

    def close(self):
        self._dialog.reject()

    def set_license_type(self, license_type):
        self._license_type = license_type

    def _mouse_moved(self, event):
        pos = event.pos()
        index = self._view.indexAt(pos)
        if index.isValid():
            if self._model.to_manage(index) and \
                    not self._pos_is_in_scrollbar_header(pos):
                self._view.setCursor(Qt.PointingHandCursor)
            else:
                self._view.setCursor(Qt.ArrowCursor)
        else:
            self._view.setCursor(Qt.ArrowCursor)
        self._old_mouse_move_event(event)

    def _mouse_released(self, event):
        pos = event.pos()
        index = self._view.indexAt(pos)
        if index.isValid():
            if self._model.to_manage(index) and \
                    not self._pos_is_in_scrollbar_header(pos):
                self._show_menu(index, pos)
        self._old_mouse_release_event(event)

    def _pos_is_in_scrollbar_header(self, pos):
        # mouse is not tracked as in view when in header or scrollbar area
        # so pretend we are there if we are near
        pos_in_header = pos.y() < 10
        if pos_in_header:
            return True

        scrollbars = self._view.findChildren(QScrollBar)
        if not scrollbars:
            return False

        pos_x = self._view.mapToGlobal(pos).x()
        for scrollbar in scrollbars:
            if not scrollbar.isVisible():
                continue

            scrollbar_x = scrollbar.mapToGlobal(QPoint(0, 0)).x()
            if scrollbar_x - 10 <= pos_x <= scrollbar_x + scrollbar.width():
                return True

        return False

    def _show_menu(self, index, pos):
        node_id, \
        node_name, \
        is_online, \
        is_itself, \
        is_wiped = self._model.get_node_id_online_itself(index)
        if not node_id:
            return

        license_free = self._license_type == FREE_LICENSE

        menu = QMenu(self._view)
        menu.setStyleSheet("background-color: #EFEFF4; ")
        menu.setToolTipsVisible(license_free)
        if license_free:
            menu.setStyleSheet(
                'QToolTip {{background-color: #222222; color: white;}}')
            menu.hovered.connect(lambda a: self._on_menu_hovered(a, menu))

        def add_menu_item(caption,
                          index=None,
                          action_name=None,
                          action_type="",
                          start_transfers=False,
                          disabled=False,
                          tooltip=""):
            action = menu.addAction(caption)
            action.setEnabled(not disabled)
            action.tooltip = tooltip if tooltip else ""
            if not start_transfers:
                action.triggered.connect(lambda: self._on_menu_clicked(
                    index, action_name, action_type))
            else:
                action.triggered.connect(self.start_transfers.emit)

        tooltip = tr("Not available for free license") \
            if license_free and not is_itself else ""
        if not is_online:
            action_in_progress = ("hideNode", "") in \
                                 self._nodes_actions.get(node_id, set())
            item_text = tr("Remove node") if not action_in_progress \
                else tr("Remove node in progress...")
            add_menu_item(item_text,
                          index,
                          "hideNode",
                          disabled=action_in_progress)
        elif is_itself:
            add_menu_item(tr("Transfers..."), start_transfers=True)
        if not is_wiped:
            wipe_in_progress = ("execute_remote_action", "wipe") in \
                                 self._nodes_actions.get(node_id, set())
            if not wipe_in_progress:
                action_in_progress = ("execute_remote_action", "logout") in \
                                     self._nodes_actions.get(node_id, set())
                item_text = tr("Log out") if not action_in_progress \
                    else tr("Log out in progress...")
                add_menu_item(item_text,
                              index,
                              "execute_remote_action",
                              "logout",
                              disabled=action_in_progress
                              or license_free and not is_itself,
                              tooltip=tooltip)
            item_text = tr("Log out && wipe") if not wipe_in_progress \
                else tr("Wipe in progress...")
            add_menu_item(item_text,
                          index,
                          "execute_remote_action",
                          "wipe",
                          disabled=wipe_in_progress
                          or license_free and not is_itself,
                          tooltip=tooltip)

        pos_to_show = QPoint(pos.x(), pos.y() + 20)
        menu.exec_(self._view.mapToGlobal(pos_to_show))

    def _on_menu_clicked(self, index, action_name, action_type):
        node_id, \
        node_name, \
        is_online, \
        is_itself, \
        is_wiped = self._model.get_node_id_online_itself(index)
        if action_name == "hideNode" and is_online:
            self.show_tray_notification.emit(
                tr("Action unavailable for online node"))
            return

        if (action_name == "hideNode" or action_type == "wipe"):
            if action_name == "hideNode":
                alert_str = tr(
                    '"{}" node will be removed '
                    'from list of devices. Files will not be wiped.'.format(
                        node_name))
            else:
                alert_str = tr(
                    'All files from "{}" node\'s '
                    'pvtbox secured folder will be wiped. '.format(node_name))
            if not self._user_confirmed_action(alert_str):
                return

        if not is_itself:
            self._nodes_actions[node_id].add((action_name, action_type))
        self.management_action.emit(action_name, action_type, node_id,
                                    is_itself)

    def _on_menu_hovered(self, action, menu):
        if not action.tooltip:
            return

        a_geometry = menu.actionGeometry(action)
        point = menu.mapToGlobal(
            QPoint(a_geometry.x() + 30,
                   a_geometry.y() + 5))
        QToolTip.showText(point, action.tooltip, menu, a_geometry,
                          60 * 60 * 1000)

    def _user_confirmed_action(self, alert_str):
        msg = tr("<b>Are</b> you <b>sure</b>?<br><br>{}".format(alert_str))
        userAnswer = msgbox(msg,
                            title=' ',
                            buttons=[
                                (tr('Cancel'), 'Cancel'),
                                (tr('Yes'), 'Yes'),
                            ],
                            parent=self._dialog,
                            default_index=0,
                            enable_close_button=True)

        return userAnswer == 'Yes'

    def on_management_action_in_progress(self, action_name, action_type,
                                         node_id):
        self._nodes_actions[node_id].add((action_name, action_type))
Beispiel #6
0
class CollaborationSettingsDialog(object):
    ADD_BUTTON_ACTIVE_COLOR = "#f78d1e"
    ADD_BUTTON_PASSIVE_COLOR = "#9a9a9a"
    ERROR_COLOR = '#FF9999'
    LINE_EDIT_NORMAL_COLOR = "#EFEFF1"

    def __init__(self, parent, parent_window, colleagues, folder, dp):
        self._dialog = QDialog(parent_window)
        self._dp = dp
        self._colleagues = colleagues
        self._parent = parent
        self._parent_window = parent_window
        self._folder = folder

        self._is_owner = False
        self._dialog.setWindowIcon(QIcon(':/images/icon.png'))
        self._ui = Ui_Dialog()
        self._ui.setupUi(self._dialog)

        self._init_ui()

    def _init_ui(self):
        self._dialog.setWindowFlags(Qt.Dialog)
        self._dialog.setAttribute(Qt.WA_TranslucentBackground)
        self._dialog.setAttribute(Qt.WA_MacFrameworkScaled)
        self._dialog.setWindowTitle(self._dialog.windowTitle() + self._folder)

        self._ui.colleagues_list.setAlternatingRowColors(True)
        self._colleagues_list = ColleaguesList(self._parent,
                                               self._ui.colleagues_list,
                                               self._dp, self._show_menu)

        self._loader_movie = QMovie(":/images/loader.gif")
        self._ui.loader_label.setMovie(self._loader_movie)
        self._set_fonts()

        self._ui.add_frame.setVisible(False)
        self._set_add_button_background(self.ADD_BUTTON_PASSIVE_COLOR)
        self._ui.add_button.clicked.connect(self._on_add_button_clicked)
        self._ui.add_button.setVisible(False)
        self._ui.close_button.clicked.connect(self._on_close_button_clicked)
        self._ui.refresh_button.clicked.connect(self._on_refresh)

        self._line_edit_style = "background-color: {};"
        self._ui.error_label.setStyleSheet("color: {};".format(
            self.ERROR_COLOR))

    def _set_fonts(self):
        ui = self._ui
        controls = [
            ui.colleagues_label, ui.mail_edit, ui.edit_radio, ui.view_radio,
            ui.add_button
        ]

        for control in controls:
            font = control.font()
            font_size = control.font().pointSize() * self._dp
            if font_size > 0:
                control_font = QFont(font.family(), font_size)
                control_font.setBold(font.bold())
                control.setFont(control_font)

    def show(self):
        logger.debug("Opening collaboration settings dialog")

        screen_width = QApplication.desktop().width()
        parent_x = self._dialog.parent().x()
        parent_width = self._dialog.parent().width()
        width = self._dialog.width()
        offset = 16
        if parent_x + parent_width / 2 > screen_width / 2:
            x = parent_x - width - offset
            if x < 0:
                x = 0
        else:
            x = parent_x + parent_width + offset
            diff = x + width - screen_width
            if diff > 0:
                x -= diff
        self._dialog.move(x, self._dialog.parent().y())

        # Execute dialog
        self._dialog.raise_()
        self.show_cursor_loading(True)
        self._dialog.exec_()

    def close(self):
        self._dialog.reject()

    def show_cursor_loading(self, show_movie=False):
        if show_movie:
            self._ui.stackedWidget.setCurrentIndex(1)
            self._loader_movie.start()
        else:
            self._dialog.setCursor(Qt.WaitCursor)
            self._parent_window.setCursor(Qt.WaitCursor)

    def show_cursor_normal(self):
        self._dialog.setCursor(Qt.ArrowCursor)
        self._parent_window.setCursor(Qt.ArrowCursor)
        if self._loader_movie.state() == QMovie.Running:
            self._loader_movie.stop()

    def show_colleagues(self):
        if not self._colleagues:
            self._ui.stackedWidget.setCurrentIndex(2)
        else:
            self._ui.stackedWidget.setCurrentIndex(0)
            self._colleagues_list.show_colleagues(self._colleagues)
        self.show_cursor_normal()

    def set_owner(self, is_owner):
        self._is_owner = is_owner
        self._ui.add_button.setVisible(self._is_owner)

    def _on_add_button_clicked(self):
        if self._ui.add_frame.isVisible():
            if not self._validate_email():
                return

            to_edit = self._ui.edit_radio.isChecked()
            self._ui.add_frame.setVisible(False)
            self._set_add_button_background(self.ADD_BUTTON_PASSIVE_COLOR)
            self._parent.add_colleague(self._ui.mail_edit.text(), to_edit)
        else:
            self._ui.add_frame.setVisible(True)
            self._set_add_button_background(self.ADD_BUTTON_ACTIVE_COLOR)
            self._ui.mail_edit.setText("")

    def _set_add_button_background(self, color):
        self._ui.add_button.setStyleSheet(
            'background-color: {}; color: #fff; '
            'border-radius: 4px; font: bold "Gargi"'.format(color))

    def _on_close_button_clicked(self):
        self._ui.add_frame.setVisible(False)
        self._set_add_button_background(self.ADD_BUTTON_PASSIVE_COLOR)
        self._clear_error()
        self._ui.mail_edit.setText("")

    def _validate_email(self):
        email_control = self._ui.mail_edit
        email_control.setStyleSheet(
            self._line_edit_style.format(self.LINE_EDIT_NORMAL_COLOR))
        regex = '^.+@.{2,}$'

        email_control.setText(email_control.text().strip())
        if not re.match(regex, email_control.text()):
            self._ui.error_label.setText(tr("Please enter a valid e-mail"))
            email_control.setStyleSheet(
                self._line_edit_style.format(self.ERROR_COLOR))
            email_control.setFocus()
            return False

        self._clear_error()
        return True

    def _clear_error(self):
        self._ui.error_label.setText("")
        self._ui.mail_edit.setStyleSheet(
            self._line_edit_style.format(self.LINE_EDIT_NORMAL_COLOR))

    def _on_refresh(self):
        self.show_cursor_loading()
        self._parent.query_collaboration_info()

    def _show_menu(self, colleague, pos):
        if not self._is_owner and not colleague.is_you or colleague.is_deleting:
            return

        menu = QMenu(self._ui.colleagues_list)
        menu.setStyleSheet("background-color: #EFEFF4; ")
        if colleague.is_you:
            if colleague.is_owner:
                action = menu.addAction(tr("Quit collaboration"))
                action.triggered.connect(self._on_quit_collaboration)
            else:
                action = menu.addAction(tr("Leave collaboration"))
                action.triggered.connect(self._on_leave_collaboration)
        else:
            rights_group = QActionGroup(menu)
            rights_group.setExclusive(True)

            menu.addSection(tr("Access rights"))
            action = menu.addAction(tr("Can view"))
            action.setCheckable(True)
            rights_action = rights_group.addAction(action)
            rights_action.setData(False)
            rights_action.setChecked(not colleague.can_edit)
            action = menu.addAction(tr("Can edit"))
            action.setCheckable(True)
            rights_action = rights_group.addAction(action)
            rights_action.setChecked(colleague.can_edit)
            rights_action.setData(True)
            rights_group.triggered.connect(
                lambda a: self._on_grant_edit(colleague, a))
            menu.addSeparator()

            action = menu.addAction(tr("Remove user"))
            action.triggered.connect(lambda: self._on_remove_user(colleague))

        pos_to_show = QPoint(pos.x(), pos.y() + 10)
        menu.exec_(pos_to_show)

    def _on_quit_collaboration(self):
        alert_str = "Collaboration will be cancelled, " \
                    "collaboration folder will be deleted " \
                    "from all colleagues' Pvtbox secured sync folders " \
                    "on all nodes."
        if self._user_confirmed_action(alert_str):
            self._parent.cancel_collaboration()

    def _on_leave_collaboration(self):
        alert_str = "Collaboration folder will be deleted " \
                    "from Pvtbox secured sync folders " \
                    "on all your nodes."
        if self._user_confirmed_action(alert_str):
            self._parent.leave_collaboration()

    def _on_remove_user(self, colleague):
        alert_str = "Colleague {} will be removed from collaboration. " \
                    "Collaboration folder will be deleted from colleague's " \
                    "Pvtbox secured sync folders on all nodes." \
            .format(colleague.email)
        if self._user_confirmed_action(alert_str):
            self._parent.remove(colleague.id)

    def _on_grant_edit(self, colleague, action):
        to_edit = action.data()
        self._parent.grant_edit(colleague.id, to_edit)

    def _user_confirmed_action(self, alert_str):
        msg = tr("<b>Are</b> you <b>sure</b>?<br><br>{}".format(alert_str))
        user_answer = msgbox(msg,
                             title=' ',
                             buttons=[
                                 (tr('Cancel'), 'Cancel'),
                                 (tr('Yes'), 'Yes'),
                             ],
                             parent=self._dialog,
                             default_index=0,
                             enable_close_button=True)

        return user_answer == 'Yes'
class TransfersDialog(object):
    FILE_LIST_ITEM_SIZE = 88
    CURRENT_TASK_STATES = {
        DOWNLOAD_STARTING, DOWNLOAD_LOADING, DOWNLOAD_FINISHING,
        DOWNLOAD_FAILED
    }
    ERROR_STATES = {DOWNLOAD_NO_DISK_ERROR}
    STATE_NOTIFICATIONS = {
        DOWNLOAD_NOT_READY: tr("Waiting for nodes..."),
        DOWNLOAD_READY: tr("Waiting for other downloads..."),
        DOWNLOAD_STARTING: tr("Starting download..."),
        DOWNLOAD_LOADING: tr("Downloading..."),
        DOWNLOAD_FINISHING: tr("Finishing download..."),
        DOWNLOAD_FAILED: tr("Download failed"),
        DOWNLOAD_NO_DISK_ERROR: tr("Insufficient disk space"),
    }

    WORKING = 0
    PAUSED = 1
    RESUMING = 2
    PAUSED_NOTIFICATIONS = {
        PAUSED: tr("Paused..."),
        RESUMING: tr("Resuming..."),
    }

    def __init__(self,
                 parent,
                 revert_downloads,
                 pause_resume_clicked,
                 add_to_sync_folder,
                 handle_link,
                 transfers_ready,
                 paused,
                 dp=None,
                 speed_chart_capacity=0,
                 download_speeds=(),
                 upload_speeds=(),
                 signalserver_address=''):
        self._dialog = QDialog(parent)
        self._dp = dp
        self._revert_downloads = revert_downloads
        self._pause_resume_clicked = pause_resume_clicked
        self._add_to_sync_folder = add_to_sync_folder
        self._handle_link = handle_link
        self._transfers_ready = transfers_ready
        self._parent = parent
        self._signalserver_address = signalserver_address

        self._dialog.setWindowIcon(QIcon(':/images/icon.png'))
        self._ui = Ui_Dialog()
        self._ui.setupUi(self._dialog)

        self._reverted_downloads = set()
        self._downloads_items = defaultdict(list)
        self._uploads_items = defaultdict(list)
        self._http_downloads = set()

        self._paused_state = self.WORKING if not paused else self.PAUSED

        self._total_files = 0
        self._total_size = 0

        self._init_ui()
        self._init_charts(download_speeds, upload_speeds, speed_chart_capacity)

    def _init_ui(self):
        self._icon_urls = {
            'add_file': [
                ':/images/transfers/add_file.svg',
                ':/images/transfers/add_file_hovered.svg'
            ],
            'link_insert': [
                ':/images/transfers/link_insert.svg',
                ':/images/transfers/link_insert_hovered.svg'
            ],
            'revert':
            [':/images/revert.svg', ':/images/transfers/revert_clicked.svg'],
            'pause': [':/images/pause.svg', ':/images/pause_hovered.svg'],
            'play': [':/images/play.svg', ':/images/play_hovered.svg'],
        }

        ui = self._ui
        self._dialog.setWindowFlags(Qt.Dialog)
        self._dialog.setAttribute(Qt.WA_TranslucentBackground)
        self._dialog.setAttribute(Qt.WA_MacFrameworkScaled)

        self._set_file_list_options(ui.downloads_list)
        self._set_file_list_options(ui.uploads_list)
        ui.downloads_list.verticalScrollBar().valueChanged.connect(
            self.on_downloads_scroll_changed)
        ui.uploads_list.verticalScrollBar().valueChanged.connect(
            self.on_uploads_scroll_changed)

        self._old_main_resize_event = ui.centralwidget.resizeEvent
        ui.centralwidget.resizeEvent = self._main_resize_event

        self._set_fonts()

        ui.add_button.enterEvent = lambda _: \
            self._enter_leave(ui.add_button, 'add_file')
        ui.add_button.leaveEvent = lambda _: \
            self._enter_leave(ui.add_button, 'add_file', False)
        ui.insert_link_button.enterEvent = lambda _: \
            self._enter_leave(ui.insert_link_button, 'link_insert')
        ui.insert_link_button.leaveEvent = lambda _: \
            self._enter_leave(ui.insert_link_button, 'link_insert', False)
        ui.revert_all_button.enterEvent = lambda _: \
            self._enter_leave(ui.revert_all_button, 'revert')
        ui.revert_all_button.leaveEvent = lambda _: \
            self._enter_leave(ui.revert_all_button, 'revert', False)
        ui.pause_all_button.enterEvent = lambda _: \
            self._enter_leave(ui.pause_all_button,
                              'play' if self._paused_state == self.PAUSED
                              else 'pause')
        ui.pause_all_button.leaveEvent = lambda _: \
            self._enter_leave(ui.pause_all_button,
                              'play' if self._paused_state == self.PAUSED
                              else 'pause', False)

        if self._paused_state == self.PAUSED:
            ui.pause_all_button.setText(tr("Resume all"))
            ui.pause_all_button.setIcon(QIcon(":/images/play.svg"))
        else:
            ui.pause_all_button.setText(tr("Pause all   "))
            ui.pause_all_button.setIcon(QIcon(":/images/pause.svg"))

    def _init_charts(self, download_speeds, upload_speeds,
                     speed_chart_capacity):
        self._last_downloads_speeds = deque(download_speeds,
                                            maxlen=speed_chart_capacity)
        self._last_uploads_speeds = deque(upload_speeds,
                                          maxlen=speed_chart_capacity)
        max_download_speed = max(self._last_downloads_speeds) \
            if self._last_downloads_speeds else 0
        max_upload_speed = max(self._last_uploads_speeds) \
            if self._last_uploads_speeds else 0
        max_speed = max(max_download_speed, max_upload_speed)
        self._download_speed_chart = SpeedChart(
            self._ui.downloads_speed_widget,
            speed_chart_capacity,
            QColor("green"),
            speeds=download_speeds,
            dp=self._dp,
            max_speed=max_speed)
        self._upload_speed_chart = SpeedChart(self._ui.uploads_speed_widget,
                                              speed_chart_capacity,
                                              QColor("orange"),
                                              speeds=upload_speeds,
                                              is_upload=True,
                                              dp=self._dp,
                                              max_speed=max_speed)

    def on_size_speed_changed(self, download_speed, download_size,
                              upload_speed, upload_size):
        self._ui.download_speed_value.setText(
            tr("{}/s").format(format_with_units(download_speed)))
        self._ui.download_size_value.setText(format_with_units(download_size))

        self._ui.upload_speed_value.setText(
            tr("{}/s").format(format_with_units(upload_speed)))
        self._ui.upload_size_value.setText(format_with_units(upload_size))

    def on_downloads_info_changed(self, downloads_info, supress_paused=False):
        logger.verbose("Updating downloads_info")
        self._update_downloads_list(downloads_info, supress_paused)
        self._transfers_ready()

    def on_downloads_state_changed(self, changed_info):
        if self._paused_state == self.PAUSED:
            self._transfers_ready()
            return

        elif self._paused_state == self.RESUMING:
            self._paused_state = self.WORKING

        logger.verbose("Changing downloads state with %s", changed_info)
        for obj_id in changed_info:
            items = self._downloads_items.get(obj_id, [])
            for item in items:
                self._change_item_widget(self._ui.downloads_list, item,
                                         changed_info[obj_id]["state"],
                                         changed_info[obj_id]["downloaded"])

        self._transfers_ready()

    def on_uploads_info_changed(self, uploads_info):
        logger.verbose("Updating uploads_info")
        self._update_uploads_list(uploads_info)
        self._transfers_ready()

    def on_uploads_state_changed(self, changed_info):
        logger.verbose("Changing uploads state with %s", changed_info)
        for obj_id in changed_info:
            items = self._uploads_items.get(obj_id, [])
            for item in items:
                self._change_item_widget(self._ui.uploads_list, item,
                                         changed_info[obj_id]["state"],
                                         changed_info[obj_id]["uploaded"])

        self._transfers_ready()

    def refresh_time_deltas(self):
        self._refresh_file_list_time_deltas(self._ui.downloads_list,
                                            self._downloads_items)
        self._refresh_file_list_time_deltas(self._ui.uploads_list,
                                            self._uploads_items)

    def show(self, on_finished):
        def finished():
            self._dialog.finished.disconnect(finished)
            self._ui.pause_all_button.clicked.disconnect(pause_all)
            self._ui.revert_all_button.clicked.disconnect(revert_all)
            self._ui.add_button.clicked.disconnect(add)
            self._ui.insert_link_button.clicked.disconnect(insert_link)
            on_finished()

        def pause_all():
            self._toggle_paused_state()

        def revert_all():
            if self._downloads_items and \
                    not self._has_user_confirmed_revert():
                return

            self._revert_all()

        def add():
            self._on_add_to_sync_folder()

        def insert_link():
            self._on_insert_link()

        logger.debug("Opening transfers dialog")

        screen_width = QApplication.desktop().width()
        parent_x = self._dialog.parent().x()
        parent_width = self._dialog.parent().width()
        width = self._dialog.width()
        offset = 16
        if parent_x + parent_width / 2 > screen_width / 2:
            x = parent_x - width - offset
            if x < 0:
                x = 0
        else:
            x = parent_x + parent_width + offset
            diff = x + width - screen_width
            if diff > 0:
                x -= diff
        self._dialog.move(x, self._dialog.parent().y())

        self._dialog.setAcceptDrops(True)
        self._dialog.dragEnterEvent = self._drag_enter_event
        self._dialog.dropEvent = self._drop_event

        # Execute dialog
        self._dialog.finished.connect(finished)
        self._ui.pause_all_button.clicked.connect(pause_all)
        self._ui.revert_all_button.clicked.connect(revert_all)
        self._ui.add_button.clicked.connect(add)
        self._ui.insert_link_button.clicked.connect(insert_link)
        self._dialog.raise_()
        self._dialog.show()

    def raise_dialog(self):
        self._dialog.raise_()

    def close(self):
        self._dialog.reject()

    def revert_failed(self, failed_uuids):
        self._reverted_downloads.difference_update(set(failed_uuids))

    def set_nodes_num(self, nodes_num):
        self._dialog.setWindowTitle(
            tr("Transfers - {} peer(s) connected").format(nodes_num))

    def show_all_disconnected_alert(self):
        self._dialog.setWindowTitle(
            tr("Transfers - Connect more devices to sync"))

    def _get_downloads_obj_ids_sorted(self, downloads_info):
        def sort_key(obj_id):
            info = downloads_info[obj_id]
            return -info['priority'] * 10000 - \
                   (info['downloaded'] - info['size']) // (64 * 1024)

        current_tasks = []
        ready_tasks = []
        not_ready_tasks = []
        for obj_id, info in downloads_info.items():
            state = info["state"]
            if state in self.CURRENT_TASK_STATES:
                current_tasks.append(obj_id)
            elif state == DOWNLOAD_READY:
                ready_tasks.append(obj_id)
            else:
                not_ready_tasks.append(obj_id)
        ready_tasks.sort(key=sort_key)
        not_ready_tasks.sort(key=sort_key)
        obj_ids_sorted = current_tasks + ready_tasks + not_ready_tasks
        return obj_ids_sorted

    def _update_downloads_list(self, downloads_info, supress_paused=False):
        if self._paused_state == self.PAUSED and not supress_paused:
            return

        elif self._paused_state == self.RESUMING:
            self._paused_state = self.WORKING

        obj_ids_sorted = self._get_downloads_obj_ids_sorted(downloads_info)
        self._downloads_items.clear()
        self._http_downloads.clear()
        self._ui.downloads_list.setUpdatesEnabled(False)
        self._total_size = 0
        self._total_files = 0
        index = 0
        for obj_id in obj_ids_sorted:
            if obj_id in self._reverted_downloads:
                continue

            info = downloads_info[obj_id]
            for file_info in info["files_info"]:
                self._add_file_to_file_list(
                    index,
                    self._ui.downloads_list,
                    self._downloads_items,
                    obj_id,
                    rel_path=file_info["target_file_path"],
                    created_time=file_info["mtime"],
                    was_updated=not file_info.get("is_created", True),
                    is_deleted=file_info.get("is_deleted"),
                    transfered=info["downloaded"],
                    size=info["size"],
                    state=info["state"],
                    is_file=info["is_file"])
                self._total_size += info["size"]
                self._total_files += 1
                index += 1

        for i in range(index, self._ui.downloads_list.count()):
            item = self._ui.downloads_list.takeItem(index)
            self._ui.downloads_list.removeItemWidget(item)

        self._reverted_downloads.intersection_update(set(obj_ids_sorted))

        self._update_totals()
        self._set_revert_all_enabled()
        self._set_current_downloads_page()
        self._ui.downloads_list.setUpdatesEnabled(True)

    def _update_totals(self):
        self._ui.total_files_label.setText(
            tr("{} file(s)").format(self._total_files))
        self._ui.total_size_label.setText(format_with_units(self._total_size))

    def _update_uploads_list(self, uploads_info):
        self._uploads_items.clear()
        self._ui.uploads_list.setUpdatesEnabled(False)
        total_files = 0
        index = 0
        for obj_id in uploads_info:
            info = uploads_info[obj_id]
            for file_info in info["files_info"]:
                self._add_file_to_file_list(
                    index,
                    self._ui.uploads_list,
                    self._uploads_items,
                    obj_id,
                    rel_path=file_info["target_file_path"],
                    created_time=file_info["mtime"],
                    was_updated=not file_info.get("is_created", True),
                    is_deleted=file_info.get("is_deleted"),
                    transfered=info["uploaded"],
                    size=info["size"],
                    state=info["state"],
                    is_file=info["is_file"])
                total_files += 1
                index += 1

        for i in range(index, self._ui.uploads_list.count()):
            item = self._ui.uploads_list.takeItem(index)
            self._ui.uploads_list.removeItemWidget(item)

        self._set_current_uploads_page()
        self._ui.uploads_list.setUpdatesEnabled(True)

    def _set_fonts(self):
        ui = self._ui
        controls = [ui.no_downloads_label, ui.no_uploads_label]
        controls.extend([c for c in ui.downloads_frame.findChildren(QLabel)])
        controls.extend([c for c in ui.downloads_bottom.findChildren(QLabel)])
        controls.extend(
            [c for c in ui.downloads_bottom.findChildren(QPushButton)])
        controls.extend([c for c in ui.uploads_frame.findChildren(QLabel)])
        controls.extend(
            [c for c in ui.uploads_bottom.findChildren(QPushButton)])

        for control in controls:
            font = control.font()
            font_size = control.font().pointSize() * self._dp
            if font_size > 0:
                control.setFont(QFont(font.family(), font_size))

    def _enter_leave(self, button, icon_str, entered=True):
        icon_url = self._icon_urls[icon_str][int(entered)]
        button.setIcon(QIcon(icon_url))

    def _set_file_list_options(self, file_list):
        file_list.setFocusPolicy(Qt.NoFocus)
        file_list.setFont(QFont('Nano', 10 * self._dp))
        # file_list.setGridSize(QSize(
        #     self.FILE_LIST_ITEM_SIZE, self.FILE_LIST_ITEM_SIZE - 14))
        file_list.setResizeMode(QListView.Adjust)
        file_list.setAutoScroll(False)
        file_list.setUniformItemSizes(True)

    def _add_file_to_file_list(self,
                               index,
                               file_list,
                               items_dict,
                               obj_id,
                               rel_path,
                               created_time,
                               was_updated,
                               is_deleted,
                               transfered,
                               size=0,
                               state=None,
                               is_file=True):
        item = file_list.item(index)
        if item:
            item.setData(Qt.UserRole, [
                rel_path, created_time, size, was_updated, is_deleted,
                transfered, state, is_file, obj_id
            ])
            self._update_file_list_item_widget(file_list, item)
            items_dict[obj_id].append(item)
            return

        item = QListWidgetItem()
        item.setFlags(item.flags() & ~Qt.ItemIsSelectable)
        item.setSizeHint(QSize(file_list.width(), self.FILE_LIST_ITEM_SIZE))
        item.setData(Qt.UserRole, [
            rel_path, created_time, size, was_updated, is_deleted, transfered,
            state, is_file, obj_id
        ])

        file_list.addItem(item)
        rect = file_list.viewport().contentsRect()
        top = file_list.indexAt(rect.topLeft())
        if top.isValid():
            bottom = file_list.indexAt(rect.bottomLeft())
            if not bottom.isValid():
                bottom = file_list.model().index(file_list.count() - 1)
            if top.row() <= file_list.row(item) <= bottom.row() + 1:
                widget = self._create_file_list_item_widget(
                    file_list, [
                        rel_path, created_time, size, was_updated, is_deleted,
                        transfered, state, is_file, obj_id
                    ])
                file_list.setItemWidget(item, widget)
        if item not in items_dict[obj_id]:
            items_dict[obj_id].append(item)

    def on_downloads_scroll_changed(self, *args, **kwargs):
        self._on_list_scroll_changed(self._ui.downloads_list)

    def on_uploads_scroll_changed(self, *args, **kwargs):
        self._on_list_scroll_changed(self._ui.uploads_list)

    def _on_list_scroll_changed(self, file_list):
        rect = file_list.viewport().contentsRect()
        top = file_list.indexAt(rect.topLeft())
        if top.isValid():
            bottom = file_list.indexAt(rect.bottomLeft())
            if not bottom.isValid():
                bottom = file_list.model().index(file_list.count() - 1)
            for index in range(top.row(), bottom.row() + 1):
                item = file_list.item(index)
                widget = file_list.itemWidget(item)
                if widget:
                    continue
                widget = self._create_file_list_item_widget(
                    file_list, item.data(Qt.UserRole))
                file_list.setItemWidget(item, widget)

    def _create_file_list_item_widget(self, file_list, data):
        rel_path, created_time, \
        size, was_updated, is_deleted, \
        transfered, state, is_file, obj_id = data
        is_upload = state is None  # uploads list
        is_shared = not is_upload and created_time == 0
        is_http_download = not is_upload and created_time < 0
        if is_http_download:
            self._http_downloads.add(obj_id)

        widget = QWidget(parent=file_list)
        widget.setFixedHeight(self.FILE_LIST_ITEM_SIZE)

        main_layout = QVBoxLayout(widget)
        main_layout.setSpacing(2)

        file_name_label = QLabel(widget)
        file_name_label.setObjectName("file_name_label")
        file_name_label.setFixedWidth(max(file_list.width() - 80, 320))
        file_name_label.setFixedHeight(20)
        file_name_label.setFont(QFont('Noto Sans', 10 * self._dp))
        file_name_label.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        file_name_label.setText(elided(rel_path, file_name_label))
        main_layout.addWidget(file_name_label)

        time_size_revert_layout = QHBoxLayout()
        time_size_revert_layout.setSpacing(0)
        main_layout.addLayout(time_size_revert_layout)

        time_size_layout = QVBoxLayout()
        time_size_layout.setSpacing(0)
        time_size_revert_layout.addLayout(time_size_layout)
        time_size_revert_layout.addStretch()

        time_delta_label = QLabel(widget)
        time_delta_label.setObjectName("time_delta_label")
        if is_shared:
            time_delta_label.setText(tr("Shared file"))
        elif is_http_download:
            time_delta_label.setText(tr("Uploaded from web"))
        else:
            try:
                time_delta_label.setText(
                    get_added_time_string(created_time, was_updated,
                                          is_deleted))
            except RuntimeError:
                pass
        time_delta_label.setFont(QFont('Noto Sans', 8 * self._dp))
        time_delta_label.setMinimumHeight(14)
        time_delta_label.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        time_delta_label.setStyleSheet('color: #A792A9;')
        time_size_layout.addWidget(time_delta_label)

        is_created = not was_updated and not is_deleted and not is_shared
        if not is_upload:
            revert_button = QPushButton(widget)
            revert_button.is_entered = False
            revert_button.setObjectName("revert_button")
            revert_button.setFlat(True)
            revert_button.setChecked(True)
            revert_button.setFont(QFont("Noto Sans", 8 * self._dp,
                                        italic=True))
            revert_button.setMouseTracking(True)
            revert_button.setCursor(Qt.PointingHandCursor)
            self._set_revert_button_options(revert_button, obj_id, is_created,
                                            is_shared, is_http_download,
                                            is_file, rel_path, size)

            time_size_revert_layout.addWidget(revert_button,
                                              alignment=Qt.AlignVCenter)
            spacerItem = QSpacerItem(6, 10, QSizePolicy.Maximum,
                                     QSizePolicy.Minimum)
            time_size_layout.addItem(spacerItem)

        size_layout = QHBoxLayout()
        size_layout.setSpacing(0)
        time_size_layout.addLayout(size_layout)
        self._set_size_layout(size_layout, widget, transfered, size, is_upload)

        if is_upload:
            spacerItem = QSpacerItem(6, 6, QSizePolicy.Maximum,
                                     QSizePolicy.Minimum)
            main_layout.addItem(spacerItem)
        else:
            progress_layout = QHBoxLayout()
            progress_layout.setSpacing(6)
            main_layout.addLayout(progress_layout)
            self._set_progress_layout(progress_layout, widget, transfered,
                                      size, state)

        if is_upload:
            return widget

        def enter(_):
            revert_button.is_entered = True
            is_created = revert_button.property("properties")[0]
            color = '#f9af61' if not is_created else 'red'
            revert_button.setStyleSheet(
                'QPushButton {{margin: 0;border: 0; text-align:right center;'
                'color: {0};}} '
                'QPushButton:!enabled {{color: #aaaaaa;}} '
                'QToolTip {{background-color: #222222; color: white;}}'.format(
                    color))
            revert_button.setIcon(
                QIcon(':images/transfers/{}_active.svg'.format(
                    revert_button.text().strip().lower())))

        def leave(_):
            revert_button.is_entered = False
            revert_button.setStyleSheet(
                'QPushButton {margin: 0;border: 0; text-align:right center;'
                'color: #333333;} '
                'QPushButton:!enabled {color: #aaaaaa;}')
            revert_button.setIcon(
                QIcon(':images/transfers/{}_inactive.svg'.format(
                    revert_button.text().strip().lower())))

        revert_button.enterEvent = enter
        revert_button.leaveEvent = leave

        def revert_button_clicked():
            is_created, is_shared, is_file, rel_path, obj_id, size = \
                revert_button.property("properties")
            color = '#f78d1e' if not is_created else '#e50000'
            revert_button.setStyleSheet(
                'margin: 0; border: 0; text-align:right center;'
                'color: {};'.format(color))
            revert_button.setIcon(
                QIcon(':images/transfers/{}_clicked.svg'.format(
                    revert_button.text().strip().lower())))
            if not self._has_user_confirmed_revert(rel_path, is_shared,
                                                   is_created):
                return

            self._reverted_downloads.add(obj_id)
            reverted_files = reverted_patches = reverted_shares = []
            if is_shared:
                reverted_shares = [obj_id]
            elif is_file:
                reverted_files = [obj_id]
            else:
                reverted_patches = [obj_id]
            self._revert_downloads(reverted_files, reverted_patches,
                                   reverted_shares)
            items = self._downloads_items.get(obj_id, [])
            for item in items:
                self._ui.downloads_list.takeItem(
                    self._ui.downloads_list.row(item))
            self._total_files = max(self._total_files - len(items), 0)
            self._total_size = max(self._total_size - size, 0)
            self._update_totals()
            self._set_revert_all_enabled()
            self._set_current_downloads_page()

        revert_button.clicked.connect(revert_button_clicked)

        return widget

    def _set_size_layout(self, size_layout, widget, transfered, size,
                         is_upload):
        direction_label = QLabel(widget)
        direction_label.setMinimumHeight(14)
        direction_text = '\u2191\u0020' if is_upload else '\u2193\u0020'
        direction_label.setText(direction_text)
        direction_label.setFont(QFont('Noto Sans', 8 * self._dp))
        direction_label.setAlignment(Qt.AlignRight | Qt.AlignTrailing
                                     | Qt.AlignVCenter)
        direction_label.setStyleSheet('color: #A792A9;')
        size_layout.addWidget(direction_label)

        transfered_label = QLabel(widget)
        transfered_label.setObjectName("transfered_label")
        transfered_label.setMinimumHeight(14)
        transfered_label.setText(format_with_units(transfered))
        transfered_label.setFont(QFont('Noto Sans', 8 * self._dp))
        transfered_label.setAlignment(Qt.AlignLeading | Qt.AlignLeft
                                      | Qt.AlignVCenter)
        transfered_label.setStyleSheet('color: #A792A9;')
        size_layout.addWidget(transfered_label)

        if not is_upload:
            slash_label = QLabel(widget)
            slash_label.setMinimumHeight(14)
            slash_label.setText('/')
            slash_label.setFont(QFont('Noto Sans', 8 * self._dp))
            slash_label.setAlignment(Qt.AlignRight | Qt.AlignTrailing
                                     | Qt.AlignVCenter)
            slash_label.setStyleSheet('color: #A792A9;')
            size_layout.addWidget(slash_label)

            size_label = QLabel(widget)
            size_label.setObjectName("size_label")
            size_label.setMinimumHeight(14)
            size_label.setText(format_with_units(size))
            size_label.setFont(QFont('Noto Sans', 8 * self._dp))
            size_label.setAlignment(Qt.AlignLeading | Qt.AlignLeft
                                    | Qt.AlignVCenter)
            size_label.setStyleSheet('color: #A792A9;')
            size_layout.addWidget(size_label)

        size_layout.addStretch()

    def _set_progress_layout(self, progress_layout, widget, transfered, size,
                             state):
        is_current = state in self.CURRENT_TASK_STATES
        is_error = state in self.ERROR_STATES

        progress_background = QStackedWidget(widget)
        progress_background.setObjectName("progress_background")
        progress_bar = QProgressBar(progress_background)
        progress_bar.setObjectName("progress_bar")
        progress_bar.setMinimum(0)
        progress_bar.setMaximum(size if is_current and state != DOWNLOAD_FAILED
                                and self._paused_state == self.WORKING else 0)
        if is_current:
            progress_bar.setValue(transfered)
        progress_bar.setTextVisible(False)

        progress_label = QLabel(widget)
        progress_label.setObjectName("progress_label")

        self._set_progress_bar_style(progress_bar, progress_background,
                                     progress_label, state, is_current,
                                     is_error)

        progress_background.addWidget(progress_bar)
        progress_layout.addWidget(progress_background,
                                  alignment=Qt.AlignVCenter)

        progress_label.setFont(QFont('Noto Sans', 7 * self._dp))
        progress_layout.addWidget(progress_label)
        spacerItem = QSpacerItem(6, 10, QSizePolicy.Maximum,
                                 QSizePolicy.Minimum)
        progress_layout.addItem(spacerItem)

    def _set_revert_button_options(self, revert_button, obj_id, is_created,
                                   is_shared, is_http_download, is_file,
                                   rel_path, size):
        revert_text = tr("Delete") if is_created \
            else tr('Revert') if not is_shared and not is_http_download \
            else tr("Cancel")
        revert_button.setText(revert_text + '  ')
        revert_button.setIcon(
            QIcon(':images/transfers/{}_{}.svg'.format(
                revert_button.text().strip().lower(),
                'active' if revert_button.is_entered else 'inactive')))
        tooltip_text = tr("Action disabled while sync paused") \
            if not is_http_download and self._paused_state == self.PAUSED \
            else tr("Delete file and cancel download") if is_created \
            else tr("Revert changes and cancel download") \
            if not is_shared and not is_http_download \
            else tr("Cancel shared file download") if is_shared \
            else tr("You can cancel upload from web panel")
        revert_button.setToolTip(tooltip_text)
        revert_button.setStyleSheet(
            'QPushButton {{margin: 0;border: 0; text-align:right center;'
            'color: {0};}} '
            'QPushButton:!enabled {{color: #aaaaaa;}}'.format(
                '#333333' if not revert_button.is_entered else
                '#f9af61' if not is_created else 'red'))
        revert_button.setEnabled(not is_http_download
                                 and self._paused_state != self.PAUSED)

        revert_button.setProperty(
            "properties",
            [is_created, is_shared, is_file, rel_path, obj_id, size])

    def _update_file_list_item_widget(self, file_list, item):
        rel_path, \
        created_time, \
        size, \
        was_updated, \
        is_deleted, \
        transfered, \
        state, \
        is_file, \
        obj_id = item.data(Qt.UserRole)

        is_upload = state is None  # uploads list
        is_shared = not is_upload and created_time == 0
        is_created = not was_updated and not is_deleted and not is_shared
        is_http_download = not is_upload and created_time < 0
        if is_http_download:
            self._http_downloads.add(obj_id)

        is_current = state in self.CURRENT_TASK_STATES
        is_error = state in self.ERROR_STATES

        widget = file_list.itemWidget(item)
        if not widget:
            return

        file_name_label = widget.findChildren(QLabel, "file_name_label")[0]
        file_name_label.setText(elided(rel_path, file_name_label))

        time_delta_label = widget.findChildren(QLabel, "time_delta_label")[0]
        if is_shared:
            time_delta_label.setText(tr("Shared file"))
        elif is_http_download:
            time_delta_label.setText(tr("Uploaded from web"))
        else:
            try:
                time_delta_label.setText(
                    get_added_time_string(created_time, was_updated,
                                          is_deleted))
            except RuntimeError:
                pass

        transfered_label = widget.findChildren(QLabel, "transfered_label")[0]
        transfered_label.setText(format_with_units(transfered))

        if is_upload:
            return

        size_label = widget.findChildren(QLabel, "size_label")[0]
        size_label.setText(format_with_units(size))

        revert_button = widget.findChildren(QPushButton, "revert_button")[0]
        self._set_revert_button_options(revert_button, obj_id, is_created,
                                        is_shared, is_http_download, is_file,
                                        rel_path, size)

        progress_bar = widget.findChildren(QProgressBar, "progress_bar")[0]
        progress_background = widget.findChildren(QStackedWidget,
                                                  "progress_background")[0]
        progress_bar.setValue(transfered)
        progress_bar.setMaximum(size if is_current and state != DOWNLOAD_FAILED
                                and self._paused_state == self.WORKING else 0)
        progress_label = widget.findChildren(QLabel, "progress_label")[0]
        self._set_progress_bar_style(progress_bar, progress_background,
                                     progress_label, state, is_current,
                                     is_error)

    def _change_item_widget(self,
                            file_list,
                            item,
                            state=None,
                            transfered=None):
        rel_path, \
        created_time, \
        size, \
        was_updated, \
        is_deleted, \
        old_transfered, \
        old_state, \
        is_file, \
        obj_id = item.data(Qt.UserRole)

        if transfered is None:
            state = old_state
            transfered = old_transfered
            is_upload = False
        else:
            is_upload = state is None
            item.setData(Qt.UserRole, [
                rel_path, created_time, size, was_updated, is_deleted,
                transfered, state, is_file, obj_id
            ])

        widget = file_list.itemWidget(item)
        if not widget:
            return

        is_shared = not is_upload and created_time == 0
        is_created = not was_updated and not is_deleted and not is_shared
        is_http_download = not is_upload and created_time < 0

        children = widget.findChildren(QLabel, "transfered_label")
        if not children or len(children) > 1:
            logger.warning("Can't find transfered_label for %s", rel_path)
        else:
            transfered_label = children[0]
            transfered_label.setText(format_with_units(transfered))

        if is_upload:
            return

        is_current = state in self.CURRENT_TASK_STATES
        is_error = state in self.ERROR_STATES

        children = widget.findChildren(QProgressBar, "progress_bar")
        back_children = widget.findChildren(QStackedWidget,
                                            "progress_background")
        if not children or len(children) > 1 or \
                not back_children or len(back_children) > 1:
            logger.warning("Can't find progress_bar for %s", rel_path)
            return

        progress_background = back_children[0]
        progress_bar = children[0]
        progress_bar.setValue(transfered)
        progress_bar.setMaximum(size if is_current and state != DOWNLOAD_FAILED
                                and self._paused_state == self.WORKING else 0)

        children = widget.findChildren(QLabel, "progress_label")
        if not children or len(children) > 1:
            logger.warning("Can't find progress_label for %s", rel_path)
            return

        progress_label = children[0]
        self._set_progress_bar_style(progress_bar, progress_background,
                                     progress_label, state, is_current,
                                     is_error)

        revert_button = widget.findChildren(QPushButton, "revert_button")[0]
        self._set_revert_button_options(revert_button, obj_id, is_created,
                                        is_shared, is_http_download, is_file,
                                        rel_path, size)

    def _set_progress_bar_style(self, progress_bar, progress_background,
                                progress_label, state, is_current, is_error):
        progress_active = is_current and self._paused_state == self.WORKING
        if progress_active:
            progress_background.setStyleSheet("background-color: #cceed6")
            progress_bar.setStyleSheet("QProgressBar::chunk {"
                                       "background-color: #01AB33;"
                                       "}")
            progress_background.setFixedHeight(2)
            progress_bar.setFixedHeight(2)
        elif is_error:
            progress_background.setStyleSheet("background-color: #red")
            progress_bar.setStyleSheet("QProgressBar::chunk {"
                                       "background-color: #ffcccb;"
                                       "}")
            progress_background.setFixedHeight(1)
            progress_bar.setFixedHeight(1)
        else:
            progress_background.setStyleSheet("background-color: #d6d6d6")
            progress_bar.setStyleSheet("QProgressBar::chunk {"
                                       "background-color: #777777;"
                                       "}")
            progress_background.setFixedHeight(1)
            progress_bar.setFixedHeight(1)

        progress_text = self.STATE_NOTIFICATIONS[state] \
            if self._paused_state == self.WORKING or is_error \
            else self.PAUSED_NOTIFICATIONS[self._paused_state]
        progress_label.setText(progress_text)
        progress_label.setStyleSheet(
            "color: #01AB33" if progress_active else
            "color: #A792A9;" if not is_error else "color: red;")

    def _refresh_file_list_time_deltas(self, file_list, items):
        for obj_id in items:
            for item in items.get(obj_id, []):
                self._refresh_item_time_delta(file_list, item)

    def _refresh_item_time_delta(self, file_list, item):
        rel_path, \
        created_time, \
        size, \
        was_updated, \
        is_deleted, \
        transfered, \
        state, \
        is_file, \
        obj_id = item.data(Qt.UserRole)

        is_upload = state is None  # uploads list
        is_shared = not is_upload and created_time == 0
        is_http_download = not is_upload and created_time < 0
        if is_shared or is_http_download:
            return

        widget = file_list.itemWidget(item)
        if not widget:
            return
        children = widget.findChildren(QLabel, "time_delta_label")
        if not children or len(children) > 1:
            logger.warning("Can't find time_delta_label for %s", rel_path)
        else:
            time_delta_label = children[0]
            try:
                time_delta_label.setText(
                    get_added_time_string(created_time, was_updated,
                                          is_deleted))
            except RuntimeError:
                pass

    def _revert_all(self):
        logger.verbose("Revert downloads")
        reverted_files = reverted_patches = reverted_shares = []
        for obj_id in list(self._downloads_items.keys()):
            if obj_id in self._reverted_downloads:
                continue

            items = self._downloads_items.get(obj_id, [])
            if not items:
                logger.warning("No items for obj_id %s", obj_id)
                continue
            first_item = items[0]

            rel_path, \
            created_time, \
            size, \
            was_updated, \
            is_deleted, \
            transfered, \
            state, \
            is_file, \
            old_obj_id = first_item.data(Qt.UserRole)
            is_shared = created_time == 0
            is_http_download = created_time < 0
            if is_http_download:
                continue

            if is_shared:
                reverted_shares.append(obj_id)
            elif is_file:
                reverted_files.append(obj_id)
            else:
                reverted_patches.append(obj_id)
            self._reverted_downloads.add(obj_id)
            self._total_files = max(self._total_files - len(items), 0)
            self._total_size = max(self._total_size - size, 0)
            for item in self._downloads_items[obj_id]:
                self._ui.downloads_list.takeItem(
                    self._ui.downloads_list.row(item))
            self._downloads_items.pop(obj_id, None)

        logger.verbose("Reverting downloads %s, %s, %s", reverted_files,
                       reverted_patches, reverted_shares)
        self._revert_downloads(reverted_files, reverted_patches,
                               reverted_shares)

        self._set_revert_all_enabled()
        self._update_totals()
        self._set_current_downloads_page()

    def _toggle_paused_state(self):
        self._pause_resume_clicked()

    def set_paused_state(self, paused=True):
        under_mouse = self._ui.pause_all_button.underMouse()
        if paused:
            self._paused_state = self.PAUSED
            self._ui.pause_all_button.setText(tr("Resume all"))
            self._enter_leave(self._ui.pause_all_button, 'play', under_mouse)
        else:
            self._paused_state = self.RESUMING
            self._ui.pause_all_button.setText(tr("Pause all   "))
            self._enter_leave(self._ui.pause_all_button, 'pause', under_mouse)

        self._set_revert_all_enabled()

        logger.verbose("Downloads %s",
                       self.PAUSED_NOTIFICATIONS[self._paused_state])
        for obj_id in self._downloads_items:
            items = self._downloads_items.get(obj_id, [])
            for item in items:
                self._change_item_widget(self._ui.downloads_list, item)

    def _has_user_confirmed_revert(self,
                                   file_path=None,
                                   is_share=False,
                                   is_created=False):
        if file_path:  # 1 file
            if is_share:
                msg_text = tr("Do you want to cancel shared file {} download?") \
                    .format(file_path)
            elif is_created:
                msg_text = tr("Do you want to delete file {} "
                              "from all your devices?").format(file_path)
            else:
                msg_text = tr("Do you want to revert last changes for file {} "
                              "on all your devices?").format(file_path)
        else:  # many files
            msg_text = tr(
                "Do you want to delete new files from all your devices,\n"
                "revert all last changes on all devices,\n"
                "and cancel shared files downloads?")

        userAnswer = msgbox(msg_text,
                            buttons=[
                                (tr('Yes'), 'Yes'),
                                (tr('No'), 'No'),
                            ],
                            parent=self._dialog,
                            default_index=1)
        return userAnswer == 'Yes'

    def _main_resize_event(self, e):
        self._old_main_resize_event(e)
        if e.oldSize().height() != self._ui.centralwidget.height():
            self.on_downloads_scroll_changed()
            self.on_uploads_scroll_changed()

        width = (self._ui.centralwidget.width() - 6) // 2
        self._ui.downloads_frame.setFixedWidth(width)
        self._ui.uploads_frame.setFixedWidth(width)
        if e.oldSize().width() == self._ui.centralwidget.width():
            return

        speed_charts_height = self._ui.downloads_speed_widget.width() * 0.3
        self._ui.downloads_speed_widget.setFixedHeight(speed_charts_height)
        self._download_speed_chart.resize()
        self._ui.uploads_speed_widget.setFixedHeight(speed_charts_height)
        self._upload_speed_chart.resize()

        self._file_list_resizeEvent(self._ui.downloads_list,
                                    self._downloads_items)
        self._file_list_resizeEvent(self._ui.uploads_list, self._uploads_items)

    def _file_list_resizeEvent(self, file_list, file_list_items):
        for items in file_list_items.values():
            for item in items:
                self._resize_item(item, file_list)

    def _resize_item(self, item, file_list):
        rel_path, \
        created_time, \
        size, \
        was_updated, \
        is_deleted, \
        transfered, \
        state, \
        is_file, \
        obj_id = item.data(Qt.UserRole)

        widget = file_list.itemWidget(item)
        if not widget:
            return
        widget.setMaximumWidth(file_list.width())
        children = widget.findChildren(QLabel, "file_name_label")
        if not children or len(children) > 1:
            logger.warning("Can't find file_name_label for %s", rel_path)
        else:
            file_name_label = children[0]
            file_name_label.setFixedWidth(max(file_list.width() - 80, 320))
            file_name_label.setText(elided(rel_path, file_name_label))

    def _set_revert_all_enabled(self):
        self._ui.revert_all_button.setEnabled(
            bool(set(self._downloads_items) - self._http_downloads)
            and self._paused_state != self.PAUSED)

    def _set_current_downloads_page(self):
        self._ui.downloads_pages.setCurrentIndex(
            0 if self._downloads_items else 1)

    def _set_current_uploads_page(self):
        self._ui.uploads_pages.setCurrentIndex(0 if self._uploads_items else 1)

    def update_speed_charts(self, download_speed, upload_speed):
        self._last_downloads_speeds.append(download_speed)
        self._last_uploads_speeds.append(upload_speed)
        max_speed = max(max(self._last_downloads_speeds),
                        max(self._last_uploads_speeds))
        self._download_speed_chart.update(download_speed, max_speed)
        self._upload_speed_chart.update(upload_speed, max_speed)

    def _on_add_to_sync_folder(self):
        logger.verbose("Add files to sync directory")
        title = tr('Choose files to copy to sync directory')
        selected_files_or_folders = QFileDialog.getOpenFileNames(
            self._dialog, title)[0]
        self._add_to_sync_folder(selected_files_or_folders)

    def _drag_enter_event(self, event):
        data = event.mimeData()
        if data.hasUrls():
            event.accept()
        else:
            event.ignore()

    def _drop_event(self, event):
        data = event.mimeData()
        dropped_files_or_folders = []
        if data.hasUrls():
            event.acceptProposedAction()
            event.accept()
            for url in data.urls():
                dropped_files_or_folders.append(url.toLocalFile())
            self._add_to_sync_folder(dropped_files_or_folders)
        else:
            event.ignore()

    def _on_insert_link(self):
        insert_link_dialog = InsertLinkDialog(self._dialog, self._dp,
                                              self._signalserver_address)
        link, is_shared = insert_link_dialog.show()
        logger.debug("link '%s'", link)
        if link:
            self._handle_link(link, is_shared)

    def set_signalserver_address(self, address):
        self._signalserver_address = address