class SupportDialog(QObject): SHORT_FEEDBACK_INTERVAL = 5 * 60 * 1000 DAYS_TO_FEEDBACK = 7 DROPDOWN_BACKGROUND_COLOR = "#f78d1e" DROPDOWN_COLOR = "white" _sending_error = Signal() SUBJECT = { 1: "TECHNICAL", 2: "OTHER", 3: "FEEDBACK", } def __init__(self, parent, parent_window, config, dp=1, selected_index=0): QObject.__init__(self, parent) self._parent = parent self._parent_window = parent_window self._config = config self._dp = dp self._selected_index = selected_index self._dialog = QDialog(parent_window) self._dialog.setWindowFlags(Qt.Dialog) self._dialog.setAttribute(Qt.WA_MacFrameworkScaled) self._is_opened = False self._pipe = None self._feedback_mode = False self._ui = Ui_Dialog() self._ui.setupUi(self._dialog) self._init_ui() self._old_close_event = self._dialog.closeEvent self._feedback_timer = QTimer(self) self._feedback_timer.setSingleShot(True) self._feedback_timer.timeout.connect(self._show_feedback_form) self._parent.service_started.connect(self._check_feedback_needed) self._parent.exit_request.connect(self._on_exit_request) def _init_ui(self): self._ui.pushButton.setEnabled(False) self._ui.comboBox.addItem(tr("---Select Subject---")) self._ui.comboBox.addItem(tr("Technical Question")) self._ui.comboBox.addItem(tr("Other Question")) self._ui.comboBox.addItem(tr("Feedback")) self._ui.comboBox.setCurrentIndex(self._selected_index) palette = self._ui.comboBox.palette() palette.setColor(QPalette.HighlightedText, QColor(self.DROPDOWN_COLOR)) palette.setColor(QPalette.Highlight, QColor(self.DROPDOWN_BACKGROUND_COLOR)) self._ui.comboBox.setPalette(palette) palette = self._ui.comboBox.view().palette() palette.setColor(QPalette.HighlightedText, QColor(self.DROPDOWN_COLOR)) palette.setColor(QPalette.Highlight, QColor(self.DROPDOWN_BACKGROUND_COLOR)) self._ui.comboBox.view().setPalette(palette) self._set_tooltip() self._ui.comboBox.currentIndexChanged.connect(self._on_index_changed) self._ui.plainTextEdit.textChanged.connect(self._set_tooltip) self._ui.pushButton.clicked.connect(self._on_send_clicked) self._ui.text_label.linkActivated.connect(self._on_link_activated) self._sending_error.connect(self._clear_pipe_state) self._set_fonts() def _set_fonts(self): ui = self._ui controls = [ ui.plainTextEdit, ui.pushButton, ui.comboBox, ui.text_label, ui.checkBox ] 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 set_selected_index(self, selected_index): self._selected_index = selected_index if self._is_opened: self._ui.comboBox.setCurrentIndex(self._selected_index) def show(self): if self._parent.dialogs_opened(): return self._is_opened = True logger.debug("Support dialog opening...") self._pipe = None self.set_selected_index(self._selected_index) self._ui.checkBox.setChecked(False) self._ui.comboBox.setEnabled(not self._feedback_mode) self._dialog.exec_() logger.debug("Support dialog closed") if self._pipe: try: self._pipe.stop() self._clear_pipe_state() except Exception as e: logger.error("Unexpected error stopping pipe: (%s)", e) self._is_opened = False self._selected_index = 0 self._ui.plainTextEdit.document().clear() self._ui.checkBox.setChecked(False) def dialog_opened(self): return self._is_opened def close(self): self._dialog.close() def _set_tooltip(self): if not self._selected_index: tooltip = tr("Please select subject") self._ui.pushButton.setEnabled(False) elif not self._ui.plainTextEdit.document().toPlainText(): tooltip = tr("Message can't be empty") self._ui.pushButton.setEnabled(False) else: tooltip = tr("Click to send message") self._ui.pushButton.setEnabled(True) self._ui.pushButton.setToolTip(tooltip) def _on_index_changed(self, selected_index): self._selected_index = selected_index self._set_tooltip() def _on_send_clicked(self): self._dialog.setEnabled(False) self._pipe = ProgressPipe(self, self._ui.pushButton, timeout=1000, final_text=tr("Sent"), final_timeout=500) self._pipe.pipe_finished.connect(self._on_pipe_finished) if self._ui.checkBox.isChecked(): self._pipe.add_task(tr("Compressing"), self._archive_logs()) self._pipe.add_task(tr("Uploading"), self._upload_file()) self._pipe.add_task(tr("Sending"), self._send_message()) self._pipe.start() def _on_pipe_finished(self): self._clear_feedback_flag() self.close() def _clear_pipe_state(self): self._dialog.setEnabled(True) try: self._pipe.pipe_finished.disconnect(self._on_pipe_finished) except Exception as e: logger.warning("Can't disconnect signal: %s", e) self._ui.pushButton.setText(tr("SEND")) def _send_message(self): def send(log_file_name=""): logger.debug("Support compressed log_file_name %s", log_file_name) if self._selected_index not in self.SUBJECT: logger.warning("Attempt to send message to support " "with invalid subject") return subject = self.SUBJECT[self._selected_index] res = self._parent.web_api.send_support_message( subject, self._ui.plainTextEdit.document().toPlainText(), log_file_name) was_error = False msg = tr("Can't send message to support") if res and "result" in res: if res["result"] != "success": was_error = True msg = str(res.get("info", msg)) else: was_error = True if was_error: self._parent.show_tray_notification(msg) self._sending_error.emit() raise SendingError(msg) return send def _archive_logs(self): def archive(): # uses function attributes to track progress # archive.size, archive.progress, archive.stop logs_dir = get_bases_dir(self._config.sync_directory) log_files = glob("{}{}*.log".format(logs_dir, os.sep)) log_sizes = list(map(os.path.getsize, log_files)) # mark overall size archive.size = sum(log_sizes) old_archives = glob("{}{}2*_logs.zip".format(logs_dir, os.sep)) try: list(map(remove_file, old_archives)) except Exception as e: logger.warning("Can't delete old archives. Reason: (%s)", e) if get_free_space(logs_dir) < archive.size // 5: # archive.size // 5 is approx future archive size msg = tr("Insufficient disk space to archive logs. " "Please clean disk") self._parent.show_tray_notification(msg) self._sending_error.emit() raise SendingError(msg) archive_name = time.strftime('%Y%m%d_%H%M%S_logs.zip') archive_path = "{}{}{}".format(logs_dir, os.sep, archive_name) archive_dir = op.dirname(archive_path) f = zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) try: with cwd(archive_dir): for i, log_file in enumerate(log_files): if not op.isfile(log_file): continue f.write(op.basename(log_file)) # mark progress archive.progress += log_sizes[i] if archive.stop: return except Exception as e: msg = tr("Can't archive logs.") logger.warning(msg + " Reason: (%s)", e) self._parent.show_tray_notification(msg) self._sending_error.emit() raise SendingError(msg) finally: f.close() if archive.stop: remove_file(archive_path) return archive_path return archive def _upload_file(self): def upload(path): # uses function attributes to track progress # upload.size, upload.progress, upload.stop upload.size = op.getsize(path) res = self._parent.web_api.upload_file(path, "application/zip", callback) was_error = False msg = tr("Can't upload archive file") if res and "result" in res: if res["result"] == "success": filename = res.get("file_name", "") else: was_error = True msg = str(res.get("info", msg)) else: was_error = True if was_error and not upload.stop: self._parent.show_tray_notification(msg) self._sending_error.emit() raise SendingError(msg) remove_file(path) return filename def callback(monitor): upload.progress = monitor.bytes_read if upload.stop: raise SendingError("Stopped") return upload def _on_link_activated(self): open_link(self._parent.get_help_uri())() self.close() def _check_feedback_needed(self): if self._feedback_timer.isActive(): return start_date = get_init_done() now = datetime.datetime.now() logger.debug("Start date is %s", start_date) if start_date is None: return interval = (start_date - now) \ + datetime.timedelta(days=self.DAYS_TO_FEEDBACK) if interval.total_seconds() <= 0: logger.debug("Feedback form date time is now") self._show_feedback_form() else: self._feedback_timer.setInterval(interval.seconds * 1000) self._feedback_timer.start() logger.debug("Feedback form date time is %s", now + interval) def _show_feedback_form(self): if self._is_opened: self._feedback_timer.setInterval(self.SHORT_FEEDBACK_INTERVAL) self._feedback_timer.start() return self._feedback_mode = True self._selected_index = 3 self._dialog.closeEvent = self._close_event window_title = self._dialog.windowTitle() label_text = self._ui.text_label.text() feedback_text = tr("Please leave your feedback for Pvtbox") self._ui.text_label.setText( "<html><head/><body><p>{}</p></body></html>".format(feedback_text)) self._dialog.setWindowTitle(tr("Feedback")) self.show() self._dialog.setWindowTitle(window_title) self._ui.text_label.setText(label_text) def _close_event(self, event): if event.spontaneous(): self._clear_feedback_flag() self._old_close_event(event) def _clear_feedback_flag(self): if self._feedback_mode: logger.debug("Feedback flag cleared") clear_init_done() self._feedback_mode = False self._dialog.closeEvent = self._old_close_event def _on_exit_request(self): if self._is_opened: self.close()
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'