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()
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 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))
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